From 19de558d8605fb55488941e7da8a589cad033eed Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Sun, 29 Mar 2026 10:43:03 +0100 Subject: [PATCH] chore: refactoring code to reuse the same function in tests and app --- .changeset/gentle-ducks-peel.md | 5 ++ .../editorExtension/codeBlockExtraction.ts | 78 +++++++++++++++++++ .../editorExtension/syntaxHighlight.test.ts | 54 ++----------- .../editorExtension/syntaxHighlight.ts | 58 +------------- 4 files changed, 93 insertions(+), 102 deletions(-) create mode 100644 .changeset/gentle-ducks-peel.md create mode 100644 src/modules/syntaxHighlight/editorExtension/codeBlockExtraction.ts diff --git a/.changeset/gentle-ducks-peel.md b/.changeset/gentle-ducks-peel.md new file mode 100644 index 0000000..119d1e1 --- /dev/null +++ b/.changeset/gentle-ducks-peel.md @@ -0,0 +1,5 @@ +--- +"sqlseal": patch +--- + +fixed syntax highlighting in callouts diff --git a/src/modules/syntaxHighlight/editorExtension/codeBlockExtraction.ts b/src/modules/syntaxHighlight/editorExtension/codeBlockExtraction.ts new file mode 100644 index 0000000..1c0a536 --- /dev/null +++ b/src/modules/syntaxHighlight/editorExtension/codeBlockExtraction.ts @@ -0,0 +1,78 @@ +export interface CodeBlockMatch { + startIndex: number + content: string + /** Characters stripped from the start of each content line (e.g. "> " in callouts) */ + linePrefix?: string +} + +/** + * Extract sqlseal code blocks from markdown text, including those inside callouts. + * + * This function handles both regular code blocks and those inside Obsidian callouts + * (which are prefixed with "> " on each line). For callout blocks, the prefix is + * stripped from content lines so the SQL parser can process them cleanly. + * + * @param text - The raw markdown text to search + * @returns Array of code block matches with their content and position information + */ +export function extractCodeBlocks(text: string): CodeBlockMatch[] { + // Parsing — match code blocks with optional callout prefix (e.g. "> ") on fence lines. + // The prefix is captured so positions can be mapped back to the raw document. + const codeBlockRegex = /^([ \t]*(?:> )*)```(sqlseal)\n([\s\S]*?)^[ \t]*(?:> )*```/gm + let match + const results: CodeBlockMatch[] = [] + + while ((match = codeBlockRegex.exec(text)) !== null) { + const fencePrefix = match[1] || '' + const langTag = match[2] + const rawContent = match[3] + const blockStart = match.index + const langTagEnd = blockStart + fencePrefix.length + 3 + langTag.length // prefix + ``` + langTag + const contentStart = langTagEnd + 1 // +1 for the newline after the opening fence + + if (!fencePrefix) { + results.push({ content: rawContent, startIndex: contentStart }) + } else { + // Strip the callout prefix from each content line so the grammar can parse it cleanly. + const strippedLines = rawContent.split('\n').map(line => + line.startsWith(fencePrefix) ? line.slice(fencePrefix.length) : line + ) + results.push({ + content: strippedLines.join('\n'), + startIndex: contentStart, + linePrefix: fencePrefix + }) + } + } + + return results +} + +/** + * Map a position in stripped content back to the raw document position. + * + * For callout blocks, each line has a prefix (e.g. "> ") that was removed before + * parsing. This function restores that offset: for a position on line N, it adds + * (N+1) * prefixLen to account for all the stripped prefixes up to that point. + * + * Formula: rawPos = startIndex + strippedPos + (lineNum + 1) * prefixLen + * where lineNum = count of \n characters in content before posInContent + * + * @param content - The stripped content (after prefix removal) + * @param linePrefix - The prefix that was stripped (e.g. "> "), empty string if none + * @param startIndex - Starting position of the content in the raw document + * @param posInContent - Position within the stripped content + * @returns Position in the raw document + */ +export function toDocPos( + content: string, + linePrefix: string, + startIndex: number, + posInContent: number +): number { + if (!linePrefix) return startIndex + posInContent + + const prefixLen = linePrefix.length + const lineCount = (content.slice(0, posInContent).match(/\n/g) || []).length + return startIndex + posInContent + (lineCount + 1) * prefixLen +} diff --git a/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.test.ts b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.test.ts index 3c0452a..e6f4d0a 100644 --- a/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.test.ts +++ b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.test.ts @@ -2,57 +2,15 @@ * Tests for the callout syntax-highlighting fix (main-ow0). * * SQLSealViewPlugin is not imported here because it pulls in Obsidian/CodeMirror - * globals that don't exist in Node. Instead, we extract and test the three pieces - * of pure logic that were changed: + * globals that don't exist in Node. Instead, we test the pure logic functions + * that were extracted for testability: * - * 1. The regex that finds sqlseal blocks (including inside callouts) - * 2. The prefix-stripping that cleans "> " from each content line - * 3. The toDocPos() formula that maps stripped positions back to raw doc positions + * 1. extractCodeBlocks() - The regex that finds sqlseal blocks (including inside callouts) + * and the prefix-stripping that cleans "> " from each content line + * 2. toDocPos() - The formula that maps stripped positions back to raw doc positions */ -// -------------------------------------------------------------------------- -// 1. Replica of getCodeBlocks() logic — same regex and stripping as source -// -------------------------------------------------------------------------- - -interface CodeBlockMatch { - startIndex: number - content: string - linePrefix?: string -} - -function extractCodeBlocks(text: string): CodeBlockMatch[] { - const codeBlockRegex = /^([ \t]*(?:> )*)```(sqlseal)\n([\s\S]*?)^[ \t]*(?:> )*```/gm - let match - const results: CodeBlockMatch[] = [] - while ((match = codeBlockRegex.exec(text)) !== null) { - const fencePrefix = match[1] || '' - const langTag = match[2] - const rawContent = match[3] - const blockStart = match.index - const langTagEnd = blockStart + fencePrefix.length + 3 + langTag.length - const contentStart = langTagEnd + 1 - - if (!fencePrefix) { - results.push({ content: rawContent, startIndex: contentStart }) - } else { - const strippedLines = rawContent.split('\n').map(line => - line.startsWith(fencePrefix) ? line.slice(fencePrefix.length) : line - ) - results.push({ content: strippedLines.join('\n'), startIndex: contentStart, linePrefix: fencePrefix }) - } - } - return results -} - -// -------------------------------------------------------------------------- -// 2. Replica of toDocPos() — same formula as privateDecorateCodeblock() -// -------------------------------------------------------------------------- - -function toDocPos(content: string, linePrefix: string, startIndex: number, posInContent: number): number { - const prefixLen = linePrefix.length - const lineCount = (content.slice(0, posInContent).match(/\n/g) || []).length - return startIndex + posInContent + (lineCount + 1) * prefixLen -} +import { extractCodeBlocks, toDocPos } from './codeBlockExtraction'; // -------------------------------------------------------------------------- // Tests diff --git a/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts index 5548f39..e9dc542 100644 --- a/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts +++ b/src/modules/syntaxHighlight/editorExtension/syntaxHighlight.ts @@ -16,13 +16,7 @@ import { Decorator, highlighterOperation } from '../grammar/highlighterOperation import { FilePathWidget } from './widgets/FilePathWidget'; import { RendererRegistry } from '../../editor/renderer/rendererRegistry'; import { SQLSealLangDefinition } from '../../editor/parser'; - -interface CodeBlockMatch { - startIndex: number, - content: string, - /** Characters stripped from the start of each content line (e.g. "> " in callouts) */ - linePrefix?: string -} +import { extractCodeBlocks, toDocPos, CodeBlockMatch } from './codeBlockExtraction'; const markDecorations = { blockFlag: Decoration.mark({ class: 'cm-sqlseal-block-flag' }), @@ -85,34 +79,7 @@ export class SQLSealViewPlugin implements PluginValue { }] } - // Parsing — match code blocks with optional callout prefix (e.g. "> ") on fence lines. - // The prefix is captured so positions can be mapped back to the raw document. - const codeBlockRegex = /^([ \t]*(?:> )*)```(sqlseal)\n([\s\S]*?)^[ \t]*(?:> )*```/gm; - let match; - let results: CodeBlockMatch[] = [] - while ((match = codeBlockRegex.exec(text)) !== null) { - const fencePrefix = match[1] || ''; - const langTag = match[2]; - const rawContent = match[3]; - const blockStart = match.index; - const langTagEnd = blockStart + fencePrefix.length + 3 + langTag.length; // prefix + ``` + langTag - const contentStart = langTagEnd + 1; // +1 for the newline after the opening fence - - if (!fencePrefix) { - results.push({ content: rawContent, startIndex: contentStart }) - } else { - // Strip the callout prefix from each content line so the grammar can parse it cleanly. - const strippedLines = rawContent.split('\n').map(line => - line.startsWith(fencePrefix) ? line.slice(fencePrefix.length) : line - ) - results.push({ - content: strippedLines.join('\n'), - startIndex: contentStart, - linePrefix: fencePrefix - }) - } - } - return results + return extractCodeBlocks(text) } decorateFilename(dec: Decorator, { content, startIndex }: CodeBlockMatch) { @@ -141,18 +108,6 @@ export class SQLSealViewPlugin implements PluginValue { const { content, startIndex, linePrefix } = codeblockMatch const decorations = this.parseWithGrammar(content); - /** - * Map a position in the (possibly stripped) content back to the raw document. - * For callout blocks, each line has a prefix (e.g. "> ") that was removed before - * parsing. We restore that offset here: for a position on line N, add (N+1) * prefixLen. - */ - const toDocPos = (posInContent: number): number => { - if (!linePrefix) return startIndex + posInContent - const prefixLen = linePrefix.length - const lineCount = (content.slice(0, posInContent).match(/\n/g) || []).length - return startIndex + posInContent + (lineCount + 1) * prefixLen - } - return (decorations || []).flatMap(dec => { switch (dec.type) { case 'filename': @@ -161,8 +116,8 @@ export class SQLSealViewPlugin implements PluginValue { const decoration = markDecorations[dec.type as keyof typeof markDecorations]; if (decoration) { return decoration.range( - toDocPos(dec.start), - toDocPos(dec.end) + toDocPos(content, linePrefix || '', startIndex, dec.start), + toDocPos(content, linePrefix || '', startIndex, dec.end) ) } else { return [] @@ -172,11 +127,6 @@ export class SQLSealViewPlugin implements PluginValue { } private buildDecorations(view: EditorView): DecorationSet { - const builder: Array> = []; - // const text = view.state.doc.toString(); - // const codeBlockRegex = /```(sqlseal)\n([\s\S]*?)```/g; - // let match; - const results = this.getCodeBlocks(view) const decorators = results.flatMap(r => this.privateDecorateCodeblock(r))