import type { appdxItemTags, articleGroupTags, paragraphItemTags, supplProvisionAppdxItemTags } from "../../law/std"; import { sentenceChildrenToString } from "../../parser/cst/rules/$sentenceChildren"; import { sentencesArrayToString } from "../../parser/cst/rules/$sentencesArray"; import { rangeOfELs } from "../el"; import type { AttrEntries, SentencesArray, Controls, SentenceChildEL } from "./inline"; /** * Line: the unit of a CST of the parsed lawtext. * * A `Line` represents a physical line in the lawtext separated by newline characters. The `Line` object has a property named `type` with the type of `LineType`, and the different types of `Line` are distinguished by that property. */ export type Line = | BlankLine | TOCHeadLine | ArticleGroupHeadLine | AppdxItemHeadLine | SupplProvisionHeadLine | SupplProvisionAppdxItemHeadLine | ArticleLine | ParagraphItemLine | TableColumnLine | OtherLine ; /** * LineType: the identifiers that distinguish the different types of `Line`. */ export enum LineType { BNK = "BNK", TOC = "TOC", ARG = "ARG", APP = "APP", SPR = "SPR", SPA = "SPA", ART = "ART", PIT = "PIT", TBL = "TBL", OTH = "OTH", } interface BaseLineOptions { type: TType, range: [start: number, end: number] | null, lineEndText: string, } abstract class BaseLine { public type: TType; public range: [start: number, end: number] | null; public lineEndText: string; public constructor( options: BaseLineOptions, ) { this.type = options.type; this.range = options.range; this.lineEndText = options.lineEndText; } public text(): string { return this.rangeTexts().map(([, t]) => t).join(""); } public abstract rangeTexts(): [range: [start: number, end: number] | null, text:string, description: string][]; public lineEndTextRange(): [start: number, end: number] | null { if (!this.range) return null; return [this.range[1] - this.lineEndText.length, this.range[1]]; } } type IndentsLineOptions = BaseLineOptions & { indentTexts: string[], } abstract class IndentsLine extends BaseLine { public indentTexts: string[]; public constructor( options: IndentsLineOptions, ) { super(options); this.indentTexts = options.indentTexts; } public indentRangeTexts() { const ret: ReturnType = []; let lastEnd = this.range ? this.range[0] : NaN; for (let i = 0; i < this.indentTexts.length; i++) { ret.push([ (!isNaN(lastEnd)) ? [lastEnd, lastEnd + this.indentTexts[i].length] : null, this.indentTexts[i], "Indent", ]); lastEnd += this.indentTexts[i].length; } return ret; } public get indentsEndPos() { const indentRangeTexts = this.indentRangeTexts(); const pos = ( indentRangeTexts.length > 0 ? indentRangeTexts[indentRangeTexts.length - 1][0]?.[1] : null ) ?? (this.range && this.range[0]) ?? null; return pos; } } type WithControlsLineOptions = IndentsLineOptions & { controls: Controls, } abstract class WithControlsLine extends IndentsLine { public controls: Controls; public constructor( options: WithControlsLineOptions, ) { super(options); this.controls = options.controls; } public controlsRangeTexts() { const ret: ReturnType = []; for (const control of this.controls) { ret.push([control.controlRange, control.control, "Control"]); ret.push([control.trailingSpaceRange, control.trailingSpace, "ControlTrailingSpace"]); } return ret; } public get controlsEndPos() { const pos = ( this.controls.length > 0 ? this.controls[this.controls.length - 1].trailingSpaceRange?.[1] : null ) ?? this.indentsEndPos; return pos; } } type BlankLineOptions = Omit, "type">; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $blankLine } from "../../parser/cst/rules/$blankLine"; /* eslint-disable tsdoc/syntax */ /** * A blank line with no printable characters. Please see the source code of {@link $blankLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class BlankLine extends BaseLine { public constructor( options: BlankLineOptions, ) { super({ ...options, type: LineType.BNK }); } public rangeTexts() { return [[this.lineEndTextRange(), this.lineEndText, "LineEnd"]] as ReturnType; } } type TOCHeadLineOptions = Omit, "type"> & { title: string; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $tocHeadLine } from "../../parser/cst/rules/$tocHeadLine"; /* eslint-disable tsdoc/syntax */ /** * A head line of a TOC (Table Of Contents). Please see the source code of {@link $tocHeadLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class TOCHeadLine extends WithControlsLine { public title: string; public constructor( options: TOCHeadLineOptions, ) { super({ ...options, type: LineType.TOC }); this.title = options.title; } public get titleRange(): [number, number] | null { if (!this.range) return null; const lastEnd = ( (this.controlsEndPos) ?? (this.range[0] + this.indentTexts.map(t => t.length).reduce((a, b) => a + b, 0)) ); return [ lastEnd, lastEnd + this.title.length, ]; } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push(...this.controlsRangeTexts()); ret.push([this.titleRange, this.title, "Title"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } } type ArticleGroupHeadLineOptions = Omit, "type"> & { mainTag: (typeof articleGroupTags)[number], sentenceChildren: SentenceChildEL[], }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $articleGroupHeadLine } from "../../parser/cst/rules/$articleGroupHeadLine"; /* eslint-disable tsdoc/syntax */ /** * A head line of an article group (e.g. "Chapter"). Please see the source code of {@link $articleGroupHeadLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class ArticleGroupHeadLine extends WithControlsLine { public mainTag: (typeof articleGroupTags)[number]; public title: SentenceChildEL[]; public constructor( options: ArticleGroupHeadLineOptions, ) { super({ ...options, type: LineType.ARG }); this.mainTag = options.mainTag; this.title = options.sentenceChildren; } public get titleRange(): [number, number] | null { if (this.title.length > 0) { return rangeOfELs(this.title); } else { const pos = this.controlsEndPos; return (pos !== null) ? [pos, pos] : null; } } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push(...this.controlsRangeTexts()); ret.push([this.titleRange, sentenceChildrenToString(this.title), "Title"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } } type AppdxItemHeadLineOptions = Omit, "type"> & { mainTag: (typeof appdxItemTags)[number], title: SentenceChildEL[], relatedArticleNum: SentenceChildEL[], }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $appdxItemHeadLine } from "../../parser/cst/rules/$appdxItemHeadLine"; /* eslint-disable tsdoc/syntax */ /** * A head line of an appended item such as an appended table. Please see the source code of {@link $appdxItemHeadLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class AppdxItemHeadLine extends WithControlsLine { public mainTag: (typeof appdxItemTags)[number]; public title: SentenceChildEL[]; public relatedArticleNum: SentenceChildEL[]; public constructor( options: AppdxItemHeadLineOptions, ) { super({ ...options, type: LineType.APP }); this.mainTag = options.mainTag; this.title = options.title; this.relatedArticleNum = options.relatedArticleNum; } public get titleRange(): [number, number] | null { if (this.title.length > 0) { return rangeOfELs(this.title); } else { const pos = this.controlsEndPos; return (pos !== null) ? [pos, pos] : null; } } public get relatedArticleNumRange(): [number, number] | null { if (this.relatedArticleNum.length > 0) { return rangeOfELs(this.relatedArticleNum); } else { const base = this.titleRange; if (!base) return null; return [base[1], base[1]]; } } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push(...this.controlsRangeTexts()); ret.push([this.titleRange, sentenceChildrenToString(this.title), "Title"]); ret.push([this.relatedArticleNumRange, sentenceChildrenToString(this.relatedArticleNum), "RelatedArticleNum"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } } type SupplProvisionHeadLineOptions = Omit, "type"> & { title: string; titleRange: [number, number] | null; openParen: string; amendLawNum: string; closeParen: string; extractText: string; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $supplProvisionHeadLine } from "../../parser/cst/rules/$supplProvisionHeadLine"; /* eslint-disable tsdoc/syntax */ /** * A head line of a supplementary provision. Please see the source code of {@link $supplProvisionHeadLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class SupplProvisionHeadLine extends WithControlsLine { public title: string; public titleRange: [number, number] | null; public openParen: string; public amendLawNum: string; public closeParen: string; public extractText: string; public constructor( options: SupplProvisionHeadLineOptions, ) { super({ ...options, type: LineType.SPR }); this.title = options.title; this.titleRange = options.titleRange; this.openParen = options.openParen; this.amendLawNum = options.amendLawNum; this.closeParen = options.closeParen; this.extractText = options.extractText; } public get openParenRange(): [number, number] | null { const base = this.titleRange; if (!base) return null; return [base[1], base[1] + this.openParen.length]; } public get amendLawNumRange(): [number, number] | null { const base = this.openParenRange; if (!base) return null; return [base[1], base[1] + this.amendLawNum.length]; } public get closeParenRange(): [number, number] | null { const base = this.amendLawNumRange; if (!base) return null; return [base[1], base[1] + this.closeParen.length]; } public get extractTextRange(): [number, number] | null { const base = this.closeParenRange; if (!base) return null; return [base[1], base[1] + this.extractText.length]; } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push(...this.controlsRangeTexts()); ret.push([this.titleRange, this.title, "Title"]); ret.push([this.openParenRange, this.openParen, "OpenParen"]); ret.push([this.amendLawNumRange, this.amendLawNum, "AmendLawNum"]); ret.push([this.closeParenRange, this.closeParen, "CloseParen"]); ret.push([this.extractTextRange, this.extractText, "ExtractText"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } } type SupplProvisionAppdxItemHeadLineOptions = Omit, "type"> & { mainTag: (typeof supplProvisionAppdxItemTags)[number], title: SentenceChildEL[], relatedArticleNum: SentenceChildEL[], }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $supplProvisionAppdxItemHeadLine } from "../../parser/cst/rules/$supplProvisionAppdxItemHeadLine"; /* eslint-disable tsdoc/syntax */ /** * A head line of an appended item in a supplementary provision. Please see the source code of {@link $supplProvisionAppdxItemHeadLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class SupplProvisionAppdxItemHeadLine extends WithControlsLine { public mainTag: (typeof supplProvisionAppdxItemTags)[number]; public title: SentenceChildEL[]; public relatedArticleNum: SentenceChildEL[]; public constructor( options: SupplProvisionAppdxItemHeadLineOptions, ) { super({ ...options, type: LineType.SPA }); this.mainTag = options.mainTag; this.title = options.title; this.relatedArticleNum = options.relatedArticleNum; } public get titleRange(): [number, number] | null { if (this.title.length > 0) { return rangeOfELs(this.title); } else { const pos = this.controlsEndPos; return (pos !== null) ? [pos, pos] : null; } } public get relatedArticleNumRange(): [number, number] | null { if (this.relatedArticleNum.length > 0) { return rangeOfELs(this.relatedArticleNum); } else { const base = this.titleRange; if (!base) return null; return [base[1], base[1]]; } } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push(...this.controlsRangeTexts()); ret.push([this.titleRange, sentenceChildrenToString(this.title), "Title"]); ret.push([this.relatedArticleNumRange, sentenceChildrenToString(this.relatedArticleNum), "RelatedArticleNum"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } } type ArticleLineOptions = Omit, "type"> & { title: string, midSpace: string, sentencesArray: SentencesArray, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $articleLine } from "../../parser/cst/rules/$articleLine"; /* eslint-disable tsdoc/syntax */ /** * A first line of an article. Please see the source code of {@link $articleLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class ArticleLine extends IndentsLine { public title: string; public midSpace: string; public sentencesArray: SentencesArray; public constructor( options: ArticleLineOptions, ) { super({ ...options, type: LineType.ART }); this.title = options.title; this.midSpace = options.midSpace; this.sentencesArray = options.sentencesArray; } public get titleRange(): [number, number] | null { const pos = this.indentsEndPos; return (pos !== null) ? [pos, pos + this.title.length] : null; } public get midSpaceRange(): [number, number] | null { const base = this.titleRange; if (!base) return null; return [base[1], base[1] + this.midSpace.length]; } public get sentencesArrayRange(): [number, number] | null { if (this.sentencesArray.length > 0) { const ranges = this.sentencesArray.flat().map(ss => [ ss.leadingSpaceRange, ...ss.attrEntries.map(e => [e.entryRange, e.trailingSpaceRange]).flat(), ...ss.sentences.map(s => s.range), ]).flat().filter(r => r !== null) as [start: number, end: number][]; if (ranges.length === 0) return null; return [ranges[0][0], ranges[ranges.length - 1][1]]; } else { const base = this.midSpaceRange; if (!base) return null; return [base[1], base[1]]; } } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push([this.titleRange, this.title, "Title"]); ret.push([this.midSpaceRange, this.midSpace, "MidSpace"]); ret.push([this.sentencesArrayRange, sentencesArrayToString(this.sentencesArray), "SentencesArray"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } } type ParagraphItemLineOptions = Omit, "type"> & { mainTag: TTag, controls: Controls, title: string, midSpace: string, sentencesArray: SentencesArray, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $paragraphItemLine } from "../../parser/cst/rules/$paragraphItemLine"; /* eslint-disable tsdoc/syntax */ /** * A first line of a paragraph, item, and subitem. Please see the source code of {@link $paragraphItemLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class ParagraphItemLine extends WithControlsLine { public mainTag: TTag; public override controls: Controls; public title: string; public midSpace: string; public sentencesArray: SentencesArray; public constructor( options: ParagraphItemLineOptions, ) { super({ ...options, type: LineType.PIT }); this.mainTag = options.mainTag; this.controls = options.controls; this.title = options.title; this.midSpace = options.midSpace; this.sentencesArray = options.sentencesArray; } public get titleRange(): [number, number] | null { const pos = this.controlsEndPos; return (pos !== null) ? [pos, pos + this.title.length] : null; } public get midSpaceRange(): [number, number] | null { const base = this.titleRange; if (!base) return null; return [base[1], base[1] + this.midSpace.length]; } public get sentencesArrayRange(): [number, number] | null { if (this.sentencesArray.length > 0) { const ranges = this.sentencesArray.flat().map(ss => [ ss.leadingSpaceRange, ...ss.attrEntries.map(e => [e.entryRange, e.trailingSpaceRange]).flat(), ...ss.sentences.map(s => s.range), ]).flat().filter(r => r !== null) as [start: number, end: number][]; if (ranges.length === 0) return null; return [ranges[0][0], ranges[ranges.length - 1][1]]; } else { const base = this.midSpaceRange; if (!base) return null; return [base[1], base[1]]; } } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push(...this.controlsRangeTexts()); ret.push([this.titleRange, this.title, "Title"]); ret.push([this.midSpaceRange, this.midSpace, "MidSpace"]); ret.push([this.sentencesArrayRange, sentencesArrayToString(this.sentencesArray), "SentencesArray"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } public withTag(tag: TNewTag): ParagraphItemLine { return new ParagraphItemLine( { ...this, mainTag: tag }, ); } } type TableColumnLineOptions = Omit, "type"> & { firstColumnIndicator: "*" | "", midIndicatorsSpace: string, columnIndicator: "-" | "*", midSpace: string, attrEntries: AttrEntries, multilineIndicator: "|" | "", sentencesArray: SentencesArray, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $tableColumnLine } from "../../parser/cst/rules/$tableColumnLine"; /* eslint-disable tsdoc/syntax */ /** * A line of table column. Please see the source code of {@link $tableColumnLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class TableColumnLine extends IndentsLine { public firstColumnIndicator: "*" | ""; public midIndicatorsSpace: string; public columnIndicator: "-" | "*"; public midSpace: string; public attrEntries: AttrEntries; public multilineIndicator: "|" | ""; public sentencesArray: SentencesArray; public constructor( options: TableColumnLineOptions, ) { super({ ...options, type: LineType.TBL }); this.firstColumnIndicator = options.firstColumnIndicator; this.midIndicatorsSpace = options.midIndicatorsSpace; this.columnIndicator = options.columnIndicator; this.midSpace = options.midSpace; this.attrEntries = options.attrEntries; this.multilineIndicator = options.multilineIndicator; this.sentencesArray = options.sentencesArray; } public get firstColumnIndicatorRange(): [number, number] | null { const pos = this.indentsEndPos; return (pos !== null) ? [pos, pos + this.firstColumnIndicator.length] : null; } public get midIndicatorsSpaceRange(): [number, number] | null { const base = this.firstColumnIndicatorRange; if (!base) return null; return [base[1], base[1] + this.midIndicatorsSpace.length]; } public get columnIndicatorRange(): [number, number] | null { const base = this.midIndicatorsSpaceRange; if (!base) return null; return [base[1], base[1] + this.columnIndicator.length]; } public get midSpaceRange(): [number, number] | null { const base = this.columnIndicatorRange; if (!base) return null; return [base[1], base[1] + this.midSpace.length]; } public attrEntriesRangeTexts() { const ret: ReturnType = []; for (const attrEntry of this.attrEntries) { ret.push([ attrEntry.entryRange, attrEntry.entryText, "AttrEntry", ]); ret.push([ attrEntry.trailingSpaceRange, attrEntry.trailingSpace, "AttrEntryTrailingSpace", ]); } return ret; } public get multilineIndicatorRange(): [number, number] | null { const attrEntriesRanges = this.attrEntriesRangeTexts().map(r => r[0]); const base = ( attrEntriesRanges.length > 0 ? attrEntriesRanges[attrEntriesRanges.length - 1] : this.midSpaceRange ) ?? null; if (!base) return null; return [base[1], base[1] + this.multilineIndicator.length]; } public get sentencesArrayRange(): [number, number] | null { if (this.sentencesArray.length > 0) { const ranges = this.sentencesArray.flat().map(ss => [ ss.leadingSpaceRange, ...ss.attrEntries.map(e => [e.entryRange, e.trailingSpaceRange]).flat(), ...ss.sentences.map(s => s.range), ]).flat().filter(r => r !== null) as [start: number, end: number][]; if (ranges.length === 0) return null; return [ranges[0][0], ranges[ranges.length - 1][1]]; } else { const base = this.multilineIndicatorRange; if (!base) return null; return [base[1], base[1]]; } } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push([this.firstColumnIndicatorRange, this.firstColumnIndicator, "FirstColumnIndicator"]); ret.push([this.midIndicatorsSpaceRange, this.midIndicatorsSpace, "MidIndicatorsSpace"]); ret.push([this.columnIndicatorRange, this.columnIndicator, "ColumnIndicator"]); ret.push([this.midSpaceRange, this.midSpace, "MidSpace"]); ret.push(...this.attrEntriesRangeTexts()); ret.push([this.multilineIndicatorRange, this.multilineIndicator, "MultilineIndicator"]); ret.push([this.sentencesArrayRange, sentencesArrayToString(this.sentencesArray, { escapeLeadingSpaces: true }), "SentencesArray"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } } type OtherLineOptions = Omit, "type"> & { sentencesArray: SentencesArray, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { type $otherLine } from "../../parser/cst/rules/$otherLine"; /* eslint-disable tsdoc/syntax */ /** * A line of other types. Please see the source code of {@link $otherLine} for the detailed syntax. */ /* eslint-enable tsdoc/syntax */ export class OtherLine extends WithControlsLine { public sentencesArray: SentencesArray; public constructor( options: OtherLineOptions, ) { super({ ...options, type: LineType.OTH }); this.sentencesArray = options.sentencesArray; } public get sentencesArrayRange(): [number, number] | null { if (this.sentencesArray.length > 0) { const ranges = this.sentencesArray.flat().map(ss => [ ss.leadingSpaceRange, ...ss.attrEntries.map(e => [e.entryRange, e.trailingSpaceRange]).flat(), ...ss.sentences.map(s => s.range), ]).flat().filter(r => r !== null) as [start: number, end: number][]; if (ranges.length === 0) return null; return [ranges[0][0], ranges[ranges.length - 1][1]]; } else { const pos = this.controlsEndPos; return (pos !== null) ? [pos, pos] : null; } } public rangeTexts() { const ret: ReturnType = []; ret.push(...this.indentRangeTexts()); ret.push(...this.controlsRangeTexts()); ret.push([this.sentencesArrayRange, sentencesArrayToString(this.sentencesArray), "SentencesArray"]); ret.push([this.lineEndTextRange(), this.lineEndText, "LineEnd"]); return ret; } }