mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
fix contrat,wagon allocation wehn a booking is split
This commit is contained in:
@@ -3,6 +3,10 @@ import Handlebars from 'handlebars';
|
||||
/** One numbered clause of a dynamic article, with optional nested bullets. */
|
||||
export interface RenderedClause {
|
||||
text: string;
|
||||
/** Computed outline number, e.g. "3" or "2.1.4". */
|
||||
number: string;
|
||||
/** Nesting level: 1 = clause, 2 = sub-clause (x.y), 3 = x.y.z, … */
|
||||
depth: number;
|
||||
bullets: string[];
|
||||
}
|
||||
|
||||
@@ -16,10 +20,26 @@ export interface RenderedArticle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a template article body into clauses. Format: one clause per line;
|
||||
* lines prefixed with "- " become bullets nested under the preceding clause.
|
||||
* A body that reduces to a single clause without bullets renders as a plain
|
||||
* paragraph rather than a numbered list of one.
|
||||
* Leading outline token on a clause line: "1.", "2)", "1.1", "1.1.1." …
|
||||
* The token's segment count sets the clause depth; its digits are ignored —
|
||||
* numbering is recomputed sequentially so stale numbers self-heal.
|
||||
* A single-segment token requires its "."/")" ("10 tons…" is prose, "10. x"
|
||||
* is clause ten); multi-segment tokens ("1.1") may omit it. A token may also
|
||||
* end the line — that is an empty clause still being typed in the editor.
|
||||
*/
|
||||
const CLAUSE_NUMBER_RE = /^(?:(\d+(?:\.\d+)+)[.)]?|(\d+)[.)])(?:\s+|$)/;
|
||||
|
||||
/** Deepest supported sub-clause level (1.1.1.1.1.1). */
|
||||
const MAX_CLAUSE_DEPTH = 6;
|
||||
|
||||
/**
|
||||
* Parse a template article body into clauses. Format: one clause per line.
|
||||
* A leading outline number ("2. ", "2.1 ", "2.1.3 ") nests the line as a
|
||||
* sub-clause at that depth — the typed digits are stripped and renumbered
|
||||
* sequentially, so editing order never leaves stale numbers in the document.
|
||||
* Lines prefixed with "- " become bullets nested under the preceding clause.
|
||||
* A body that reduces to a single un-numbered clause without bullets renders
|
||||
* as a plain paragraph rather than a numbered list of one.
|
||||
*/
|
||||
export function parseArticleBody(body: string): Pick<RenderedArticle, 'paragraph' | 'clauses'> {
|
||||
const lines = (body ?? '')
|
||||
@@ -28,20 +48,44 @@ export function parseArticleBody(body: string): Pick<RenderedArticle, 'paragraph
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
const clauses: RenderedClause[] = [];
|
||||
// counters[i] = current number at depth i+1; truncated when a shallower
|
||||
// clause arrives so deeper numbering restarts at 1.
|
||||
const counters: number[] = [];
|
||||
let sawNumberToken = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('- ')) {
|
||||
const bullet = line.slice(2).trim();
|
||||
if (clauses.length === 0) {
|
||||
clauses.push({ text: bullet, bullets: [] });
|
||||
counters.splice(0, counters.length, 1);
|
||||
clauses.push({ text: bullet, number: '1', depth: 1, bullets: [] });
|
||||
} else {
|
||||
clauses[clauses.length - 1].bullets.push(bullet);
|
||||
}
|
||||
} else {
|
||||
clauses.push({ text: line, bullets: [] });
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = CLAUSE_NUMBER_RE.exec(line);
|
||||
const token = match ? (match[1] ?? match[2]) : null;
|
||||
let depth = token ? Math.min(token.split('.').length, MAX_CLAUSE_DEPTH) : 1;
|
||||
// A sub-clause can only sit directly under an existing parent — "1.1.1"
|
||||
// typed as the first line clamps to whatever level is actually open.
|
||||
depth = Math.min(depth, counters.length + 1);
|
||||
if (match) sawNumberToken = true;
|
||||
|
||||
counters.splice(depth);
|
||||
while (counters.length < depth) counters.push(0);
|
||||
counters[depth - 1] += 1;
|
||||
|
||||
clauses.push({
|
||||
text: match ? line.slice(match[0].length).trim() : line,
|
||||
number: counters.slice(0, depth).join('.'),
|
||||
depth,
|
||||
bullets: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (clauses.length === 1 && clauses[0].bullets.length === 0) {
|
||||
if (clauses.length === 1 && clauses[0].bullets.length === 0 && !sawNumberToken) {
|
||||
return { paragraph: clauses[0].text, clauses: [] };
|
||||
}
|
||||
return { clauses };
|
||||
|
||||
@@ -26,6 +26,40 @@ describe('parseArticleBody', () => {
|
||||
expect(parsed.paragraph).toBe('This Agreement becomes effective when signed.');
|
||||
expect(parsed.clauses).toEqual([]);
|
||||
});
|
||||
|
||||
it('nests numbered sub-clauses by their outline token and renumbers sequentially', () => {
|
||||
const parsed = parseArticleBody(
|
||||
'1. Scope\n5.1 Rail transport\n1.1.1 Wagon supply\n2. Payment',
|
||||
);
|
||||
expect(parsed.clauses.map((c) => [c.number, c.depth, c.text])).toEqual([
|
||||
['1', 1, 'Scope'],
|
||||
['1.1', 2, 'Rail transport'],
|
||||
['1.1.1', 3, 'Wagon supply'],
|
||||
['2', 1, 'Payment'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('clamps a sub-clause with no open parent to the next available level', () => {
|
||||
const parsed = parseArticleBody('1.1.1 Orphan sub-clause\nSecond clause.');
|
||||
expect(parsed.clauses.map((c) => [c.number, c.depth])).toEqual([
|
||||
['1', 1],
|
||||
['2', 1],
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves prose that merely starts with a number un-tokenized', () => {
|
||||
const parsed = parseArticleBody('10 tons is the minimum load.\nPayment in advance.');
|
||||
expect(parsed.clauses.map((c) => c.text)).toEqual([
|
||||
'10 tons is the minimum load.',
|
||||
'Payment in advance.',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps a single explicitly numbered line as a clause, not a paragraph', () => {
|
||||
const parsed = parseArticleBody('1. Only clause.');
|
||||
expect(parsed.paragraph).toBeUndefined();
|
||||
expect(parsed.clauses.map((c) => [c.number, c.text])).toEqual([['1', 'Only clause.']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolateTemplateText', () => {
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
{{else}}
|
||||
<ol class="clauses">
|
||||
{{#each clauses}}
|
||||
<li>
|
||||
<li class="clause depth-{{depth}}">
|
||||
<span class="clause-no">{{number}}.</span>
|
||||
{{text}}
|
||||
{{#if bullets.length}}
|
||||
<ul class="clause-bullets">
|
||||
|
||||
@@ -282,28 +282,27 @@
|
||||
.article-name { color: #0e5b45; }
|
||||
.article-paragraph { margin: 4px 0 0; }
|
||||
ol.clauses {
|
||||
counter-reset: clause;
|
||||
list-style: none;
|
||||
margin: 6px 0 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
ol.clauses > li {
|
||||
counter-increment: clause;
|
||||
ol.clauses > li.clause {
|
||||
margin-bottom: 6px;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
text-align: justify;
|
||||
}
|
||||
ol.clauses > li::before {
|
||||
ol.clauses .clause-no {
|
||||
color: #0e5b45;
|
||||
content: counter(clause) ".";
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 9.5pt;
|
||||
font-weight: 700;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
/* Sub-clause indentation: each outline level steps in. */
|
||||
ol.clauses > li.depth-2 { padding-left: 20px; }
|
||||
ol.clauses > li.depth-3 { padding-left: 40px; }
|
||||
ol.clauses > li.depth-4 { padding-left: 60px; }
|
||||
ol.clauses > li.depth-5 { padding-left: 80px; }
|
||||
ol.clauses > li.depth-6 { padding-left: 100px; }
|
||||
ul.clause-bullets {
|
||||
margin: 5px 0 2px;
|
||||
padding-left: 16px;
|
||||
|
||||
Reference in New Issue
Block a user