mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
chore: refactoring code to reuse the same function in tests and app
This commit is contained in:
parent
575eb6657f
commit
19de558d86
4 changed files with 93 additions and 102 deletions
5
.changeset/gentle-ducks-peel.md
Normal file
5
.changeset/gentle-ducks-peel.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"sqlseal": patch
|
||||
---
|
||||
|
||||
fixed syntax highlighting in callouts
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue