@weave-md/validate
Advanced tools
| import { Diagnostic } from '@weave-md/core'; | ||
| export declare function validateInlineSyntax(markdown: string, filePath?: string): Diagnostic[]; | ||
| export declare function validateInlineMath(markdown: string, filePath?: string): Diagnostic[]; | ||
| export declare function validateInlineSubstitute(markdown: string, filePath?: string): Diagnostic[]; |
+166
-2
@@ -1,5 +0,6 @@ | ||
| import { isEscaped, findClosingBracket } from '../links/index.js'; | ||
| import { isEscaped } from '../links/index.js'; | ||
| export function validateInlineSyntax(markdown, filePath) { | ||
| const diagnostics = []; | ||
| diagnostics.push(...validateInlineMath(markdown, filePath)); | ||
| diagnostics.push(...validateInlineSubstitute(markdown, filePath)); | ||
| return diagnostics; | ||
@@ -49,3 +50,3 @@ } | ||
| const { line: startLine, character: startColumn } = getLineCol(startPos); | ||
| const closeBracketPos = findClosingBracket(markdown, openBracket + 1); | ||
| const closeBracketPos = findClosingBracketNoNewline(markdown, openBracket + 1); | ||
| if (closeBracketPos === -1) { | ||
@@ -77,1 +78,164 @@ diagnostics.push({ | ||
| } | ||
| export function validateInlineSubstitute(markdown, filePath) { | ||
| const diagnostics = []; | ||
| // Build line start positions for O(1) line/column lookup | ||
| const lineStarts = [0]; | ||
| for (let i = 0; i < markdown.length; i++) { | ||
| if (markdown[i] === '\r') { | ||
| if (i + 1 < markdown.length && markdown[i + 1] === '\n') { | ||
| lineStarts.push(i + 2); | ||
| i++; | ||
| } | ||
| else { | ||
| lineStarts.push(i + 1); | ||
| } | ||
| } | ||
| else if (markdown[i] === '\n') { | ||
| lineStarts.push(i + 1); | ||
| } | ||
| } | ||
| const getLineCol = (pos) => { | ||
| let lo = 0; | ||
| let hi = lineStarts.length - 1; | ||
| while (lo < hi) { | ||
| const mid = (lo + hi + 1) >> 1; | ||
| if (lineStarts[mid] <= pos) { | ||
| lo = mid; | ||
| } | ||
| else { | ||
| hi = mid - 1; | ||
| } | ||
| } | ||
| return { line: lo + 1, character: pos - lineStarts[lo] + 1 }; | ||
| }; | ||
| const subRegex = /:sub\[/g; | ||
| let match; | ||
| while ((match = subRegex.exec(markdown)) !== null) { | ||
| const startPos = match.index; | ||
| // Skip if escaped | ||
| if (isEscaped(markdown, startPos)) { | ||
| continue; | ||
| } | ||
| const openBracket = startPos + 4; // position of '[' | ||
| const { line: startLine, character: startColumn } = getLineCol(startPos); | ||
| const closeBracketPos = findClosingBracketNoNewline(markdown, openBracket + 1); | ||
| if (closeBracketPos === -1) { | ||
| diagnostics.push({ | ||
| severity: 'error', | ||
| message: 'Inline substitution syntax :sub[...]{...} has unclosed bracket', | ||
| filePath, | ||
| position: { line: startLine, character: startColumn }, | ||
| code: 'unclosed-inline-sub' | ||
| }); | ||
| continue; | ||
| } | ||
| // Check for opening brace after closing bracket | ||
| if (closeBracketPos + 1 >= markdown.length || markdown[closeBracketPos + 1] !== '{') { | ||
| diagnostics.push({ | ||
| severity: 'error', | ||
| message: 'Inline substitution syntax :sub[...]{...} missing replacement braces', | ||
| filePath, | ||
| position: { line: startLine, character: startColumn }, | ||
| code: 'missing-sub-replacement' | ||
| }); | ||
| continue; | ||
| } | ||
| const openBracePos = closeBracketPos + 1; | ||
| const closeBracePos = findClosingBrace(markdown, openBracePos + 1); | ||
| if (closeBracePos === -1) { | ||
| diagnostics.push({ | ||
| severity: 'error', | ||
| message: 'Inline substitution syntax :sub[...]{...} has unclosed brace', | ||
| filePath, | ||
| position: { line: startLine, character: startColumn }, | ||
| code: 'unclosed-inline-sub-brace' | ||
| }); | ||
| continue; | ||
| } | ||
| // Check for empty initial content | ||
| const initialContent = markdown.substring(openBracket + 1, closeBracketPos); | ||
| const unescapedInitial = initialContent.replace(/\\\[/g, '').replace(/\\\]/g, ''); | ||
| if (unescapedInitial.trim() === '') { | ||
| diagnostics.push({ | ||
| severity: 'warning', | ||
| message: 'Inline substitution syntax :sub[...]{...} has empty initial content', | ||
| filePath, | ||
| position: { line: startLine, character: startColumn }, | ||
| code: 'empty-inline-sub-initial' | ||
| }); | ||
| } | ||
| // Check for empty replacement content | ||
| const replacementContent = markdown.substring(openBracePos + 1, closeBracePos); | ||
| const unescapedReplacement = replacementContent.replace(/\\\{/g, '').replace(/\\\}/g, ''); | ||
| if (unescapedReplacement.trim() === '') { | ||
| diagnostics.push({ | ||
| severity: 'warning', | ||
| message: 'Inline substitution syntax :sub[...]{...} has empty replacement content', | ||
| filePath, | ||
| position: { line: startLine, character: startColumn }, | ||
| code: 'empty-inline-sub-replacement' | ||
| }); | ||
| } | ||
| } | ||
| return diagnostics; | ||
| } | ||
| function findClosingBracketNoNewline(str, startIndex) { | ||
| let depth = 1; | ||
| let i = startIndex; | ||
| while (i < str.length && depth > 0) { | ||
| const ch = str[i]; | ||
| // Cannot span lines | ||
| if (ch === '\n' || ch === '\r') { | ||
| return -1; | ||
| } | ||
| // Handle escapes: only \] \[ \\ are valid escapes in initial content | ||
| if (ch === '\\' && i + 1 < str.length) { | ||
| const next = str[i + 1]; | ||
| if (next === ']' || next === '[' || next === '\\') { | ||
| i += 2; | ||
| continue; | ||
| } | ||
| } | ||
| if (ch === '[') { | ||
| depth++; | ||
| } | ||
| else if (ch === ']') { | ||
| depth--; | ||
| if (depth === 0) { | ||
| return i; | ||
| } | ||
| } | ||
| i++; | ||
| } | ||
| return -1; | ||
| } | ||
| function findClosingBrace(str, startIndex) { | ||
| let depth = 1; | ||
| let i = startIndex; | ||
| while (i < str.length && depth > 0) { | ||
| const ch = str[i]; | ||
| // Cannot span lines | ||
| if (ch === '\n' || ch === '\r') { | ||
| return -1; | ||
| } | ||
| // Handle escapes: only \} \{ \\ are valid escapes in replacement content | ||
| if (ch === '\\' && i + 1 < str.length) { | ||
| const next = str[i + 1]; | ||
| if (next === '}' || next === '{' || next === '\\') { | ||
| i += 2; | ||
| continue; | ||
| } | ||
| } | ||
| if (ch === '{') { | ||
| depth++; | ||
| } | ||
| else if (ch === '}') { | ||
| depth--; | ||
| if (depth === 0) { | ||
| return i; | ||
| } | ||
| } | ||
| i++; | ||
| } | ||
| return -1; | ||
| } |
+8
-8
| { | ||
| "name": "@weave-md/validate", | ||
| "version": "0.2.0-alpha.0", | ||
| "version": "0.3.0-alpha.0", | ||
| "description": "Reference validation for Weave Markdown", | ||
@@ -20,2 +20,5 @@ "type": "module", | ||
| ], | ||
| "scripts": { | ||
| "build": "tsc" | ||
| }, | ||
| "keywords": [ | ||
@@ -35,3 +38,3 @@ "weave", | ||
| "peerDependencies": { | ||
| "@weave-md/core": "0.2.0-alpha.0" | ||
| "@weave-md/core": "workspace:*" | ||
| }, | ||
@@ -43,8 +46,5 @@ "dependencies": { | ||
| "@types/node": "^20.10.0", | ||
| "typescript": "^5.7.0", | ||
| "@weave-md/core": "0.2.0-alpha.0" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc" | ||
| "@weave-md/core": "workspace:*", | ||
| "typescript": "^5.7.0" | ||
| } | ||
| } | ||
| } |
-21
| MIT License | ||
| Copyright (c) 2025 weavepage | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
41316
13.18%988
20.05%14
-6.67%