diff --git a/spec/index.html b/spec/index.html index 6125735e..97199c9d 100644 --- a/spec/index.html +++ b/spec/index.html @@ -55,6 +55,7 @@

Options

`--assets-dir``assetsDir`Directory in which to place assets when using `--assets=external`. Defaults to "assets". `--lint-spec``lintSpec`Enforce some style and correctness checks. `--error-formatter`The eslint formatter to be used for printing warnings and errors when using `--verbose`. Either the name of a built-in eslint formatter or the package name of an installed eslint compatible formatter. + `--max-clause-depth N`Warn when clauses exceed a nesting depth of N, and cause those clauses to be numbered by incrementing their parent clause's number rather than by nesting a new number within their parent clause. `--strict`Exit with an error if there are warnings. Cannot be used with `--watch`. `--multipage`Emit a distinct page for each top-level clause. diff --git a/src/args.ts b/src/args.ts index f7aa0277..9c4453cc 100644 --- a/src/args.ts +++ b/src/args.ts @@ -67,6 +67,12 @@ export const options = [ description: 'The formatter for warnings and errors; either a path prefixed with "." or "./", or package name, of an installed eslint compatible formatter (default: eslint-formatter-codeframe)', }, + { + name: 'max-clause-depth', + type: Number, + description: + 'The maximum nesting depth for clauses; exceeding this will cause a warning. Defaults to no limit.', + }, { name: 'multipage', type: Boolean, diff --git a/src/clauseNums.ts b/src/clauseNums.ts index f77f1631..143a6c06 100644 --- a/src/clauseNums.ts +++ b/src/clauseNums.ts @@ -9,6 +9,8 @@ export default function iterator(spec: Spec): ClauseNumberIterator { const ids: (string | number[])[] = []; let inAnnex = false; let currentLevel = 0; + let hasWarnedForExcessNesting = false; + const MAX_LEVELS = spec.opts.maxClauseDepth ?? Infinity; return { next(clauseStack: Clause[], node: HTMLElement) { @@ -22,28 +24,47 @@ export default function iterator(spec: Spec): ClauseNumberIterator { message: 'clauses cannot follow annexes', }); } - if (level - currentLevel > 1) { + if (level - currentLevel > 1 && (level < MAX_LEVELS || currentLevel < MAX_LEVELS - 1)) { spec.warn({ type: 'node', node, - ruleId: 'skipped-caluse', + ruleId: 'skipped-clause', message: 'clause is being numbered without numbering its parent clause', }); } + if (!hasWarnedForExcessNesting && level + 1 > (spec.opts.maxClauseDepth ?? Infinity)) { + spec.warn({ + type: 'node', + node, + ruleId: 'max-clause-depth', + message: `clause exceeds maximum nesting depth of ${spec.opts.maxClauseDepth}`, + }); + hasWarnedForExcessNesting = true; + } const nextNum = annex ? nextAnnexNum : nextClauseNum; - if (level === currentLevel) { - ids[currentLevel] = nextNum(clauseStack, node); - } else if (level > currentLevel) { - ids.push(nextNum(clauseStack, node)); + if (level >= MAX_LEVELS) { + if (ids.length === MAX_LEVELS) { + const lastLevelIndex = MAX_LEVELS - 1; + const lastLevel = ids[lastLevelIndex] as number[]; + lastLevel[lastLevel.length - 1]++; + } else { + while (ids.length < MAX_LEVELS) { + ids.push([1]); + } + } } else { - ids.length = level + 1; - ids[level] = nextNum(clauseStack, node); + if (level === currentLevel) { + ids[currentLevel] = nextNum(clauseStack, node); + } else if (level > currentLevel) { + ids.push(nextNum(clauseStack, node)); + } else { + ids.length = level + 1; + ids[level] = nextNum(clauseStack, node); + } } - - currentLevel = level; - + currentLevel = Math.min(level, MAX_LEVELS - 1); return ids.flat().join('.'); }, }; diff --git a/src/cli.ts b/src/cli.ts index cf96ee93..fd038dbc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -113,6 +113,9 @@ const build = debounce(async function build() { if (args['mark-effects']) { opts.markEffects = true; } + if (args['max-clause-depth']) { + opts.maxClauseDepth = args['max-clause-depth']; + } if (args['no-toc'] != null) { opts.toc = !args['no-toc']; } diff --git a/src/ecmarkup.ts b/src/ecmarkup.ts index 34aae168..815b37e8 100644 --- a/src/ecmarkup.ts +++ b/src/ecmarkup.ts @@ -32,6 +32,7 @@ export interface Options { copyright?: boolean; date?: Date; location?: string; + maxClauseDepth?: number; multipage?: boolean; extraBiblios?: ExportedBiblio[]; contributors?: string; diff --git a/test/baselines/generated-reference/max-clause-depth.html b/test/baselines/generated-reference/max-clause-depth.html new file mode 100644 index 00000000..a9cc5bca --- /dev/null +++ b/test/baselines/generated-reference/max-clause-depth.html @@ -0,0 +1,46 @@ + + + + + +
+
+ + +

1 One

+ +

1.1 Two

+ + +

1.2 Three

+ +

1.3 four

+
+
+ +

1.4 Three Again

+ +

1.5 Four Again

+
+
+
+ +

1.6 Two Again

+
+
+ +

2 One Again

+
+
\ No newline at end of file diff --git a/test/baselines/sources/max-clause-depth.html b/test/baselines/sources/max-clause-depth.html new file mode 100644 index 00000000..3643aeca --- /dev/null +++ b/test/baselines/sources/max-clause-depth.html @@ -0,0 +1,35 @@ + + + + +
+  copyright: false
+  assets: none
+  maxClauseDepth: 2
+
+ + +

One

+ +

Two

+ + +

Three

+ +

four

+
+
+ +

Three Again

+ +

Four Again

+
+
+
+ +

Two Again

+
+
+ +

One Again

+
diff --git a/test/clauseIds.js b/test/clauseIds.js index a2f495af..d34a097a 100644 --- a/test/clauseIds.js +++ b/test/clauseIds.js @@ -7,7 +7,7 @@ describe('clause id generation', () => { let iter; beforeEach(() => { - iter = sectionNums(); + iter = sectionNums({ opts: {} }); }); specify('generating clause ids', () => { diff --git a/test/errors.js b/test/errors.js index a99bfb5c..a4fc5275 100644 --- a/test/errors.js +++ b/test/errors.js @@ -1237,4 +1237,49 @@ ${M} `); }); }); + + describe('max clause depth', () => { + it('max depth', async () => { + await assertError( + positioned` + +

One

+ ${M} +

Two

+
+ +

Not warned

+
+
+ `, + { + ruleId: 'max-clause-depth', + nodeType: 'emu-clause', + message: 'clause exceeds maximum nesting depth of 1', + }, + { + maxClauseDepth: 1, + }, + ); + }); + + it('negative', async () => { + await assertErrorFree( + ` + +

One

+ +

Two

+
+ +

Not warned

+
+
+ `, + { + maxClauseDepth: 2, + }, + ); + }); + }); });