diff --git a/README.md b/README.md index 5fd0bc0..07104d3 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ - 🎯 AST-based symbol searching for TypeScript/JavaScript - 📝 Interactive fix mode with colored diffs - 🎨 Beautiful diff display -- 🚫 .docsignore support for excluding files +- 🚫 Ignore file support for excluding files ## Installation @@ -171,7 +171,7 @@ Create `.coderefrc.json` in your project root: { "projectRoot": ".", "docsDir": "docs", - "ignoreFile": ".docsignore" + "ignoreFile": ".gitignore" } ``` diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index b2bcab9..9ddda62 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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 + + + +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 diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index b8e29fb..825e4bf 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -8,7 +8,7 @@ Create `.coderefrc.json` in your project root: { "projectRoot": ".", "docsDir": "docs", - "ignoreFile": ".docsignore", + "ignoreFile": ".gitignore", "ignorePatterns": ["**/*.draft.md"], "verbose": false } @@ -55,15 +55,18 @@ The tool loads configuration from multiple sources with the following precedence #### `ignoreFile` - **Type**: `string` (optional) -- **Default**: `".docsignore"` +- **Default**: `undefined` - **Description**: Path to ignore file relative to `projectRoot`. The file follows `.gitignore` syntax. ```json { - "ignoreFile": ".docsignore" + "ignoreFile": ".gitignore" } ``` +> **Note**: Prior to version 0.2.0, the default value was `.docsignore`. +> If you want to continue using `.docsignore`, explicitly set `ignoreFile: '.docsignore'` in your configuration. + #### `ignorePatterns` - **Type**: `string[]` (optional) @@ -161,7 +164,7 @@ Configuration with custom ignore patterns and verbose output: { "projectRoot": ".", "docsDir": "documentation", - "ignoreFile": ".docsignore", + "ignoreFile": ".gitignore", "ignorePatterns": ["**/*.draft.md", "**/archive/**", "**/_*.md"], "verbose": true } @@ -175,7 +178,7 @@ Configuration for a monorepo with multiple documentation directories: { "projectRoot": "packages/my-package", "docsDir": "docs", - "ignoreFile": "../../.docsignore" + "ignoreFile": "../../.gitignore" } ``` @@ -217,9 +220,9 @@ Alternatively, you can define configuration in `package.json`: ## Ignore Files -### .docsignore Syntax +### Ignore File Syntax -The `.docsignore` file follows the same syntax as `.gitignore`: +The ignore file follows the same syntax as `.gitignore`: ``` # Ignore draft files diff --git a/src/cli/validate.ts b/src/cli/validate.ts index 755a5f0..235e7ad 100644 --- a/src/cli/validate.ts +++ b/src/cli/validate.ts @@ -103,14 +103,14 @@ export async function main(args: string[] = process.argv.slice(2)): Promise { const relativePath = path.relative(config.projectRoot, file); return !isIgnored(relativePath, ignorePatterns); @@ -118,7 +118,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise markdownFiles.length) { console.log( - `📋 ${allMarkdownFiles.length - markdownFiles.length} files excluded by .docsignore\n` + `📋 ${allMarkdownFiles.length - markdownFiles.length} files excluded by ignore patterns\n` ); } diff --git a/src/config.test.ts b/src/config.test.ts index 47d4c7e..ba0ded8 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -39,7 +39,6 @@ describe('Config System', () => { expect(config).toEqual({ projectRoot: testDir, docsDir: 'docs', - ignoreFile: '.docsignore', verbose: false, }); }); @@ -219,7 +218,7 @@ describe('Config System', () => { expect(config.docsDir).toBe('custom-docs'); expect(config.projectRoot).toBe(testDir); - expect(config.ignoreFile).toBe('.docsignore'); + expect(config.ignoreFile).toBeUndefined(); }); }); @@ -320,8 +319,9 @@ describe('Config System', () => { describe('getIgnoreFilePath', () => { it('should return absolute path to ignore file when configured', () => { - const ignorePath = getIgnoreFilePath(config); - expect(ignorePath).toBe(path.join(testDir, '.docsignore')); + const customConfig = loadConfig({ ignoreFile: '.gitignore' }); + const ignorePath = getIgnoreFilePath(customConfig); + expect(ignorePath).toBe(path.join(testDir, '.gitignore')); }); it('should return null when ignoreFile is not configured', () => { @@ -343,13 +343,12 @@ describe('Config System', () => { const docsDir = path.join(testDir, 'docs'); fs.mkdirSync(docsDir); fs.writeFileSync(path.join(docsDir, 'README.md'), '# Documentation'); - fs.writeFileSync(path.join(testDir, '.docsignore'), '*.tmp.md'); const config = loadConfig(); expect(config.projectRoot).toBe(testDir); expect(getDocsPath(config)).toBe(docsDir); - expect(getIgnoreFilePath(config)).toBe(path.join(testDir, '.docsignore')); + expect(getIgnoreFilePath(config)).toBeNull(); }); it('should handle monorepo structure with custom docsDir', () => { diff --git a/src/config.ts b/src/config.ts index 01acfeb..148ea49 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,7 +26,7 @@ export interface CodeRefConfig { docsDir: string; /** - * Path to ignore file relative to projectRoot (default: ".docsignore") + * Path to ignore file relative to projectRoot */ ignoreFile?: string; @@ -80,7 +80,6 @@ function getDefaultConfig(): CodeRefConfig { return { projectRoot: process.cwd(), docsDir: 'docs', - ignoreFile: '.docsignore', verbose: false, }; } diff --git a/src/core/validate.test.ts b/src/core/validate.test.ts index 7674149..a50a2b2 100644 --- a/src/core/validate.test.ts +++ b/src/core/validate.test.ts @@ -14,7 +14,6 @@ const mockProjectRoot = '/project'; const mockConfig: CodeRefConfig = { projectRoot: mockProjectRoot, docsDir: 'docs', - ignoreFile: '.docsignore', verbose: false, }; @@ -163,6 +162,132 @@ describe('validate-docs-code', () => { expect(result).toHaveLength(1); expect(result[0].refPath).toBe('src/example.ts'); }); + + it('コードブロック内のCODE_REFを除外すること', () => { + const content = ` +# Test Document + + + +\`\`\`html + +\`\`\` + + + `; + + const result = extractCodeRefs(content, '/docs/test.md'); + + expect(result).toHaveLength(2); + expect(result[0].refPath).toBe('src/valid.ts'); + expect(result[1].refPath).toBe('src/another-valid.ts'); + }); + + it('インラインコード内のCODE_REFを除外すること', () => { + const content = ` +# Test Document + +Use this syntax: \`\` + + + `; + + const result = extractCodeRefs(content, '/docs/test.md'); + + expect(result).toHaveLength(1); + expect(result[0].refPath).toBe('src/valid.ts'); + }); + + it('閉じていないコードブロック内のCODE_REFを除外すること', () => { + const content = ` +# Test Document + + + +\`\`\`html + + + `; + + const result = extractCodeRefs(content, '/docs/test.md'); + + expect(result).toHaveLength(1); + expect(result[0].refPath).toBe('src/valid.ts'); + }); + + it('複数のコードブロックが混在する場合にCODE_REFを正しく抽出すること', () => { + const content = ` +# Documentation + + + +\`\`\`typescript + +const x = 1; +\`\`\` + + + +\`\`\`javascript + +\`\`\` + + + `; + + const result = extractCodeRefs(content, '/docs/test.md'); + + expect(result).toHaveLength(3); + expect(result[0].refPath).toBe('src/valid1.ts'); + 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 + +\`\`\` + +\`\`\`typescript +export class ClassName {} +\`\`\` +\`\`\`\` + + + `; + + 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 + +\`\`\` +\`\`\`\` +\`\`\`\`\` + + + `; + + const result = extractCodeRefs(content, '/docs/test.md'); + + expect(result).toHaveLength(1); + expect(result[0].refPath).toBe('src/valid.ts'); + }); }); describe('validateCodeRef', () => { diff --git a/src/core/validate.ts b/src/core/validate.ts index 80f1644..5adcd60 100644 --- a/src/core/validate.ts +++ b/src/core/validate.ts @@ -53,19 +53,71 @@ export function findMarkdownFiles(dir: string): string[] { function getCodeBlockRanges(content: string): { start: number; end: number }[] { const ranges: { start: number; end: number }[] = []; - // Triple backtick code blocks - const codeBlockPattern = /```[\s\S]*?```/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 = codeBlockPattern.exec(content)) !== null) { - ranges.push({ - start: match.index, - end: match.index + match[0].length, - }); + // Only consider sequences of 3 or more backticks as code block delimiters + if (count >= 3) { + backtickSequences.push({ position: start, length: count }); + } + } else { + i++; + } } - // Inline code (backticks) + // Pair up backtick sequences with matching lengths + const used = new Set(); + + 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: backtickSequences[i].position, + end: content.length, + }); + } + } + + // Inline code (single backticks) const inlineCodePattern = /`[^`\n]+?`/g; + let match: RegExpExecArray | null; + while ((match = inlineCodePattern.exec(content)) !== null) { ranges.push({ start: match.index, diff --git a/src/utils/fix.test.ts b/src/utils/fix.test.ts index b6d4583..722d509 100644 --- a/src/utils/fix.test.ts +++ b/src/utils/fix.test.ts @@ -50,7 +50,6 @@ const mockPrompt = prompt as jest.Mocked; const mockConfig: CodeRefConfig = { projectRoot: path.resolve(__dirname, '../../..'), docsDir: 'docs', - ignoreFile: '.docsignore', verbose: false, };