From 1028bc4c2ba5bf5656b06c29ea8ba019f548df05 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Mon, 16 Feb 2026 01:47:44 -0800 Subject: [PATCH] some more meowing --- README.md | 10 ++--- lib/clover.test.ts | 81 +++++++++++++++++++++++-------------- src/lib.rs | 2 +- src/plugin/clover.rs | 95 ++++++++++++++++++++++++++++---------------- 4 files changed, 117 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index d923cdf0a7220eb6e63f7461f6c759ade803633e..c85b5fc982972850bcc85a0fc44ba4138059c4dc 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ leveraging the existing ecosystem. > - [Install](#install) > - [Components](#components) > - [Outline / Table of Contents](#outline-table-of-contents) -> - [Frontmatter](#outline-table-of-contents) +> - [Frontmatter](#frontmatter) > - [Comments](#comments) > - [Paragraph Detection](#paragraph-detection) > - [Static Statements](#static-statements) @@ -73,7 +73,7 @@ i love being alive. ${'<3'} from ${new Date().getFullYear()}. ## Install -Markdown is distributed on [NPM](http://npmjs.com/markodown) and +Markodown is distributed on [NPM](http://npmjs.com/markodown) and [JSR](https://jsr.io/@clo/markodown). The compiler runs anywhere JS+WASM runs. ```sh @@ -82,8 +82,8 @@ npx jsr add @clo/markodown ``` The compiler can be directly used from `transform`, and there are also plugins -for Rollup/Rolldown/Vite and esbuild. For example, configure Markdown with Marko -Run: +for Rollup/Rolldown/Vite and esbuild. For example, configure Markodown with +Marko Run: ```ts import marko from "@marko/run/vite"; @@ -117,7 +117,7 @@ add complex content to your pages. made with love... (c) ${new Date().getFullYear()} - + ``` [tag resolution]: https://markojs.com/docs/reference/custom-tag#relative-custom-tags diff --git a/lib/clover.test.ts b/lib/clover.test.ts index 310f02b6077d060f61cdb520a67f674459c3cfdd..b2d370350a8e20d5fae3ce5390b1005efe146d4a 100644 --- a/lib/clover.test.ts +++ b/lib/clover.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { transform, CloverQuestionExtensions } from "./mod.ts"; +import { describe, expect, it } from "vitest"; +import { CloverQuestionExtensions, transform } from "./mod.ts"; const cloverExtensions: CloverQuestionExtensions = { question: "q-block", @@ -32,9 +32,9 @@ describe("clover extensions", () => { }); expect(result.success).toBe(true); if (result.success) { - expect(result.text).toContain("
"); - expect(result.text).toContain("first question"); - expect(result.text).toContain("second question"); + expect(result.text).toContain( + "first question
second question
", + ); } }); @@ -73,7 +73,7 @@ describe("clover extensions", () => { expect(result.success).toBe(true); if (result.success) { expect(result.text).toContain( - 'content' + 'content', ); } }); @@ -102,7 +102,7 @@ describe("clover extensions", () => { expect(result.success).toBe(true); if (result.success) { expect(result.text).toContain( - "its-snowing" + "its-snowing", ); } }); @@ -117,7 +117,7 @@ describe("clover extensions", () => { if (result.success) { expect(result.text).toContain("first-one"); expect(result.text).toContain( - "second-one" + "second-one", ); } }); @@ -131,7 +131,7 @@ describe("clover extensions", () => { expect(result.success).toBe(true); if (result.success) { expect(result.text).toContain( - "valid-slug123" + "valid-slug123", ); expect(result.text).toContain("@INVALID"); expect(result.text).not.toContain("INVALID"); @@ -161,7 +161,7 @@ describe("clover extensions", () => { expect(result.success).toBe(true); if (result.success) { expect(result.text).toContain( - "2602142011" + "2602142011", ); } }); @@ -175,7 +175,7 @@ describe("clover extensions", () => { expect(result.success).toBe(true); if (result.success) { expect(result.text).toContain( - "260214201112" + "260214201112", ); } }); @@ -205,31 +205,31 @@ describe("clover extensions", () => { expect(result.success).toBe(true); if (result.success) { expect(result.text).toContain( - "2602142011" + "2602142011", ); expect(result.text).toContain( - "2602142012" + "2602142012", ); } }); }); describe("labelled redactions", () => { - it("transforms ##label## to redaction element", () => { + it("transforms #label# to redaction element", () => { const result = transform({ - source: "the ##secret name## is hidden", + source: "the #name# is hidden", markdownOnly: true, cloverExtensions, }); expect(result.success).toBe(true); if (result.success) { - expect(result.text).toContain("secret name"); + expect(result.text).toContain("name"); } }); it("handles hyphens and underscores in labels", () => { const result = transform({ - source: "##my-secret_label##", + source: "#my-secret_label#", markdownOnly: true, cloverExtensions, }); @@ -241,21 +241,21 @@ describe("clover extensions", () => { it("does not match empty labels", () => { const result = transform({ - source: "text #### more", + source: "text ## ### #### more", markdownOnly: true, cloverExtensions, }); expect(result.success).toBe(true); if (result.success) { - expect(result.text).not.toContain(""); - // #### in the middle of text stays as-is - expect(result.text).toContain("####"); + expect(result.text).not.toContain(""); + // ## in the middle of text stays as-is + expect(result.text).toContain("##"); } }); it("handles multiple redactions", () => { const result = transform({ - source: "##first## and ##second##", + source: "#first# and #second#", markdownOnly: true, cloverExtensions, }); @@ -273,7 +273,7 @@ describe("clover extensions", () => { source: `q: what is @some-artifact about? q: also see #2602142011 -The ##secret## is revealed. +The #secret# is revealed. @html
`, markdownOnly: true, @@ -281,12 +281,12 @@ The ##secret## is revealed. }); expect(result.success).toBe(true); if (result.success) { - expect(result.text).toContain(""); - expect(result.text).toContain("some-artifact"); - expect(result.text).toContain("2602142011"); - expect(result.text).toContain("secret"); - expect(result.text).toContain('
'); - expect(result.text).toContain("
"); // between consecutive q: lines + expect(result.text).toBe( + `what is some-artifact about?
also see 2602142011
+

The secret is revealed.

+
+`, + ); } }); @@ -306,7 +306,8 @@ The ##secret## is revealed. describe("without cloverExtensions", () => { it("does not parse clover syntax when extensions not provided", () => { const result = transform({ - source: "q: not a question\n@not-artifact\n#2602142011\n##not-redacted##", + source: + "q: not a question\n@not-artifact\n#2602142011\n##not-redacted##", markdownOnly: true, }); expect(result.success).toBe(true); @@ -320,3 +321,23 @@ The ##secret## is revealed. }); }); }); + +it("sample cases", () => { + expect( + transform({ + source: `q: how is/was your valentine's day? + +it was overall good but i don't understand why i'm loved as much as i am. but i appreciate her in every way. also every time i look away, my [frieren collection](/file/q+a/frieren.jpeg) is slowly growing. + +@html

[clo] is rly cutee and nice to me so i love her alot and pat her head a lot and i miss [clo] meow hug her shes rly cool and does rly cool things and shes cute and likes frieren and meow

+ +oh oops i left my answering machine out.`, + markdownOnly: true, + cloverExtensions, + }).text, + ).toBe(`how is/was your valentine’s day? +

it was overall good but i don’t understand why i’m loved as much as i am. but i appreciate her in every way. also every time i look away, my frieren collection is slowly growing.

+

[clo] is rly cutee and nice to me so i love her alot and pat her head a lot and i miss [clo] meow hug her shes rly cool and does rly cool things and shes cute and likes frieren and meow

+

oh oops i left my answering machine out.

+`); +}); diff --git a/src/lib.rs b/src/lib.rs index 92f594995249a102a76a3f7e0d40f45f2bb75b33..31e32cffd92c1001538f219b7da125a9e9fc40ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ pub struct CloverExtensions { pub artifact_ref: String, /// Element name for question refs. `#2602142011` -> `2602142011` pub question_ref: String, - /// Element name for labelled redactions. `##name##` -> `name` + /// Element name for labelled redactions. `#name#` -> `name` pub labelled_redaction: String, } diff --git a/src/plugin/clover.rs b/src/plugin/clover.rs index 32e7d6771dd720de31a3dd4e5e1ec1c8f2ca6b40..e2e077dcd1663611c4527438b9745066bab9f5c4 100644 --- a/src/plugin/clover.rs +++ b/src/plugin/clover.rs @@ -21,11 +21,15 @@ pub struct CloverConfig(pub CloverExtensions); impl MarkdownItExt for CloverConfig {} +/// Marker used to separate question lines (will become
) +const QUESTION_LINE_BREAK: &str = "\x1F"; + /// Register clover extension rules pub fn add(md: &mut MarkdownIt, config: CloverExtensions) { md.ext.insert(CloverConfig(config)); md.block.add_rule::(); md.block.add_rule::(); + md.inline.add_rule::(); md.inline.add_rule::(); md.inline.add_rule::(); md.inline.add_rule::(); @@ -38,15 +42,10 @@ pub fn add(md: &mut MarkdownIt, config: CloverExtensions) { #[derive(Debug)] pub struct QuestionBlock { pub element: String, - /// Whether this question follows another question (needs
prefix) - pub needs_break: bool, } impl NodeValue for QuestionBlock { fn render(&self, node: &Node, fmt: &mut dyn Renderer) { - if self.needs_break { - fmt.text_raw("
"); - } fmt.text_raw(&format!("<{}>", self.element)); fmt.contents(&node.children); fmt.text_raw(&format!("", self.element)); @@ -54,6 +53,32 @@ impl NodeValue for QuestionBlock { } } +/// Node for line breaks within question blocks +#[derive(Debug)] +pub struct QuestionLineBreak; + +impl NodeValue for QuestionLineBreak { + fn render(&self, _node: &Node, fmt: &mut dyn Renderer) { + fmt.text_raw("
"); + } +} + +/// Inline rule to convert line break markers to
+pub struct QuestionLineBreakRule; + +impl InlineRule for QuestionLineBreakRule { + const MARKER: char = '\x1F'; + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let input = &state.src[state.pos..state.pos_max]; + if input.starts_with(QUESTION_LINE_BREAK) { + Some((Node::new(QuestionLineBreak), 1)) + } else { + None + } + } +} + pub struct QuestionBlockRule; impl BlockRule for QuestionBlockRule { @@ -64,31 +89,36 @@ impl BlockRule for QuestionBlockRule { return None; } - let line = state.get_line(state.line); - if !line.starts_with("q: ") { + let first_line = state.get_line(state.line); + if !first_line.starts_with("q: ") { return None; } - // Check if previous line was also a question block - let needs_break = if state.line > 0 { - let prev_line = state.get_line(state.line - 1); - prev_line.starts_with("q: ") - } else { - false - }; + // Consume all consecutive q: lines + let start_line = state.line; + let mut line_count = 0; + let mut contents = Vec::new(); + + while start_line + line_count < state.line_max { + let line = state.get_line(start_line + line_count); + if !line.starts_with("q: ") { + break; + } + contents.push(&line[3..]); + line_count += 1; + } + + // Join lines with marker - converted to
by QuestionLineBreakRule + let combined = contents.join(QUESTION_LINE_BREAK); + let mapping = vec![(0, state.line_offsets[start_line].first_nonspace + 3)]; let mut node = Node::new(QuestionBlock { element: config.0.question.clone(), - needs_break, }); - - // Parse inline content (skip "q: " prefix = 3 chars) - let content = line[3..].to_owned(); - let mapping = vec![(0, state.line_offsets[state.line].first_nonspace + 3)]; node.children - .push(Node::new(InlineRoot::new(content, mapping))); + .push(Node::new(InlineRoot::new(combined, mapping))); - Some((node, 1)) + Some((node, line_count)) } } @@ -216,11 +246,6 @@ impl InlineRule for QuestionRefRule { return None; } - // Check it's not ## (that's labelled redaction) - if input.starts_with("##") { - return None; - } - let rest = &input[1..]; let digit_count = rest.chars().take_while(|c| c.is_ascii_digit()).count(); @@ -249,7 +274,7 @@ impl InlineRule for QuestionRefRule { } // ============================================================================= -// Labelled Redaction: `##label##` +// Labelled Redaction: `#label#` // ============================================================================= #[derive(Debug)] @@ -273,24 +298,24 @@ impl InlineRule for LabelledRedactionRule { let config = state.md.ext.get::()?; let input = &state.src[state.pos..state.pos_max]; - if !input.starts_with("##") { + if !input.starts_with("#") { return None; } - let rest = &input[2..]; + let rest = &input[1..]; - // Find closing ## - let label_end = rest.find("##")?; + // Find closing # + let label_end = rest.find("#")?; if label_end == 0 { return None; // Empty label } let label = &rest[..label_end]; - // Label should be simple identifier-like (alphanumeric, hyphens, underscores, spaces) + // Label should be simple identifier-like (alphanumeric, hyphens, underscores) if !label .chars() - .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == ' ') + .all(|c| c.is_alphanumeric() || c == '-' || c == '_') { return None; } @@ -300,7 +325,7 @@ impl InlineRule for LabelledRedactionRule { label: label.to_string(), }); - // ## + label + ## - Some((node, 2 + label_end + 2)) + // # + label + # + Some((node, label_end + 2)) } } -- 2.54.0