diff --git a/src/markdown/index.ts b/src/markdown/index.ts index 5e15f9b..fe02629 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -61,7 +61,13 @@ const marked = new Marked() const match = rule.exec(src); if (match) { const yamlContent = match[1]?.trim() || ''; - const props = yaml.load(yamlContent) as Record || {}; + let props: Record = {}; + try { + props = (yaml.load(yamlContent) as Record) || {}; + } catch (e) { + console.error("YAML Parse Error in code-block-yaml-tag:", e); + props = { error: "Invalid YAML content" }; + } // 提取 tag 名称,默认为 tag-unknown const tagName = (props.tag as string) || 'tag-unknown'; diff --git a/src/yarn-spinner/parse/parser.ts b/src/yarn-spinner/parse/parser.ts index 75ab37b..6f5816b 100644 --- a/src/yarn-spinner/parse/parser.ts +++ b/src/yarn-spinner/parse/parser.ts @@ -87,24 +87,17 @@ class Parser { while (!this.at("NODE_START")) { const keyTok = this.take("HEADER_KEY", "Expected node header before '---'"); const valTok = this.take("HEADER_VALUE", "Expected header value"); - if (keyTok.text === "title") title = valTok.text.trim(); - if (keyTok.text === "tags") { - const raw = valTok.text.trim(); - nodeTags = raw.split(/\s+/).filter(Boolean); - } - if (keyTok.text === "when") { - // Each when: header adds one condition (can have multiple when: headers) - const raw = valTok.text.trim(); - whenConditions.push(raw); - } + // Capture &css{ ... } styles in any header value const rawVal = valTok.text.trim(); + let finalVal = valTok.text; if (rawVal.startsWith("&css{")) { // Collect until closing '}' possibly spanning multiple lines before '---' let cssContent = rawVal.replace(/^&css\{/, ""); let closed = cssContent.includes("}"); if (closed) { cssContent = cssContent.split("}")[0]; + finalVal = rawVal.replace(/^&css\{[^}]*\}/, "").trim(); } else { // Consume subsequent TEXT or HEADER_VALUE tokens until we find a '}' while (!this.at("NODE_START") && !this.at("EOF")) { @@ -114,6 +107,7 @@ class Parser { if (t.includes("}")) { cssContent += (cssContent ? "\n" : "") + t.split("}")[0]; closed = true; + finalVal = t.split("}").slice(1).join("}").trim(); break; } else { cssContent += (cssContent ? "\n" : "") + t; @@ -127,7 +121,18 @@ class Parser { } nodeCss = (cssContent || "").trim(); } - headers[keyTok.text] = valTok.text; + + if (keyTok.text === "title") title = finalVal.trim(); + if (keyTok.text === "tags") { + const raw = finalVal.trim(); + nodeTags = raw.split(/\s+/).filter(Boolean); + } + if (keyTok.text === "when") { + // Each when: header adds one condition (can have multiple when: headers) + const raw = finalVal.trim(); + whenConditions.push(raw); + } + headers[keyTok.text] = finalVal; // allow empty lines while (this.at("EMPTY")) this.i++; } @@ -156,6 +161,26 @@ class Parser { while (this.at("EMPTY")) this.i++; if (this.at(endType) || this.at("EOF")) break; + // Handle plain indentation seamlessly within blocks + if (this.at("INDENT")) { + this.take("INDENT"); + while (!this.at("DEDENT") && !this.at(endType) && !this.at("EOF")) { + while (this.at("EMPTY")) this.i++; + if (this.at("DEDENT") || this.at(endType) || this.at("EOF")) break; + + if (this.at("OPTION")) { + out.push(this.parseOptionGroup()); + continue; + } + out.push(this.parseStatement()); + } + if (this.at("DEDENT")) { + this.take("DEDENT"); + while (this.at("EMPTY")) this.i++; + } + continue; + } + if (this.at("OPTION")) { out.push(this.parseOptionGroup()); continue;