Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,39 @@ The project is organized into three main directories under `src/`:
- `cli/`: Command-line interface implementations (validate.ts, fix.ts)
- `core/`: Core validation and fixing logic
- `utils/`: Shared utility functions

## Core Validation Logic

### CODE_REF Extraction (src/core/validate.ts)

The validation system extracts CODE_REF comments from markdown files while intelligently excluding references that appear inside code blocks or inline code. This ensures that documentation examples showing CODE_REF syntax are not mistakenly validated.

#### Code Block Detection Algorithm

The code block detection uses a sophisticated pairing algorithm to handle various markdown code block formats:

1. **Backtick Sequence Detection**: Scans the entire document to find all sequences of 3 or more consecutive backticks
2. **Length-Based Pairing**: Matches opening and closing backtick sequences with identical lengths
- ` ``` ` pairs with ` ``` ` (3 backticks)
- ` ```` ` pairs with ` ```` ` (4 backticks)
- ` ````` ` pairs with ` ````` ` (5 backticks)
3. **Unclosed Block Handling**: Treats any unpaired backtick sequence as an unclosed code block extending to the end of the file
4. **Inline Code Detection**: Separately detects single-backtick inline code using pattern matching

This algorithm correctly handles:

- Nested code blocks with different backtick lengths
- Markdown examples that show code block syntax
- Unclosed code blocks (common in draft documentation)
- Mixed inline code and code blocks

#### Validation Process

<!-- CODE_REF: src/core/validate.ts#extractCodeRefs -->

The `extractCodeRefs` function:

1. Pre-computes all code block and inline code ranges in the document
2. Finds all CODE_REF comment patterns
3. Filters out CODE_REF comments that fall within code block ranges
4. Returns only CODE_REF comments that should be validated
46 changes: 46 additions & 0 deletions src/core/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,52 @@ const x = 1;
expect(result[1].refPath).toBe('src/valid2.ts');
expect(result[2].refPath).toBe('src/valid3.ts');
});

it('4つのバッククォートで囲まれたコードブロック内のCODE_REFを除外すること', () => {
const content = `
# Implementation Details

\`\`\`\`markdown
## Class

\`\`\`html
<!-- CODE_REF: src/nested-ignored.ts:10-50 -->
\`\`\`

\`\`\`typescript
export class ClassName {}
\`\`\`
\`\`\`\`

<!-- CODE_REF: src/valid.ts:1-10 -->
`;

const result = extractCodeRefs(content, '/docs/test.md');

expect(result).toHaveLength(1);
expect(result[0].refPath).toBe('src/valid.ts');
});

it('5つのバッククォートで囲まれたコードブロック内のCODE_REFを除外すること', () => {
const content = `
# Nested Example

\`\`\`\`\`markdown
\`\`\`\`markdown
\`\`\`html
<!-- CODE_REF: src/deeply-nested-ignored.ts:1-10 -->
\`\`\`
\`\`\`\`
\`\`\`\`\`

<!-- CODE_REF: src/valid.ts:1-10 -->
`;

const result = extractCodeRefs(content, '/docs/test.md');

expect(result).toHaveLength(1);
expect(result[0].refPath).toBe('src/valid.ts');
});
});

describe('validateCodeRef', () => {
Expand Down
74 changes: 54 additions & 20 deletions src/core/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,37 +53,71 @@
function getCodeBlockRanges(content: string): { start: number; end: number }[] {
const ranges: { start: number; end: number }[] = [];

// Find all triple backtick positions
const backtickPositions: number[] = [];
const backtickPattern = /```/g;
let match: RegExpExecArray | null;
// Find all backtick sequences (3 or more consecutive backticks)
const backtickSequences: { position: number; length: number }[] = [];
let i = 0;

while (i < content.length) {
if (content[i] === '`') {
const start = i;
let count = 0;

// Count consecutive backticks
while (i < content.length && content[i] === '`') {
count++;
i++;
}

while ((match = backtickPattern.exec(content)) !== null) {
backtickPositions.push(match.index);
// Only consider sequences of 3 or more backticks as code block delimiters
if (count >= 3) {
backtickSequences.push({ position: start, length: count });
}
} else {
i++;
}
}

// Pair up backticks: even indices (0, 2, 4...) are opening, odd indices (1, 3, 5...) are closing
for (let i = 0; i < backtickPositions.length; i += 2) {
const start = backtickPositions[i];
const end = backtickPositions[i + 1];
// Pair up backtick sequences with matching lengths
const used = new Set<number>();

if (end !== undefined) {
// Closed code block
ranges.push({
start,
end: end + 3, // +3 to include the closing ```
});
} else {
// Unclosed code block (odd number of backticks)
for (let i = 0; i < backtickSequences.length; i++) {
if (used.has(i)) continue;

const opening = backtickSequences[i];

// Find the next sequence with the same length
for (let j = i + 1; j < backtickSequences.length; j++) {
if (used.has(j)) continue;

const closing = backtickSequences[j];

if (opening.length === closing.length) {
// Found a matching pair
ranges.push({
start: opening.position,
end: closing.position + closing.length,
});
used.add(i);
used.add(j);
break;
}
}
}

// Handle unclosed code blocks (sequences without a matching pair)
for (let i = 0; i < backtickSequences.length; i++) {
if (!used.has(i)) {
ranges.push({
start,
start: backtickSequences[i].position,
end: content.length,
});
}
}

// Inline code (backticks)
// Inline code (single backticks)
const inlineCodePattern = /`[^`\n]+?`/g;
let match: RegExpExecArray | null;

while ((match = inlineCodePattern.exec(content)) !== null) {
ranges.push({
start: match.index,
Expand Down Expand Up @@ -154,7 +188,7 @@
* Validate code content
*/
export function validateCodeContent(ref: CodeRef, config?: CodeRefConfig): CodeRefError[] {
const cfg = config || loadConfig();

Check warning on line 191 in src/core/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator
const projectRoot = cfg.projectRoot;
const errors: CodeRefError[] = [];

Expand All @@ -177,7 +211,7 @@
const fileContent = fs.readFileSync(absolutePath, 'utf-8');
const matches = findSymbolInAST(fileContent, absolutePath, {
className: ref.className,
memberName: ref.memberName!,

Check warning on line 214 in src/core/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Forbidden non-null assertion
});

if (matches.length > 0) {
Expand Down
Loading