/
githubmirror
/
marked
Обзор
Документация
Войти
/
githubmirror
/
marked
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/Lexer.ts
490 строк
14 KB
Jayesh Bhade
fix: keep the em/strong mask the same length as the source (#4044)
05 авг 2026, 17:51
Не верифицирован
05 авг 2026, 17:51
681373c
Код
Авторство
О чём код?
import { _Tokenizer } from './Tokenizer.ts'; import { _defaults } from './defaults.ts'; import { other, block, inline } from './rules.ts'; import type { Token, TokensList, Tokens } from './Tokens.ts'; import type { MarkedOptions } from './MarkedOptions.ts'; /** * Block Lexer */ export class _Lexer<ParserOutput = string, RendererOutput = string> { tokens: TokensList; options: MarkedOptions<ParserOutput, RendererOutput>; state: { inLink: boolean; inRawBlock: boolean; top: boolean; }; public inlineQueue: { src: string, tokens: Token[] }[]; private tokenizer: _Tokenizer<ParserOutput, RendererOutput>; constructor(options?: MarkedOptions<ParserOutput, RendererOutput>) { // TokenList cannot be created in one go this.tokens = [] as unknown as TokensList; this.tokens.links = Object.create(null); this.options = options || _defaults; this.options.tokenizer = this.options.tokenizer || new _Tokenizer<ParserOutput, RendererOutput>(); this.tokenizer = this.options.tokenizer; this.tokenizer.options = this.options; this.tokenizer.lexer = this; this.inlineQueue = []; this.state = { inLink: false, inRawBlock: false, top: true, }; const rules = { other, block: block.normal, inline: inline.normal, }; if (this.options.pedantic) { rules.block = block.pedantic; rules.inline = inline.pedantic; } else if (this.options.gfm) { rules.block = block.gfm; if (this.options.breaks) { rules.inline = inline.breaks; } else { rules.inline = inline.gfm; } } this.tokenizer.rules = rules; } /** * Expose Rules */ static get rules() { return { block, inline, }; } /** * Static Lex Method */ static lex<ParserOutput = string, RendererOutput = string>(src: string, options?: MarkedOptions<ParserOutput, RendererOutput>) { const lexer = new _Lexer<ParserOutput, RendererOutput>(options); return lexer.lex(src); } /** * Static Lex Inline Method */ static lexInline<ParserOutput = string, RendererOutput = string>(src: string, options?: MarkedOptions<ParserOutput, RendererOutput>) { const lexer = new _Lexer<ParserOutput, RendererOutput>(options); return lexer.inlineTokens(src); } /** * Preprocessing */ lex(src: string) { src = src.replace(other.carriageReturn, '\n'); this.blockTokens(src, this.tokens); for (let i = 0; i < this.inlineQueue.length; i++) { const next = this.inlineQueue[i]; this.inlineTokens(next.src, next.tokens); } this.inlineQueue = []; return this.tokens; } /** * Lexing */ blockTokens(src: string, tokens?: Token[], lastParagraphClipped?: boolean): Token[]; blockTokens(src: string, tokens?: TokensList, lastParagraphClipped?: boolean): TokensList; blockTokens(src: string, tokens: Token[] = [], lastParagraphClipped = false) { this.tokenizer.lexer = this; if (this.options.pedantic) { src = src.replace(other.tabCharGlobal, ' ').replace(other.spaceLine, ''); } let srcLength = Infinity; while (src) { if (src.length < srcLength) { srcLength = src.length; } else { this.infiniteLoopError(src.charCodeAt(0)); break; } let token: Tokens.Generic | undefined; if (this.options.extensions?.block?.some((extTokenizer) => { if (token = extTokenizer.call({ lexer: this }, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); return true; } return false; })) { continue; } // newline if (token = this.tokenizer.space(src)) { src = src.substring(token.raw.length); const lastToken = tokens.at(-1); if (token.raw.length === 1 && lastToken !== undefined) { // if there's a single \n as a spacer, it's terminating the last line, // so move it there so that we don't get unnecessary paragraph tags lastToken.raw += '\n'; } else { tokens.push(token); } continue; } // code if (token = this.tokenizer.code(src)) { src = src.substring(token.raw.length); const lastToken = tokens.at(-1); // An indented code block cannot interrupt a paragraph. if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { lastToken.raw += (lastToken.raw.endsWith('\n') ? '' : '\n') + token.raw; lastToken.text += '\n' + token.text; this.inlineQueue.at(-1)!.src = lastToken.text; } else { tokens.push(token); } continue; } // fences if (token = this.tokenizer.fences(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // heading if (token = this.tokenizer.heading(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // hr if (token = this.tokenizer.hr(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // blockquote if (token = this.tokenizer.blockquote(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // list if (token = this.tokenizer.list(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // html if (token = this.tokenizer.html(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // def if (token = this.tokenizer.def(src)) { src = src.substring(token.raw.length); const lastToken = tokens.at(-1); if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') { lastToken.raw += (lastToken.raw.endsWith('\n') ? '' : '\n') + token.raw; lastToken.text += '\n' + token.raw; this.inlineQueue.at(-1)!.src = lastToken.text; } else if (!this.tokens.links[token.tag]) { this.tokens.links[token.tag] = { href: token.href, title: token.title, }; tokens.push(token); } continue; } // table (gfm) if (token = this.tokenizer.table(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // lheading if (token = this.tokenizer.lheading(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // top-level paragraph // prevent paragraph consuming extensions by clipping 'src' to extension start let cutSrc = src; if (this.options.extensions?.startBlock) { let startIndex = Infinity; const tempSrc = src.slice(1); let tempStart; this.options.extensions.startBlock.forEach((getStartIndex) => { tempStart = getStartIndex.call({ lexer: this }, tempSrc); if (typeof tempStart === 'number' && tempStart >= 0) { startIndex = Math.min(startIndex, tempStart); } }); if (startIndex < Infinity && startIndex >= 0) { cutSrc = src.substring(0, startIndex + 1); } } if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { const lastToken = tokens.at(-1); if (lastParagraphClipped && lastToken?.type === 'paragraph') { lastToken.raw += (lastToken.raw.endsWith('\n') ? '' : '\n') + token.raw; lastToken.text += '\n' + token.text; this.inlineQueue.pop(); this.inlineQueue.at(-1)!.src = lastToken.text; } else { tokens.push(token); } lastParagraphClipped = cutSrc.length !== src.length; src = src.substring(token.raw.length); continue; } // text if (token = this.tokenizer.text(src)) { src = src.substring(token.raw.length); const lastToken = tokens.at(-1); if (lastToken?.type === 'text') { lastToken.raw += (lastToken.raw.endsWith('\n') ? '' : '\n') + token.raw; lastToken.text += '\n' + token.text; this.inlineQueue.pop(); this.inlineQueue.at(-1)!.src = lastToken.text; } else { tokens.push(token); } continue; } if (src) { this.infiniteLoopError(src.charCodeAt(0)); break; } } this.state.top = true; return tokens; } inline(src: string, tokens: Token[] = []) { this.inlineQueue.push({ src, tokens }); return tokens; } /** * Lexing/Compiling */ inlineTokens(src: string, tokens: Token[] = []): Token[] { this.tokenizer.lexer = this; // String with links masked to avoid interference with em and strong let maskedSrc = src; // Mask out reflinks if (this.tokens.links) { const links = Object.keys(this.tokens.links); if (links.length > 0) { maskedSrc = maskedSrc.replace(this.tokenizer.rules.inline.reflinkSearch, match0 => links.includes(match0.slice(match0.lastIndexOf('[') + 1, -1)) ? '[' + 'a'.repeat(match0.length - 2) + ']' : match0); } } // Mask out escaped characters. // Every mask must keep the length it replaces: emStrong and del line // maskedSrc up with src by slicing from the end. `anyPunctuation` matches // unicode punctuation, so an escaped astral character is 3 code units. maskedSrc = maskedSrc.replace(this.tokenizer.rules.inline.anyPunctuation, match0 => '+'.repeat(match0.length)); // Mask out other blocks maskedSrc = maskedSrc.replace(this.tokenizer.rules.inline.blockSkip, (match0, _link, context) => { const offset = context ? context.length : 0; return match0.slice(0, offset) + '[' + 'a'.repeat(match0.length - offset - 2) + ']'; }); // Mask out blocks from extensions maskedSrc = this.options.hooks?.emStrongMask?.call({ lexer: this }, maskedSrc) ?? maskedSrc; let keepPrevChar = false; let prevChar = ''; let srcLength = Infinity; while (src) { if (src.length < srcLength) { srcLength = src.length; } else { this.infiniteLoopError(src.charCodeAt(0)); break; } if (!keepPrevChar) { prevChar = ''; } keepPrevChar = false; let token: Tokens.Generic | undefined; // extensions if (this.options.extensions?.inline?.some((extTokenizer) => { if (token = extTokenizer.call({ lexer: this }, src, tokens)) { src = src.substring(token.raw.length); tokens.push(token); return true; } return false; })) { continue; } // escape if (token = this.tokenizer.escape(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // tag if (token = this.tokenizer.tag(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // link if (token = this.tokenizer.link(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // reflink, nolink if (token = this.tokenizer.reflink(src, this.tokens.links)) { src = src.substring(token.raw.length); const lastToken = tokens.at(-1); if (token.type === 'text' && lastToken?.type === 'text') { lastToken.raw += token.raw; lastToken.text += token.text; } else { tokens.push(token); } continue; } // em & strong if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // code if (token = this.tokenizer.codespan(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // br if (token = this.tokenizer.br(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // del (gfm) if (token = this.tokenizer.del(src, maskedSrc, prevChar)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // autolink if (token = this.tokenizer.autolink(src)) { src = src.substring(token.raw.length); tokens.push(token); continue; } // url (gfm) if (!this.state.inLink && (token = this.tokenizer.url(src))) { src = src.substring(token.raw.length); tokens.push(token); continue; } // text // prevent inlineText consuming extensions by clipping 'src' to extension start let cutSrc = src; if (this.options.extensions?.startInline) { let startIndex = Infinity; const tempSrc = src.slice(1); let tempStart; this.options.extensions.startInline.forEach((getStartIndex) => { tempStart = getStartIndex.call({ lexer: this }, tempSrc); if (typeof tempStart === 'number' && tempStart >= 0) { startIndex = Math.min(startIndex, tempStart); } }); if (startIndex < Infinity && startIndex >= 0) { cutSrc = src.substring(0, startIndex + 1); } } if (token = this.tokenizer.inlineText(cutSrc)) { src = src.substring(token.raw.length); if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started prevChar = token.raw.slice(-1); } keepPrevChar = true; const lastToken = tokens.at(-1); if (lastToken?.type === 'text') { lastToken.raw += token.raw; lastToken.text += token.text; } else { tokens.push(token); } continue; } if (src) { this.infiniteLoopError(src.charCodeAt(0)); break; } } return tokens; } private infiniteLoopError(byte: number) { const errMsg = 'Infinite loop on byte: ' + byte; if (this.options.silent) { console.error(errMsg); } else { throw new Error(errMsg); } } }