hololake-system-architecture/product-source/hololake-platform/src/utils/markdownFences.ts

92 lines
2.5 KiB
TypeScript
Raw Normal View History

export type MarkdownFenceCharacter = '`' | '~'
export interface MarkdownFence {
character: MarkdownFenceCharacter
length: number
}
export interface MarkdownFenceScanOptions {
closingMustEndLine?: boolean
maxLeadingSpaces?: number | null
}
const DEFAULT_MAX_LEADING_SPACES = 3
function fencePrefixPattern(options: MarkdownFenceScanOptions): RegExp {
return options.maxLeadingSpaces === null
? /^(\s*)(`{3,}|~{3,})/u
: /^( *)(`{3,}|~{3,})/u
}
function hasAllowedIndent(match: RegExpExecArray, options: MarkdownFenceScanOptions): boolean {
if (options.maxLeadingSpaces === null) return true
return (match[1]?.length ?? 0) <= (options.maxLeadingSpaces ?? DEFAULT_MAX_LEADING_SPACES)
}
function readFenceMatch(line: string, options: MarkdownFenceScanOptions): RegExpExecArray | null {
const match = fencePrefixPattern(options).exec(line)
return match && hasAllowedIndent(match, options) ? match : null
}
function fenceFromMatch(match: RegExpExecArray): MarkdownFence {
const fence = match[2] ?? ''
return {
character: fence.charAt(0) as MarkdownFenceCharacter,
length: fence.length,
}
}
export function readMarkdownFence(
line: string,
options: MarkdownFenceScanOptions = {},
): MarkdownFence | null {
const match = readFenceMatch(line, options)
return match ? fenceFromMatch(match) : null
}
function closingTailIsAllowed(
line: string,
match: RegExpExecArray,
options: MarkdownFenceScanOptions,
): boolean {
if (options.closingMustEndLine === false) return true
return /^[ \t]*$/u.test(line.slice(match[0].length))
}
function isClosingMarkdownFence(
line: string,
opening: MarkdownFence,
options: MarkdownFenceScanOptions,
): boolean {
const match = readFenceMatch(line, options)
if (!match) return false
const closing = fenceFromMatch(match)
return closing.character === opening.character
&& closing.length >= opening.length
&& closingTailIsAllowed(line, match, options)
}
export function advanceMarkdownFence(
line: string,
current: MarkdownFence | null,
options: MarkdownFenceScanOptions = {},
): MarkdownFence | null {
if (current) {
return isClosingMarkdownFence(line, current, options) ? null : current
}
return readMarkdownFence(line, options)
}
export function isInsideMarkdownFence(
markdownBeforeCursor: string,
options: MarkdownFenceScanOptions = {},
): boolean {
let fence: MarkdownFence | null = null
for (const line of markdownBeforeCursor.split(/\r?\n/u)) {
fence = advanceMarkdownFence(line, fence, options)
}
return fence !== null
}