rehlma / docs-reviewable-proser1
Keep prose diffs at one sentence per line
PublicWrite one prose sentence per source line and apply the convention only to text being edited.
detector.tsisolate · reads files, writes none · no network · 100 ms · 32 MB
// The production implementation lives in lib; the SDK tests execute this entrypoint.
import './lib/detector.ts';
fixtures/dart/pubspec.yaml
namefixture
dependencies
collectionany
fixtures/flutter/docs/README.md
Fixture documentation
fixtures/flutter/pubspec.yaml
namefixture
dependencies
flutter
sdkflutter
fixtures/inline/pubspec.yaml
namefixture
dependencies
flutter sdkflutter
lib/context.ts
import { findUp } from '@wellactually/sdk/v3';
import { readFileSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
// Inspect the nearest package, not another Flutter package in the monorepo.
export function packageRoot(from: string): string | null {
const manifest = findUp('pubspec.yaml', from);
if (!manifest) return null;
try {
const yaml = readFileSync(manifest, 'utf8');
const dependencies = /^dependencies:[ \t]*(?:#.*)?\r?\n((?:(?:[ \t]+[^\n]*|[ \t]*|#.*)\r?\n?)*)/m.exec(yaml)?.[1] ?? '';
// Supports ordinary block dependencies and flutter: { sdk: flutter }.
if (!/^ flutter:[ \t]*(?:\{[ \t]*sdk:[ \t]*flutter[ \t]*\}|(?:#.*)?\r?\n[ \t]+sdk:[ \t]*flutter\b)/m.test(dependencies)) return null;
return dirname(manifest);
} catch { return null; }
}
export function isFlutter(path: string): boolean {
return packageRoot(dirname(path)) !== null;
}
export function hasDocs(path: string): boolean {
const root = packageRoot(dirname(path));
return root !== null && existsSync(join(root, 'docs'));
}
// Mask comments and strings while preserving offsets and line numbers.
// This is lexical filtering, not a Dart parser; interpolation is masked too.
export function codeOnly(text: string): string {
let result = '', i = 0;
const blank = (value: string) => value.replace(/[^\n\r]/g, ' ');
while (i < text.length) {
const start = i;
if (text.startsWith('//', i)) {
while (i < text.length && text[i] !== '\n') i++;
} else if (text.startsWith('/*', i)) {
let depth = 1;
i += 2;
while (i < text.length && depth) {
if (text.startsWith('/*', i)) { depth++; i += 2; }
else if (text.startsWith('*/', i)) { depth--; i += 2; }
else i++;
}
} else if (text[i] === "'" || text[i] === '"') {
const raw = start > 0 && text[start - 1] === 'r';
const quote = text[i];
const delimiter = text.startsWith(quote.repeat(3), i) ? quote.repeat(3) : quote;
i += delimiter.length;
while (i < text.length && !text.startsWith(delimiter, i)) {
i += !raw && text[i] === '\\' ? 2 : 1;
}
i = Math.min(text.length, i + delimiter.length);
} else {
result += text[i++];
continue;
}
result += blank(text.slice(start, i));
}
return result;
}
lib/detector.ts
import { onWrite } from '@wellactually/sdk/v3';
import { codeOnly, isFlutter, hasDocs } from './context.ts';
onWrite({
globs: ["**/docs/**/*.md"],
*detect(event) {
const lines = event.text.split('\n');
let fence: string | null = null;
const eligible = new Set<number>();
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
const mark = /^\s{0,3}(`{3,}|~{3,})/.exec(line)?.[1];
if (mark) {
if (!fence) fence = mark;
else if (mark[0] === fence[0] && mark.length >= fence.length) fence = null;
continue;
}
if (fence || /^\s*(?:#|\||<!--)/.test(line) || /^(?: {4}|\t)/.test(line)) continue;
const prose = line.replace(/`+[^`]*`+/g, '').replace(/\]\([^)]*\)/g, ']').replace(/\b(?:e\.g|i\.e|Mr|Mrs|Dr|Prof|vs|etc)\./gi, '');
if (/[.!?][ \t]+[A-Z][a-z]/.test(prose)) eligible.add(index + 1);
}
for (const written of event.written) {
if (eligible.has(written.line)) yield { line: written.line };
}
},
});
package.json
{
"name": "docs-reviewable-prose",
"private": true,
"type": "module",
"tags": [
"documentation"
]
}