Skip to content

Commit 870881a

Browse files
committed
fix: enhance embed token handling in paragraphs and table cells
1 parent f7418f4 commit 870881a

5 files changed

Lines changed: 463 additions & 249 deletions

File tree

src/core/render/compiler/paragraph.js

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,48 @@
11
import { helper as helperTpl } from '../tpl.js';
22

3+
function renderParagraphText(text) {
4+
if (text.startsWith('!>')) {
5+
return helperTpl('callout important', text);
6+
}
7+
if (text.startsWith('?>')) {
8+
return helperTpl('callout tip', text);
9+
}
10+
return /* html */ `<p>${text}</p>`;
11+
}
12+
313
export const paragraphCompiler = ({ renderer }) =>
4-
(renderer.paragraph = function ({ tokens }) {
5-
const text = this.parser.parseInline(tokens);
14+
(renderer.paragraph = function ({ tokens, embedTokenMap }) {
615
let result;
716

8-
if (text.startsWith('!&gt;')) {
9-
result = helperTpl('callout important', text);
10-
} else if (text.startsWith('?&gt;')) {
11-
result = helperTpl('callout tip', text);
17+
if (embedTokenMap && tokens?.length) {
18+
// Keep original inline order: plain text/link tokens stay inline, include links are replaced.
19+
const parts = [];
20+
let inlineBuffer = [];
21+
22+
const flushInlineBuffer = () => {
23+
if (!inlineBuffer.length) {
24+
return;
25+
}
26+
const text = this.parser.parseInline(inlineBuffer);
27+
parts.push(renderParagraphText(text));
28+
inlineBuffer = [];
29+
};
30+
31+
tokens.forEach((inlineToken, inlineIndex) => {
32+
const embedToken = embedTokenMap[inlineIndex];
33+
if (embedToken?.length) {
34+
flushInlineBuffer();
35+
parts.push(this.parser.parse(embedToken));
36+
} else {
37+
inlineBuffer.push(inlineToken);
38+
}
39+
});
40+
41+
flushInlineBuffer();
42+
result = parts.join('');
1243
} else {
13-
result = /* html */ `<p>${text}</p>`;
44+
const text = this.parser.parseInline(tokens);
45+
result = renderParagraphText(text);
1446
}
1547

1648
return result;

src/core/render/compiler/tableCell.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,18 @@ export const tableCellCompiler = ({ renderer }) =>
22
(renderer.tablecell = function (token) {
33
let content;
44

5-
if (token.embedTokens && token.embedTokens.length > 0) {
5+
if (token.embedTokenMap && token.tokens?.length) {
6+
// Preserve mixed content order: render inline tokens, replacing include links by position.
7+
content = '';
8+
token.tokens.forEach((inlineToken, inlineIndex) => {
9+
const embedToken = token.embedTokenMap[inlineIndex];
10+
if (embedToken?.length) {
11+
content += this.parser.parse(embedToken);
12+
} else {
13+
content += this.parser.parseInline([inlineToken]);
14+
}
15+
});
16+
} else if (token.embedTokens && token.embedTokens.length > 0) {
617
content = this.parser.parse(token.embedTokens);
718
} else {
819
content = this.parser.parseInline(token.tokens);

src/core/render/embed.js

Lines changed: 102 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,13 @@ function extractFragmentContent(text, fragment, fullLine) {
3333
}
3434

3535
function walkFetchEmbed({ embedTokens, compile, fetch }, cb) {
36-
let token;
37-
let step = 0;
38-
let count = 0;
39-
4036
if (!embedTokens.length) {
4137
return cb({});
4238
}
4339

44-
while ((token = embedTokens[step++])) {
45-
const currentToken = token;
40+
const processStep = step => {
41+
const currentToken = embedTokens[step];
4642

47-
// eslint-disable-next-line no-loop-func
4843
const next = text => {
4944
let embedToken;
5045
if (text) {
@@ -119,17 +114,21 @@ function walkFetchEmbed({ embedTokens, compile, fetch }, cb) {
119114
tokenRef: currentToken.tokenRef,
120115
});
121116

122-
if (++count >= embedTokens.length) {
117+
if (step + 1 >= embedTokens.length) {
123118
cb({});
119+
} else {
120+
processStep(step + 1);
124121
}
125122
};
126123

127-
if (token.embed.url) {
128-
get(token.embed.url).then(next);
124+
if (currentToken.embed.url) {
125+
get(currentToken.embed.url).then(next);
129126
} else {
130-
next(token.embed.html);
127+
next(currentToken.embed.html);
131128
}
132-
}
129+
};
130+
131+
processStep(0);
133132
}
134133

135134
export function prerenderEmbed({ compiler, raw = '', fetch }, done) {
@@ -143,46 +142,56 @@ export function prerenderEmbed({ compiler, raw = '', fetch }, done) {
143142
const compile = compiler._marked;
144143
let tokens = compile.lexer(raw);
145144
const embedTokens = [];
146-
const linkRE = compile.Lexer.rules.inline.normal.link;
147145
const links = tokens.links;
148146

149-
const linkMatcher = new RegExp(linkRE.source, 'g');
150-
151147
tokens.forEach((token, index) => {
152148
if (token.type === 'paragraph') {
153-
token.text = token.text.replace(
154-
linkMatcher,
155-
(src, filename, href, title) => {
156-
const embed = compiler.compileEmbed(href, title);
149+
(token.tokens || []).forEach(
150+
(
151+
/** @type {{ type: string; href: any; title: any; }} */ inlineToken,
152+
inlineIndex,
153+
) => {
154+
if (inlineToken.type !== 'link') {
155+
return;
156+
}
157+
158+
const embed = compiler.compileEmbed(
159+
inlineToken.href,
160+
inlineToken.title,
161+
);
157162
if (embed) {
158163
embedTokens.push({
159164
index,
160165
tokenRef: token,
166+
inlineIndex,
161167
embed,
162168
});
163169
}
164-
return src;
165170
},
166171
);
167172
} else if (token.type === 'table') {
168173
token.rows.forEach((row, rowIndex) => {
169174
row.forEach((cell, cellIndex) => {
170-
cell.text = cell.text.replace(
171-
linkMatcher,
172-
(src, filename, href, title) => {
173-
const embed = compiler.compileEmbed(href, title);
174-
if (embed) {
175-
embedTokens.push({
176-
index,
177-
tokenRef: token,
178-
rowIndex,
179-
cellIndex,
180-
embed,
181-
});
182-
}
183-
return src;
184-
},
185-
);
175+
(cell.tokens || []).forEach((inlineToken, inlineIndex) => {
176+
if (inlineToken.type !== 'link') {
177+
return;
178+
}
179+
180+
const embed = compiler.compileEmbed(
181+
inlineToken.href,
182+
inlineToken.title,
183+
);
184+
if (embed) {
185+
embedTokens.push({
186+
index,
187+
tokenRef: token,
188+
rowIndex,
189+
cellIndex,
190+
inlineIndex,
191+
embed,
192+
});
193+
}
194+
});
186195
});
187196
});
188197
}
@@ -192,30 +201,73 @@ export function prerenderEmbed({ compiler, raw = '', fetch }, done) {
192201
// so that we know where to insert the embedded tokens as they
193202
// are returned
194203
const moves = [];
204+
const tokenInsertState = new WeakMap();
195205
walkFetchEmbed(
196206
{ compile, embedTokens, fetch },
197207
({ embedToken, token, rowIndex, cellIndex, tokenRef }) => {
198208
if (token) {
209+
Object.assign(links, embedToken.links);
210+
199211
if (typeof rowIndex === 'number' && typeof cellIndex === 'number') {
200212
const cell = tokenRef.rows[rowIndex][cellIndex];
213+
if (typeof token.inlineIndex === 'number') {
214+
cell.embedTokenMap ||= {};
215+
const existing = cell.embedTokenMap[token.inlineIndex];
216+
cell.embedTokenMap[token.inlineIndex] = existing
217+
? existing.concat(embedToken)
218+
: embedToken;
219+
}
201220

202-
cell.embedTokens = embedToken;
221+
// Keep the flattened array for backward compatibility with older render paths.
222+
if (cell.embedTokens && cell.embedTokens.length) {
223+
cell.embedTokens = cell.embedTokens.concat(embedToken);
224+
} else {
225+
cell.embedTokens = embedToken;
226+
}
227+
} else if (tokenRef.type === 'paragraph') {
228+
if (typeof token.inlineIndex === 'number') {
229+
tokenRef.embedTokenMap ||= {};
230+
const existing = tokenRef.embedTokenMap[token.inlineIndex];
231+
tokenRef.embedTokenMap[token.inlineIndex] = existing
232+
? existing.concat(embedToken)
233+
: embedToken;
234+
}
235+
236+
// Keep a flattened form as a fallback for custom renderers.
237+
if (tokenRef.embedTokens && tokenRef.embedTokens.length) {
238+
tokenRef.embedTokens = tokenRef.embedTokens.concat(embedToken);
239+
} else {
240+
tokenRef.embedTokens = embedToken;
241+
}
203242
} else {
204-
// iterate through the array of previously inserted tokens
205-
// to determine where the current embedded tokens should be inserted
206-
let index = token.index;
207-
moves.forEach(pos => {
208-
if (index > pos.start) {
209-
index += pos.length;
210-
}
211-
});
243+
const state = tokenInsertState.get(tokenRef);
212244

213-
Object.assign(links, embedToken.links);
245+
if (state) {
246+
const insertAt = state.nextIndex;
214247

215-
tokens = tokens
216-
.slice(0, index)
217-
.concat(embedToken, tokens.slice(index + 1));
218-
moves.push({ start: index, length: embedToken.length - 1 });
248+
tokens = tokens
249+
.slice(0, insertAt)
250+
.concat(embedToken, tokens.slice(insertAt));
251+
moves.push({ start: insertAt, delta: embedToken.length });
252+
state.nextIndex = insertAt + embedToken.length;
253+
} else {
254+
// iterate through the array of previously inserted tokens
255+
// to determine where the current embedded tokens should be inserted
256+
let index = token.index;
257+
moves.forEach(pos => {
258+
if (index > pos.start) {
259+
index += pos.delta;
260+
}
261+
});
262+
263+
tokens = tokens
264+
.slice(0, index)
265+
.concat(embedToken, tokens.slice(index + 1));
266+
moves.push({ start: index, delta: embedToken.length - 1 });
267+
tokenInsertState.set(tokenRef, {
268+
nextIndex: index + embedToken.length,
269+
});
270+
}
219271
}
220272
} else {
221273
cached[raw] = tokens.concat();

0 commit comments

Comments
 (0)