Skip to content

Commit 5587562

Browse files
authored
DISCOCD-101 Fix multiple snippets parsing error (#449)
* only collect meta lines for the current snippet * format * revert indentation * trying to fix the git compare * increment state.line for invalid cases * add test * add changeset * format
1 parent 3a1f525 commit 5587562

3 files changed

Lines changed: 131 additions & 48 deletions

File tree

.changeset/rich-aliens-fold.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stackoverflow/stacks-editor": patch
3+
---
4+
5+
fix parsing of multiple snippets

plugins/official/stack-snippets/src/schema.ts

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
RawContext,
99
validateMetaLines,
1010
validSnippetRegex,
11+
MetaLine,
1112
} from "./common";
1213
import { Node as ProseMirrorNode, NodeSpec } from "prosemirror-model";
1314

@@ -86,6 +87,9 @@ const parseSnippetBlockForMarkdownIt: MarkdownIt.ParserBlock.RuleBlock = (
8687
}
8788

8889
let rawMetaLines: RawContext[] = [];
90+
let inSnippet = false;
91+
let snippetBegin: MetaLine | null = null;
92+
let currentLangLines: RawContext[] = [];
8993

9094
//Next up, we want to find and test all the <!-- --> blocks we find.
9195
for (let i = startLine; i < endLine; i++) {
@@ -97,59 +101,99 @@ const parseSnippetBlockForMarkdownIt: MarkdownIt.ParserBlock.RuleBlock = (
97101
if (!validSnippetRegex.test(line)) {
98102
continue;
99103
}
100-
rawMetaLines = [...rawMetaLines, { line, index: i }];
101-
}
102104

103-
const metaLines = rawMetaLines.map(mapMetaLine).filter((m) => m != null);
104-
const validationResult = validateMetaLines(metaLines);
105+
const metaLine = mapMetaLine({ line, index: i });
106+
if (!metaLine) {
107+
continue;
108+
}
109+
110+
if (metaLine.type === "begin") {
111+
if (inSnippet) {
112+
// Found a new begin while still in a snippet - invalid state
113+
state.line = i + 1;
114+
return false;
115+
}
116+
inSnippet = true;
117+
snippetBegin = metaLine;
118+
rawMetaLines = [{ line, index: i }];
119+
currentLangLines = [];
120+
} else if (metaLine.type === "lang") {
121+
if (!inSnippet) {
122+
state.line = i + 1;
123+
return false;
124+
}
125+
currentLangLines.push({ line, index: i });
126+
rawMetaLines.push({ line, index: i });
127+
} else if (metaLine.type === "end" && inSnippet) {
128+
rawMetaLines.push({ line, index: i });
129+
130+
const metaLines = rawMetaLines
131+
.map(mapMetaLine)
132+
.filter((m) => m != null);
133+
const validationResult = validateMetaLines(metaLines);
134+
135+
//We now know this is a valid snippet. Last call before we start processing
136+
if (silent || !validationResult.valid) {
137+
state.line = i + 1;
138+
return validationResult.valid;
139+
}
140+
141+
// Create the snippet tokens
142+
const openToken = state.push("stack_snippet_open", "code", 1);
143+
// This value is not serialized, and so is different on every new session of Rich Text (i.e. every mode switch)
144+
openToken.attrSet("id", Utils.generateRandomId());
145+
if (!snippetBegin || snippetBegin.type !== "begin") {
146+
state.line = i + 1;
147+
return false;
148+
}
149+
openToken.attrSet("hide", snippetBegin.hide);
150+
openToken.attrSet("console", snippetBegin.console);
151+
openToken.attrSet("babel", snippetBegin.babel);
152+
openToken.attrSet(
153+
"babelPresetReact",
154+
snippetBegin.babelPresetReact
155+
);
156+
openToken.attrSet("babelPresetTS", snippetBegin.babelPresetTS);
157+
158+
// Sort and process language blocks
159+
const langSort = currentLangLines.sort((a, b) => a.index - b.index);
160+
161+
for (let j = 0; j < langSort.length; j++) {
162+
const langMeta = mapMetaLine(langSort[j]);
163+
if (!langMeta || langMeta.type !== "lang") continue;
164+
165+
//Use the beginning of the next block to establish the end of this one, or the end of the snippet
166+
const langEnd =
167+
j + 1 == langSort.length ? i : langSort[j + 1].index;
168+
//Start after the header of the lang block (+1) and the following empty line (+1)
169+
//End on the beginning of the next metaLine, less the preceding empty line (-1)
170+
//All lang blocks are forcefully indented 4 spaces, so cleave those away.
171+
const langBlock = state.getLines(
172+
langSort[j].index + 2,
173+
langEnd - 1,
174+
4,
175+
false
176+
);
177+
const langToken = state.push("stack_snippet_lang", "code", 1);
178+
langToken.content = langBlock;
179+
langToken.map = [langSort[j].index, langEnd];
180+
langToken.attrSet("language", langMeta.language);
181+
}
105182

106-
//We now know this is a valid snippet. Last call before we start processing
107-
if (silent || !validationResult.valid) {
108-
return validationResult.valid;
183+
state.push("stack_snippet_close", "code", -1);
184+
state.line = i + 1;
185+
186+
return true;
187+
}
109188
}
110189

111-
//A valid block must start with a begin and end, so cleave the opening and closing from the lines
112-
const begin = metaLines.shift();
113-
if (begin.type !== "begin") return false;
114-
const end = metaLines.pop();
115-
if (end.type !== "end") return false;
116-
117-
//The rest must be langs, sort them by index
118-
const langSort = metaLines
119-
.filter((m) => m.type == "lang") //Not strictly necessary, but useful for typing
120-
.sort((a, b) => a.index - b.index);
121-
if (!langSort.every((l) => l.type === "lang")) return false;
122-
123-
const openToken = state.push("stack_snippet_open", "code", 1);
124-
// This value is not serialized, and so is different on every new session of Rich Text (i.e. every mode switch)
125-
openToken.attrSet("id", Utils.generateRandomId());
126-
openToken.attrSet("hide", begin.hide);
127-
openToken.attrSet("console", begin.console);
128-
openToken.attrSet("babel", begin.babel);
129-
openToken.attrSet("babelPresetReact", begin.babelPresetReact);
130-
openToken.attrSet("babelPresetTS", begin.babelPresetTS);
131-
132-
for (let i = 0; i < langSort.length; i++) {
133-
//Use the beginning of the next block to establish the end of this one, or the end of the snippet
134-
const langEnd =
135-
i + 1 == langSort.length ? end.index : langSort[i + 1].index;
136-
//Start after the header of the lang block (+1) and the following empty line (+1)
137-
//End on the beginning of the next metaLine, less the preceding empty line (-1)
138-
//All lang blocks are forcefully indented 4 spaces, so cleave those away.
139-
const langBlock = state.getLines(
140-
langSort[i].index + 2,
141-
langEnd - 1,
142-
4,
143-
false
144-
);
145-
const langToken = state.push("stack_snippet_lang", "code", 1);
146-
langToken.content = langBlock;
147-
langToken.map = [langSort[i].index, langEnd];
148-
langToken.attrSet("language", langSort[i].language);
190+
// If we're still in a snippet at the end, it means we never found an end marker
191+
if (inSnippet) {
192+
state.line = endLine;
193+
return false;
149194
}
150-
state.push("stack_snippet_close", "code", -1);
151-
state.line = end.index + 1;
152-
return true;
195+
196+
return false;
153197
};
154198

155199
export const stackSnippetRichTextNodeSpec: { [name: string]: NodeSpec } = {

plugins/official/stack-snippets/test/markdownit-plugin.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { stackSnippetPlugin } from "../src/schema";
33
import {
44
invalidSnippetRenderCases,
55
validSnippetRenderCases,
6+
validBegin,
7+
validJs,
8+
validEnd,
69
} from "./stack-snippet-helpers";
710

811
describe("stackSnippetPlugin (Markdown-it)", () => {
@@ -46,4 +49,35 @@ describe("stackSnippetPlugin (Markdown-it)", () => {
4649
}
4750
}
4851
);
52+
53+
it("should correctly parse multiple consecutive snippets", () => {
54+
const multipleSnippets = `${validBegin}${validJs}${validEnd}
55+
56+
Some text in between snippets.
57+
58+
${validBegin}${validJs}${validEnd}`;
59+
60+
const tokens = mdit.parse(multipleSnippets, {});
61+
62+
// We expect:
63+
// - First snippet: open + lang + close (3 tokens)
64+
// - Paragraph with text (3 tokens: paragraph_open, inline, paragraph_close)
65+
// - Second snippet: open + lang + close (3 tokens)
66+
expect(tokens).toHaveLength(9);
67+
68+
// First snippet
69+
expect(tokens[0].type).toBe("stack_snippet_open");
70+
expect(tokens[1].type).toBe("stack_snippet_lang");
71+
expect(tokens[2].type).toBe("stack_snippet_close");
72+
73+
// Text in between
74+
expect(tokens[3].type).toBe("paragraph_open");
75+
expect(tokens[4].type).toBe("inline");
76+
expect(tokens[5].type).toBe("paragraph_close");
77+
78+
// Second snippet
79+
expect(tokens[6].type).toBe("stack_snippet_open");
80+
expect(tokens[7].type).toBe("stack_snippet_lang");
81+
expect(tokens[8].type).toBe("stack_snippet_close");
82+
});
4983
});

0 commit comments

Comments
 (0)