Skip to content
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -171,7 +171,7 @@ Create `.coderefrc.json` in your project root:
{
"projectRoot": ".",
"docsDir": "docs",
"ignoreFile": ".docsignore"
"ignoreFile": ".gitignore"
}
```

Expand Down
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
17 changes: 10 additions & 7 deletions docs/user-guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Create `.coderefrc.json` in your project root:
{
"projectRoot": ".",
"docsDir": "docs",
"ignoreFile": ".docsignore",
"ignoreFile": ".gitignore",
"ignorePatterns": ["**/*.draft.md"],
"verbose": false
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -175,7 +178,7 @@ Configuration for a monorepo with multiple documentation directories:
{
"projectRoot": "packages/my-package",
"docsDir": "docs",
"ignoreFile": "../../.docsignore"
"ignoreFile": "../../.gitignore"
}
```

Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/cli/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,22 +103,22 @@
console.log(`📋 Specified files/directories: ${options.files.join(', ')}\n`);
}

// Load .docsignore patterns
// Load ignore patterns
const ignoreFilePath = getIgnoreFilePath(config);
const ignorePatterns = ignoreFilePath ? loadDocsignorePatterns(ignoreFilePath) : [];
if (options.verbose) {
console.log(`📋 Loaded ${ignorePatterns.length} patterns from .docsignore\n`);
console.log(`📋 Loaded ${ignorePatterns.length} ignore patterns\n`);
}

// Filter files not excluded by .docsignore
// Filter files not excluded by ignore patterns
const markdownFiles = allMarkdownFiles.filter((file) => {
const relativePath = path.relative(config.projectRoot, file);
return !isIgnored(relativePath, ignorePatterns);
});

if (options.verbose && allMarkdownFiles.length > markdownFiles.length) {
console.log(
`📋 ${allMarkdownFiles.length - markdownFiles.length} files excluded by .docsignore\n`
`📋 ${allMarkdownFiles.length - markdownFiles.length} files excluded by ignore patterns\n`
);
}

Expand All @@ -126,7 +126,7 @@

// Extract all CODE_REF references
let totalRefs = 0;
const allRefs: { ref: any; file: string }[] = [];

Check warning on line 129 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Unexpected any. Specify a different type

Check warning on line 129 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Unexpected any. Specify a different type

for (const file of markdownFiles) {
const content = fs.readFileSync(file, 'utf-8');
Expand All @@ -150,7 +150,7 @@
}

// Validate each reference
const allErrors = await Promise.all(allRefs.map(({ ref }) => validateCodeRef(ref, config))).then(

Check warning on line 153 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator

Check warning on line 153 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator
(results) => results.flat()
);

Expand All @@ -162,7 +162,7 @@
console.log(`❌ Found ${allErrors.length} errors:\n`);

// Group errors by document
const errorsByDoc: Record<string, any[]> = {};

Check warning on line 165 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Unexpected any. Specify a different type

Check warning on line 165 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Unexpected any. Specify a different type

for (const error of allErrors) {
const docFile = path.relative(config.projectRoot, error.ref.docFile);
Expand Down Expand Up @@ -196,7 +196,7 @@
);
const diff = displayLineRangeDiff(
actualCode,
{ start: error.ref.startLine!, end: error.ref.endLine! },

Check warning on line 199 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Forbidden non-null assertion

Check warning on line 199 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Forbidden non-null assertion

Check warning on line 199 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Forbidden non-null assertion

Check warning on line 199 in src/cli/validate.ts

View workflow job for this annotation

GitHub Actions / code-quality (22.x)

Forbidden non-null assertion
error.suggestedLines
);
console.log(diff);
Expand Down
11 changes: 5 additions & 6 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ describe('Config System', () => {
expect(config).toEqual({
projectRoot: testDir,
docsDir: 'docs',
ignoreFile: '.docsignore',
verbose: false,
});
});
Expand Down Expand Up @@ -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();
});
});

Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down
3 changes: 1 addition & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
docsDir: string;

/**
* Path to ignore file relative to projectRoot (default: ".docsignore")
* Path to ignore file relative to projectRoot
*/
ignoreFile?: string;

Expand Down Expand Up @@ -80,7 +80,6 @@
return {
projectRoot: process.cwd(),
docsDir: 'docs',
ignoreFile: '.docsignore',
verbose: false,
};
}
Expand Down Expand Up @@ -118,7 +117,7 @@
try {
const content = fs.readFileSync(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(content);
return packageJson.coderef || null;

Check warning on line 120 in src/config.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

Check warning on line 120 in src/config.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
} catch (_error) {
// Silently ignore package.json parsing errors
return null;
Expand Down Expand Up @@ -214,7 +213,7 @@
const defaultConfig = getDefaultConfig();

// Determine projectRoot first (needed for loading config files)
const projectRoot = options.projectRoot || process.env.CODEREF_PROJECT_ROOT || process.cwd();

Check warning on line 216 in src/config.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

Check warning on line 216 in src/config.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

Check warning on line 216 in src/config.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

Check warning on line 216 in src/config.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

// Load from all sources
const packageJsonConfig = loadPackageJsonConfig(projectRoot);
Expand Down
127 changes: 126 additions & 1 deletion src/core/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const mockProjectRoot = '/project';
const mockConfig: CodeRefConfig = {
projectRoot: mockProjectRoot,
docsDir: 'docs',
ignoreFile: '.docsignore',
verbose: false,
};

Expand Down Expand Up @@ -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

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

\`\`\`html
<!-- CODE_REF: src/should-be-ignored.ts:1-5 -->
\`\`\`

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

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: \`<!-- CODE_REF: src/inline-ignored.ts:1-5 -->\`

<!-- 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('閉じていないコードブロック内のCODE_REFを除外すること', () => {
const content = `
# Test Document

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

\`\`\`html
<!-- CODE_REF: src/in-unclosed-block.ts:1-5 -->
<!-- CODE_REF: src/also-in-unclosed-block.ts:1-5 -->
`;

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

<!-- CODE_REF: src/valid1.ts:1-10 -->

\`\`\`typescript
<!-- CODE_REF: src/ignored1.ts:1-5 -->
const x = 1;
\`\`\`

<!-- CODE_REF: src/valid2.ts:10-20 -->

\`\`\`javascript
<!-- CODE_REF: src/ignored2.ts:5-10 -->
\`\`\`

<!-- CODE_REF: src/valid3.ts:20-30 -->
`;

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
<!-- 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
Loading