chore: refactoring code to reuse the same function in tests and app

This commit is contained in:
Kacper Kula 2026-03-29 10:43:03 +01:00
parent 575eb6657f
commit 19de558d86
4 changed files with 93 additions and 102 deletions

View file

@ -0,0 +1,5 @@
---
"sqlseal": patch
---
fixed syntax highlighting in callouts

View file

@ -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
}

View file

@ -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

View file

@ -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<Range<Decoration>> = [];
// 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))