@gondola-lyrics/kit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"formats.js","names":[],"sources":["../src/utils/annotation.ts","../src/utils/index.ts","../src/formats/lrc/common.ts","../src/formats/lrc/utils.ts","../src/formats/lrc/detect.ts","../src/formats/lrc/generator.ts","../src/formats/lrc/parser.ts","../src/formats/index.ts"],"sourcesContent":["import type { runtime } from '@gondola-lyrics/model'\n\nimport { LineType, getWordsText, makeLineAnnotation, makeLineAnnotationRoman, makeLineAnnotationTranslation } from '@gondola-lyrics/model/runtime'\n\nexport interface AnnotationSources {\n translations?: runtime.Line[]\n romans?: runtime.Line[]\n}\n\nexport interface AnnotationMatchOptions {\n /**\n * How far an annotation's start may sit from a line's start, in milliseconds.\n * Defaults to `0`, which requires an exact match.\n */\n tolerance?: number\n}\n\n/**\n * An annotation line carries its text in either field, depending on whether the track used word tags.\n */\nconst readText = (line: runtime.Line) => {\n return (line.content?.text ?? getWordsText(line.content?.words ?? [])).trim()\n}\n\nconst isReadable = (line: runtime.Line) => {\n return line.time?.start !== undefined && readText(line) !== ''\n}\n\n/**\n * Assigns each annotation line to a lyric line index.\n *\n * Exact matches claim their lines first, so a fuzzy match never takes a line an exact one needed.\n * The remaining annotations take the nearest unclaimed line within the tolerance, and ties keep the earlier line.\n */\nconst matchEntries = (lines: runtime.Line[], annotations: runtime.Line[], tolerance: number) => {\n const matched = new Map<number, runtime.Line>()\n if (annotations.length === 0) {\n return matched\n }\n\n const starts = lines.map((line) => line.time?.start)\n const queues = new Map<number, number[]>()\n\n for (const [index, start] of starts.entries()) {\n if (start === undefined) {\n continue\n }\n const queue = queues.get(start)\n if (queue) {\n queue.push(index)\n } else {\n queues.set(start, [index])\n }\n }\n\n const used = new Set<number>()\n const pending: runtime.Line[] = []\n\n for (const annotation of annotations) {\n const start = annotation.time?.start\n if (start === undefined) {\n continue\n }\n const index = queues.get(start)?.shift()\n if (index === undefined) {\n pending.push(annotation)\n continue\n }\n matched.set(index, annotation)\n used.add(index)\n }\n\n if (tolerance <= 0) {\n return matched\n }\n\n for (const annotation of pending) {\n const start = annotation.time?.start\n if (start === undefined) {\n continue\n }\n\n let target = -1\n let distance = Number.POSITIVE_INFINITY\n\n for (const [index, lineStart] of starts.entries()) {\n if (lineStart === undefined || used.has(index)) {\n continue\n }\n const current = Math.abs(lineStart - start)\n // Only a strictly smaller distance wins, so equally distant lines keep the earlier one.\n if (current <= tolerance && current < distance) {\n target = index\n distance = current\n }\n }\n\n if (target !== -1) {\n matched.set(target, annotation)\n used.add(target)\n }\n }\n\n return matched\n}\n\n/**\n * Attaches annotation tracks to parsed lines by start time.\n *\n * Annotations that match nothing are dropped, and an instrumental line consumes its annotation without carrying one.\n * Any annotation already on a line is kept, with the matched track appended to its layer.\n */\nexport const attachLineAnnotations = (lines: runtime.Line[], sources: AnnotationSources, options: AnnotationMatchOptions = {}): runtime.Line[] => {\n // A negative or non-finite tolerance would make every comparison meaningless, so it degrades to exact matching.\n const tolerance = Number.isFinite(options.tolerance) ? Math.max(0, options.tolerance ?? 0) : 0\n\n const translations = matchEntries(lines, (sources.translations ?? []).filter(isReadable), tolerance)\n const romans = matchEntries(lines, (sources.romans ?? []).filter(isReadable), tolerance)\n if (translations.size === 0 && romans.size === 0) {\n return lines\n }\n\n return lines.map((line, index) => {\n const translation = translations.get(index)\n const roman = romans.get(index)\n\n // An instrumental line has no singing to annotate, and the model rejects annotation on one.\n if (line.type === LineType.INSTRUMENTAL || (translation === undefined && roman === undefined)) {\n return line\n }\n\n return {\n ...line,\n annotation: makeLineAnnotation({\n romans: [\n ...(line.annotation?.romans ?? []),\n ...(roman === undefined ? [] : [makeLineAnnotationRoman({ text: readText(roman), language: roman.languages[0] })]),\n ],\n translations: [\n ...(line.annotation?.translations ?? []),\n ...(translation === undefined ? [] : [makeLineAnnotationTranslation({ text: readText(translation), language: translation.languages[0] })]),\n ],\n }),\n }\n })\n}\n","export * from './annotation'\n\n// A colon always separates units, so `1:14:514` means hours, minutes, and seconds, not minutes, seconds, and milliseconds.\nconst TIME_PATTERN = /^(?:(?:(\\d+):)?(\\d+):)?(\\d+)(?:\\.(\\d+))?$/\n\n/**\n * Bare integers are excluded because formats may use them as millisecond counts or offsets.\n */\nexport const isTime = (body: string) => {\n const trimmed = body.trim()\n return (trimmed.includes(':') || trimmed.includes('.')) && TIME_PATTERN.test(trimmed)\n}\n\n/**\n * Fractional seconds are padded or truncated to millisecond precision.\n *\n * Invalid clock syntax returns `undefined`; time bounds are left to model validation.\n */\nexport const parseTime = (body: string) => {\n if (!isTime(body)) {\n return undefined\n }\n\n const match = TIME_PATTERN.exec(body.trim()) as RegExpExecArray\n\n const hour = Number(match[1] ?? 0)\n const minute = Number(match[2] ?? 0)\n const second = Number(match[3])\n const millisecond = Number((match[4] ?? '').padEnd(3, '0').slice(0, 3))\n\n return ((hour * 60 + minute) * 60 + second) * 1000 + millisecond\n}\n\n/**\n * Tokens `h`, `m`, `s`, and `S` select hours, minutes, seconds, and fractional seconds.\n * Repeated unit tokens set minimum width; `S` tokens select up to three fractional digits.\n *\n * Omitting hours carries them into minutes; negative or non-finite input writes zero fields.\n */\nexport const formatTime = (time: number, format = 'mm:ss.SSS') => {\n if (!Number.isFinite(time) || time < 0) {\n return format.replace(/h+|m+|s+|S+/g, (run) => '0'.repeat(run.length))\n }\n\n const total = Math.floor(time)\n const totalSeconds = Math.floor(total / 1000)\n const totalMinutes = Math.floor(totalSeconds / 60)\n const millisecond = String(total % 1000).padStart(3, '0')\n\n const fields: Record<string, string> = {\n h: String(Math.floor(totalMinutes / 60)),\n m: String(format.includes('h') ? totalMinutes % 60 : totalMinutes),\n s: String(totalSeconds % 60),\n S: millisecond,\n }\n\n return format.replace(/h+|m+|s+|S+/g, (run) => {\n const field = fields[run[0] as string] as string\n\n if (run[0] === 'S') {\n return field.slice(0, run.length)\n }\n\n return field.padStart(run.length, '0')\n })\n}\n","export const FORMAT = 'lrc'\n\nexport interface LrcContent {\n original: string\n translations?: string\n romans?: string\n}\n\nexport const readLrcContent = (input: unknown): LrcContent | null => {\n if (typeof input === 'string') {\n return { original: input }\n }\n if (typeof input !== 'object' || input === null) {\n return null\n }\n\n const source = input as Partial<LrcContent>\n if (typeof source.original !== 'string') {\n return null\n }\n\n return {\n original: source.original,\n translations: typeof source.translations === 'string' ? source.translations : undefined,\n romans: typeof source.romans === 'string' ? source.romans : undefined,\n }\n}\n","import { isTime, parseTime } from '@root/utils'\n\nconst WORD_OFFSET_PATTERN = /^(\\d+)(?:,(\\d+))?$/\n\nconst WORD_CLOCK_DURATION_PATTERN = /^([^,]+),(\\d+)$/\n\nconst WORD_TAG_PATTERN = /<([^>]*)>/g\n\nexport const TAG_PATTERN = /\\[([^\\]]*)\\]/g\n\nexport const META_PATTERN = /^([A-Za-z0-9_-]+)\\s*:\\s*([\\s\\S]*)$/\n\n/**\n * Integer tags use `offset[,duration]` in milliseconds relative to the line start.\n * Clock tags are absolute and may append a duration in milliseconds, as in `<00:01.50,500>`.\n * A missing or zero duration leaves the word end unspecified.\n */\nexport const parseWordTime = (body: string, lineStart: number) => {\n if (isTime(body)) {\n const start = parseTime(body)\n\n return start === undefined ? undefined : { start, end: undefined }\n }\n\n const match = WORD_OFFSET_PATTERN.exec(body.trim())\n if (match) {\n const start = lineStart + Number(match[1])\n const duration = Number(match[2] ?? 0)\n\n return { start, end: duration > 0 ? start + duration : undefined }\n }\n\n const clock = WORD_CLOCK_DURATION_PATTERN.exec(body.trim())\n if (clock && isTime(clock[1] ?? '')) {\n const start = parseTime(clock[1] ?? '')\n const duration = Number(clock[2] ?? 0)\n\n return start === undefined ? undefined : { start, end: duration > 0 ? start + duration : undefined }\n }\n\n return undefined\n}\n\n/**\n * Tags precede their text, as in `<0,500>hel`.\n */\nexport interface WordPiece {\n text: string\n tag?: string\n}\n\n/**\n * Untagged leading text is preserved; tags without following text produce no piece.\n * A trailing tag still yields an empty piece so it can close the preceding word.\n */\nexport const splitWordTags = (content: string) => {\n const pieces: WordPiece[] = []\n let tag: string | undefined\n let cursor = 0\n\n for (const match of content.matchAll(WORD_TAG_PATTERN)) {\n const at = match.index\n const before = content.slice(cursor, at)\n\n if (before) {\n pieces.push({ text: before, tag })\n }\n\n tag = match[1] ?? ''\n cursor = at + match[0].length\n }\n\n const tail = content.slice(cursor)\n if (tail) {\n pieces.push({ text: tail, tag })\n } else if (tag !== undefined) {\n pieces.push({ text: '', tag })\n }\n\n return pieces\n}\n","import { isTime } from '@root/utils'\n\nimport { readLrcContent } from './common'\nimport { TAG_PATTERN } from './utils'\n\n/**\n * A single readable time tag is enough for recognition; metadata alone is not.\n * This is a format probe, not a validity check, so an input of another shape is simply not LRC.\n */\nexport const detectLrc = (input: unknown) => {\n const content = readLrcContent(input)\n if (!content) {\n return false\n }\n\n const lines = content.original.replace(/\\r\\n?/g, '\\n').split('\\n')\n return lines.some((line) => [...line.matchAll(TAG_PATTERN)].some((match) => isTime(match[1] ?? '')))\n}\n","import type { runtime } from '@gondola-lyrics/model'\nimport type { LrcContent } from './common'\n\nimport { LineType, WordType } from '@gondola-lyrics/model/runtime'\n\nimport { formatTime } from '@root/utils'\n\nexport type LrcGenerateResult = LrcContent\n\nexport interface LrcGenerateOptions {\n /**\n * Word-level timestamp style; defaults to `angle`.\n *\n * `angle` is the enhanced LRC form and round-trips through this parser.\n *\n * `bracket` matches line timestamps, so this parser reads it as new lines rather than words.\n */\n wordTiming?: 'angle' | 'bracket' | 'none'\n /** Clock format for every timestamp; defaults to `mm:ss.SSS`. */\n timeFormat?: string\n /** Emit the metadata header; defaults to `true`. */\n includeMeta?: boolean\n /** Append the word duration to its tag, as in `<00:01.00,500>`. */\n includeWordEnd?: boolean\n /** Emit instrumental lines as a timestamp with no text; defaults to `true`. */\n includeInstrumental?: boolean\n}\n\ntype LrcGenerateOptionsRequired = Required<LrcGenerateOptions>\n\nconst resolveOptions = (options: LrcGenerateOptions = {}): LrcGenerateOptionsRequired => {\n return {\n wordTiming: options.wordTiming ?? 'angle',\n timeFormat: options.timeFormat ?? 'mm:ss.SSS',\n includeMeta: options.includeMeta ?? true,\n includeWordEnd: options.includeWordEnd ?? false,\n includeInstrumental: options.includeInstrumental ?? true,\n }\n}\n\nconst renderTimeTag = (time: number, options: LrcGenerateOptionsRequired) => {\n return `[${formatTime(time, options.timeFormat)}]`\n}\n\nconst renderWordTag = (start: number, end: number | undefined, options: LrcGenerateOptionsRequired) => {\n if (options.wordTiming === 'none') {\n return ''\n }\n\n const open = options.wordTiming === 'bracket' ? '[' : '<'\n const close = options.wordTiming === 'bracket' ? ']' : '>'\n // A duration of zero still states where the word stops, so it is written when the end is known.\n const duration = options.includeWordEnd && end !== undefined ? `,${Math.max(0, end - start)}` : ''\n\n return `${open}${formatTime(start, options.timeFormat)}${duration}${close}`\n}\n\n/**\n * Plain text joins words as written, including the separators stored as space words.\n */\nconst renderPlainText = (line: runtime.Line) => {\n const content = line.content\n if (!content) {\n return ''\n }\n if (content.text !== undefined) {\n return content.text\n }\n\n return content.words.map((word) => word.text).join('')\n}\n\n/**\n * Word-level lines interleave tags with their text; untimed words stay untagged.\n */\nconst renderSyllableText = (line: runtime.Line, options: LrcGenerateOptionsRequired) => {\n const content = line.content\n if (!content?.words.length) {\n return renderPlainText(line)\n }\n\n return content.words\n .map((word) => {\n if (word.type === WordType.SPACE) {\n return word.text\n }\n const start = word.time?.start\n if (start === undefined) {\n return word.text\n }\n return `${renderWordTag(start, word.time?.end, options)}${word.text}`\n })\n .join('')\n}\n\nconst renderMetaRows = (meta: runtime.Meta, options: LrcGenerateOptionsRequired) => {\n const rows: string[] = []\n\n for (const title of meta.titles) {\n rows.push(`[ti:${title.text}]`)\n }\n for (const artist of meta.artists) {\n rows.push(`[ar:${artist.text}]`)\n }\n for (const album of meta.albums) {\n rows.push(`[al:${album.text}]`)\n }\n for (const author of meta.authors) {\n rows.push(`[by:${author.text}]`)\n }\n if (meta.offset) {\n rows.push(`[offset:${meta.offset}]`)\n }\n if (meta.duration !== undefined) {\n rows.push(`[length:${formatTime(meta.duration, options.timeFormat)}]`)\n }\n for (const isrc of meta.isrcs) {\n rows.push(`[isrc:${isrc}]`)\n }\n for (const unknown of meta.unknowns) {\n rows.push(`[${unknown.key}:${unknown.value}]`)\n }\n\n return rows\n}\n\n/**\n * Returns `undefined` for a line the options leave out, so callers can drop it rather than emit a blank row.\n */\nconst renderLineRow = (line: runtime.Line, options: LrcGenerateOptionsRequired, wordLevel: boolean) => {\n if (line.type === LineType.INSTRUMENTAL && !options.includeInstrumental) {\n return undefined\n }\n\n // An untimed lyric still needs a stamp, so it starts at zero like any other line.\n const time = renderTimeTag(line.time?.start ?? 0, options)\n\n // An instrumental line carries no content, so its timestamp alone marks the gap.\n if (line.type === LineType.INSTRUMENTAL) {\n return time\n }\n\n const text = wordLevel && line.content?.words.length ? renderSyllableText(line, options) : renderPlainText(line)\n return `${time}${text}`\n}\n\nconst renderAnnotationRows = (lines: runtime.Line[], options: LrcGenerateOptionsRequired, pick: (line: runtime.Line) => { text: string }[]) => {\n const rows: string[] = []\n\n for (const line of lines) {\n const start = line.time?.start\n if (start === undefined) {\n continue\n }\n for (const item of pick(line)) {\n rows.push(`${renderTimeTag(start, options)}${item.text}`)\n }\n }\n\n return rows\n}\n\nconst joinBlocks = (header: string[], body: string[]) => {\n const blocks: string[] = []\n if (header.length) {\n blocks.push(header.join('\\n'))\n }\n if (body.length) {\n blocks.push(body.join('\\n'))\n }\n\n return blocks.join('\\n\\n')\n}\n\n/**\n * Generates the LRC text plus the annotations a single file cannot hold.\n *\n * Word tags appear only for a word-timed lyric when `wordTiming` is not `none`; otherwise every line stays whole.\n */\nexport const generateLrc = (lyric: runtime.Lyric, options: LrcGenerateOptions = {}): LrcGenerateResult => {\n const resolved = resolveOptions(options)\n const header = resolved.includeMeta && lyric.meta ? renderMetaRows(lyric.meta, resolved) : []\n\n const hasWords = lyric.lines.some((line) => (line.content?.words.length ?? 0) > 0)\n const wordLevel = hasWords && resolved.wordTiming !== 'none'\n\n const body = lyric.lines\n .map((line) => {\n return renderLineRow(line, resolved, wordLevel)\n })\n .filter((row) => row !== undefined)\n\n const result: LrcGenerateResult = {\n original: joinBlocks(header, body),\n }\n\n const translations = renderAnnotationRows(lyric.lines, resolved, (line) => line.annotation?.translations ?? [])\n if (translations.length) {\n result.translations = joinBlocks(header, translations)\n }\n\n const romans = renderAnnotationRows(lyric.lines, resolved, (line) => line.annotation?.romans ?? [])\n if (romans.length) {\n result.romans = joinBlocks(header, romans)\n }\n\n return result\n}\n","import type { runtime } from '@gondola-lyrics/model'\nimport type { LrcContent } from './common'\n\nimport {\n Timing,\n LyricStatus,\n makeLineContentText,\n makeLineContentWords,\n makeLineNormal,\n makeLyric,\n makeTimeRange,\n makeMeta,\n makeText,\n makeUnknownEntry,\n makeWordNormal,\n makeWordSpace,\n deriveLanguageUsages,\n} from '@gondola-lyrics/model/runtime'\n\nimport { attachLineAnnotations, isTime, parseTime } from '@root/utils'\n\nimport { FORMAT, readLrcContent } from './common'\nimport { META_PATTERN, TAG_PATTERN, parseWordTime, splitWordTags } from './utils'\n\nexport interface LrcParseOptions {\n /**\n * How far an annotation's timestamp may sit from a line's start, in milliseconds.\n * Defaults to `0`, which requires an exact match.\n */\n annotationTolerance?: number\n}\n\nexport type LrcParseInput = LrcContent\n\ninterface TimedLine {\n text: string\n start: number\n}\n\ninterface MetaTag {\n key: string\n value: string\n}\n\nconst isBareNumber = (value: string) => {\n return /^\\d+$/.test(value.trim())\n}\n\n/**\n * A trailing tag alone still makes the line word-level, so it can close the last word.\n * Unreadable angle-bracket text stays plain lyric content.\n */\nconst isWordTimed = (text: string) => {\n return splitWordTags(text).some((piece) => piece.tag !== undefined && parseWordTime(piece.tag, 0) !== undefined)\n}\n\nconst readMeta = (body: string) => {\n const match = META_PATTERN.exec(body)\n if (!match) {\n return undefined\n }\n\n return { key: (match[1] ?? '').toLowerCase(), value: (match[2] ?? '').trim() }\n}\n\nconst readGroup = (tags: string[], text: string, timed: TimedLine[], meta: MetaTag[]) => {\n for (const tag of tags) {\n const start = isTime(tag) ? parseTime(tag) : undefined\n if (start !== undefined) {\n timed.push({ text, start })\n continue\n }\n\n const pair = readMeta(tag)\n if (pair) {\n meta.push(pair)\n }\n }\n}\n\n/**\n * Stacked timestamps share the following text, as in `[00:10][00:20]twice`.\n * Text before the first tag is discarded because it has no timing association.\n */\nconst readTimedLines = (text: string) => {\n const timed: TimedLine[] = []\n const meta: MetaTag[] = []\n\n for (const raw of text.replace(/\\r\\n?/g, '\\n').split('\\n')) {\n const line = raw.trim()\n if (!line) {\n continue\n }\n\n const matches = [...line.matchAll(TAG_PATTERN)]\n let group: string[] = []\n\n for (const [index, match] of matches.entries()) {\n const at = match.index + match[0].length\n const next = matches[index + 1]?.index ?? line.length\n const shared = line.slice(at, next)\n\n group.push(match[1] ?? '')\n\n if (!shared.trim()) {\n continue\n }\n\n readGroup(group, shared, timed, meta)\n group = []\n }\n\n // Trailing timestamps still represent empty timed lines.\n readGroup(group, '', timed, meta)\n }\n\n return { timed, meta }\n}\n\n/**\n * Unmapped tags and unparseable durations remain in `unknowns` to avoid discarding metadata.\n * Keys are lowercased and values trimmed, so original tag spelling is not preserved.\n */\nconst readMetaTags = (tags: MetaTag[]) => {\n const meta = makeMeta()\n\n for (const tag of tags) {\n switch (tag.key) {\n case 'length':\n case 'duration': {\n // Duration tags accept both clock notation and integer milliseconds.\n const duration = parseTime(tag.value) ?? (isBareNumber(tag.value) ? Number(tag.value) : Number.NaN)\n if (Number.isFinite(duration)) {\n meta.duration = Math.trunc(duration)\n } else {\n meta.unknowns.push(makeUnknownEntry({ key: tag.key, value: tag.value }))\n }\n break\n }\n case 'offset':\n meta.offset = Math.trunc(Number(tag.value)) || 0\n break\n case 'ti':\n case 'title':\n meta.titles.push(makeText({ text: tag.value }))\n break\n case 'ar':\n case 'artist':\n meta.artists.push(makeText({ text: tag.value }))\n break\n case 'al':\n case 'album':\n meta.albums.push(makeText({ text: tag.value }))\n break\n case 'by':\n case 'author':\n meta.authors.push(makeText({ text: tag.value }))\n break\n case 'isrc':\n meta.isrcs.push(tag.value)\n break\n default:\n meta.unknowns.push(makeUnknownEntry({ key: tag.key, value: tag.value }))\n break\n }\n }\n\n return meta\n}\n\n/**\n * Whitespace becomes a space word preserving the source run, as in `lo World`.\n * A tag opens a segment; untagged words continue from the previous end so the timeline has no holes.\n * Explicit word ends are kept as written, so duration tags survive unchanged.\n */\nconst readWords = (text: string, lineStart: number) => {\n const drafts: { text: string; start?: number; end?: number; space?: boolean }[] = []\n const open = new Set<{ start?: number; end?: number }>()\n\n const closeSegment = (boundary: number | undefined) => {\n if (boundary === undefined) {\n return\n }\n for (const draft of open) {\n if (draft.start !== undefined && draft.end === undefined && boundary >= draft.start) {\n draft.end = boundary\n }\n }\n open.clear()\n }\n\n let segmentStart: number | undefined = lineStart\n let cursor: number | undefined = lineStart\n\n for (const piece of splitWordTags(text)) {\n const time = piece.tag === undefined ? undefined : parseWordTime(piece.tag, lineStart)\n\n // A tag, even without following text, closes the segment it opens.\n if (time?.start !== undefined) {\n closeSegment(time.start)\n segmentStart = time.start\n cursor = time.start\n }\n\n if (!piece.text) {\n continue\n }\n\n let applied = false\n\n for (const run of piece.text.split(/(\\s+)/)) {\n if (!run) {\n continue\n }\n\n // A separator keeps its own text and never carries a tag.\n if (!run.trim()) {\n if (drafts.length) {\n drafts.push({ text: run, space: true })\n }\n continue\n }\n\n const tagged = !applied && time?.start !== undefined\n applied = true\n\n const start = tagged ? segmentStart : cursor\n const end = tagged ? time?.end : undefined\n const draft = { text: run, start, end }\n\n drafts.push(draft)\n if (end !== undefined) {\n cursor = end\n } else {\n open.add(draft)\n }\n }\n }\n\n return { drafts, closeSegment }\n}\n\n/**\n * A line ends at the next line's start; without one, it ends at its final word's end.\n * An open word is closed by the next timed word, or by the line end, so every sung word stays inside the line.\n */\nconst readLine = (text: string, start: number, nextStart: number | undefined) => {\n const wordTimed = isWordTimed(text)\n const { drafts, closeSegment } = wordTimed ? readWords(text, start) : { drafts: [], closeSegment: () => {} }\n\n let end = nextStart !== undefined && nextStart >= start ? nextStart : undefined\n\n // Without a later line, a final word carrying its own end bounds the line; otherwise it stays open.\n if (end === undefined) {\n const last = [...drafts].reverse().find((draft) => !draft.space && draft.start !== undefined)\n end = last?.end\n }\n\n // Earlier segments were closed by their next tag, so this only closes the final one.\n closeSegment(end)\n\n const words = drafts.map((draft) => {\n if (draft.space) {\n return makeWordSpace({ text: draft.text })\n }\n // A missing start omits the range instead of inventing a word timestamp.\n const time = draft.start === undefined ? undefined : makeTimeRange({ start: draft.start, end: draft.end })\n return makeWordNormal({ text: draft.text, time })\n })\n\n const time = makeTimeRange({ start, end })\n const content = wordTimed ? makeLineContentWords(words) : makeLineContentText(text.trim())\n\n return makeLineNormal({ time, content })\n}\n\n/**\n * Builds one line per timed row, so an annotation track reuses the original text's reading unchanged.\n * An empty timed row still yields a line, since it marks a gap rather than a parse failure.\n */\nconst readLines = (timed: TimedLine[]): runtime.Line[] => {\n const lines: runtime.Line[] = []\n\n for (const [index, item] of timed.entries()) {\n const next = timed[index + 1]?.start\n // LRC has no line ends; only a nondecreasing next timestamp can supply one.\n lines.push(readLine(item.text, item.start, next))\n }\n\n return lines\n}\n\nconst parseLrcText = (text: string): runtime.Lyric => {\n const { timed, meta } = readTimedLines(text)\n const metaTags = readMetaTags(meta)\n\n if (!timed.length) {\n return makeLyric({ format: FORMAT, status: LyricStatus.INVALID, meta: metaTags })\n }\n\n const lines = readLines(timed)\n const wordTimed = lines.some((line) => (line.content?.words.length ?? 0) > 0)\n\n return makeLyric({\n format: FORMAT,\n status: LyricStatus.VALID,\n timing: wordTimed ? Timing.WORD : Timing.LINE,\n meta: metaTags,\n languages: deriveLanguageUsages(lines),\n lines,\n })\n}\n\n/**\n * Source order, empty timed lines, and missing word times are preserved for model validation.\n *\n * Annotation tracks are read as lines too and attached by timestamp, and rows that match no line are dropped.\n *\n * `INVALID` means no readable timed lines; `VALID` does not imply model validation has passed.\n */\nexport const parseLrc = (input: string | LrcParseInput, options: LrcParseOptions = {}): runtime.Lyric => {\n const source = readLrcContent(input) ?? { original: '' }\n const lyric = parseLrcText(source.original)\n\n if (lyric.status === LyricStatus.INVALID) {\n return lyric\n }\n\n const translations = source.translations === undefined ? [] : readLines(readTimedLines(source.translations).timed)\n const romans = source.romans === undefined ? [] : readLines(readTimedLines(source.romans).timed)\n\n if (translations.length === 0 && romans.length === 0) {\n return lyric\n }\n\n const lines = attachLineAnnotations(lyric.lines, { translations, romans }, { tolerance: options.annotationTolerance })\n\n return { ...lyric, lines }\n}\n","import type { runtime } from '@gondola-lyrics/model'\nimport type { LyricFormat } from './format'\nimport type { LrcGenerateOptions, LrcGenerateResult, LrcParseInput, LrcParseOptions } from './lrc'\n\nimport { detectLrc, generateLrc, parseLrc } from './lrc'\n\n/**\n * Recognition reads only the original text; the annotation tracks never decide the format.\n *\n * Unrecognized text returns `undefined`.\n */\nexport const detectFormat = (input: unknown) => {\n if (detectLrc(input)) {\n return 'lrc'\n }\n\n return undefined\n}\n\n/**\n * Unrecognized text returns `null`; a recognized format returns a lyric with its parser-assigned status.\n * Annotation tracks travel with the source and are attached by the format's parser.\n */\nexport const parse = (input: unknown, options: LrcParseOptions = {}): runtime.Lyric | null => {\n const format = detectFormat(input)\n if (!format) {\n return null\n }\n\n switch (format) {\n case 'lrc':\n return parseLrc(input as string | LrcParseInput, options)\n }\n}\n\n/**\n * Generates text in the requested format, which need not match `lyric.format`.\n */\nexport const generate = (lyric: runtime.Lyric, target: LyricFormat, options: LrcGenerateOptions = {}): LrcGenerateResult => {\n switch (target) {\n case 'lrc':\n return generateLrc(lyric, options)\n }\n}\n\nexport type { LyricFormat } from './format'\n\nexport * from './lrc'\n"],"mappings":"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"}
1
+ {"version":3,"file":"formats.js","names":["readWords","readLine"],"sources":["../src/utils/annotation.ts","../src/utils/language.ts","../src/utils/time.ts","../src/utils/xml/utils.ts","../src/utils/xml/reader.ts","../src/formats/lrc/common.ts","../src/formats/lrc/utils.ts","../src/formats/lrc/detect.ts","../src/formats/lrc/generator.ts","../src/formats/result.ts","../src/formats/lrc/parser.ts","../src/formats/ttml/common.ts","../src/formats/ttml/detect.ts","../src/formats/ttml/utils.ts","../src/formats/ttml/parser.ts","../src/formats/index.ts"],"sourcesContent":["import type { runtime } from '@gondola-lyrics/model'\n\nimport { LineType, getWordsText, makeLineAnnotation, makeLineAnnotationRoman, makeLineAnnotationTranslation } from '@gondola-lyrics/model/runtime'\n\nexport interface AnnotationSources {\n translations?: runtime.Line[]\n romans?: runtime.Line[]\n}\n\nexport interface AnnotationMatchOptions {\n /**\n * How far an annotation's start may sit from a line's start, in milliseconds.\n * Defaults to `0`, which requires an exact match.\n */\n tolerance?: number\n}\n\n/**\n * An annotation line carries its text in either field, depending on whether the track used word tags.\n */\nconst readText = (line: runtime.Line) => {\n return (line.content?.text ?? getWordsText(line.content?.words ?? [])).trim()\n}\n\nconst isReadable = (line: runtime.Line) => {\n return line.time?.start !== undefined && readText(line) !== ''\n}\n\n/**\n * Assigns each annotation line to a lyric line index.\n *\n * Exact matches claim their lines first, so a fuzzy match never takes a line an exact one needed.\n * The remaining annotations take the nearest unclaimed line within the tolerance, and ties keep the earlier line.\n */\nconst matchEntries = (lines: runtime.Line[], annotations: runtime.Line[], tolerance: number) => {\n const matched = new Map<number, runtime.Line>()\n if (annotations.length === 0) {\n return matched\n }\n\n const starts = lines.map((line) => line.time?.start)\n const queues = new Map<number, number[]>()\n\n for (const [index, start] of starts.entries()) {\n if (start === undefined) {\n continue\n }\n const queue = queues.get(start)\n if (queue) {\n queue.push(index)\n } else {\n queues.set(start, [index])\n }\n }\n\n const used = new Set<number>()\n const pending: runtime.Line[] = []\n\n for (const annotation of annotations) {\n const start = annotation.time?.start\n if (start === undefined) {\n continue\n }\n const index = queues.get(start)?.shift()\n if (index === undefined) {\n pending.push(annotation)\n continue\n }\n matched.set(index, annotation)\n used.add(index)\n }\n\n if (tolerance <= 0) {\n return matched\n }\n\n for (const annotation of pending) {\n const start = annotation.time?.start\n if (start === undefined) {\n continue\n }\n\n let target = -1\n let distance = Number.POSITIVE_INFINITY\n\n for (const [index, lineStart] of starts.entries()) {\n if (lineStart === undefined || used.has(index)) {\n continue\n }\n const current = Math.abs(lineStart - start)\n // Only a strictly smaller distance wins, so equally distant lines keep the earlier one.\n if (current <= tolerance && current < distance) {\n target = index\n distance = current\n }\n }\n\n if (target !== -1) {\n matched.set(target, annotation)\n used.add(target)\n }\n }\n\n return matched\n}\n\n/**\n * Attaches annotation tracks to parsed lines by start time.\n *\n * Annotations that match nothing are dropped, and an instrumental line consumes its annotation without carrying one.\n * Any annotation already on a line is kept, with the matched track appended to its layer.\n */\nexport const attachLineAnnotations = (lines: runtime.Line[], sources: AnnotationSources, options: AnnotationMatchOptions = {}): runtime.Line[] => {\n // A negative or non-finite tolerance would make every comparison meaningless, so it degrades to exact matching.\n const tolerance = Number.isFinite(options.tolerance) ? Math.max(0, options.tolerance ?? 0) : 0\n\n const translations = matchEntries(lines, (sources.translations ?? []).filter(isReadable), tolerance)\n const romans = matchEntries(lines, (sources.romans ?? []).filter(isReadable), tolerance)\n if (translations.size === 0 && romans.size === 0) {\n return lines\n }\n\n return lines.map((line, index) => {\n const translation = translations.get(index)\n const roman = romans.get(index)\n\n // An instrumental line has no singing to annotate, and the model rejects annotation on one.\n if (line.type === LineType.INSTRUMENTAL || (translation === undefined && roman === undefined)) {\n return line\n }\n\n return {\n ...line,\n annotation: makeLineAnnotation({\n romans: [\n ...(line.annotation?.romans ?? []),\n ...(roman === undefined ? [] : [makeLineAnnotationRoman({ text: readText(roman), language: roman.languages[0] })]),\n ],\n translations: [\n ...(line.annotation?.translations ?? []),\n ...(translation === undefined ? [] : [makeLineAnnotationTranslation({ text: readText(translation), language: translation.languages[0] })]),\n ],\n }),\n }\n })\n}\n","/**\n * Returns a language tag in the canonical form the model stores, which is lower case.\n *\n * This mirrors the model's own helper, which it does not export.\n * Comparing tags stays with the consumer, since BCP 47 matches by subtag prefix rather than by string equality.\n */\nexport const lowerTag = (tag: string | undefined): string | undefined => {\n return tag ? tag.toLowerCase() : undefined\n}\n","// A colon always separates units, so `1:14:514` means hours, minutes, and seconds, not minutes, seconds, and milliseconds.\nconst CLOCK_PATTERN = /^(?:(?:(\\d+):)?(\\d+):)?(\\d+)(?:\\.(\\d+))?$/\n\n// `ms` has to be tried before `m`, so the longer unit wins.\nconst OFFSET_PATTERN = /^(\\d+(?:\\.\\d+)?)(ms|h|m|s)$/iu\n\n/**\n * Reads a clock value, where the fraction is padded or truncated to millisecond precision.\n *\n * `14.514` is fourteen seconds, not fourteen minutes: a colon, not a dot, introduces the larger units.\n */\nconst parseClock = (value: string): number | undefined => {\n // A dot alone is enough to make a number a clock reading, which is what keeps `500` out.\n if (!value.includes(':') && !value.includes('.')) {\n return undefined\n }\n\n const match = CLOCK_PATTERN.exec(value)\n if (!match) {\n return undefined\n }\n\n const hour = Number(match[1] ?? 0)\n const minute = Number(match[2] ?? 0)\n const second = Number(match[3] ?? 0)\n const millisecond = Number((match[4] ?? '').padEnd(3, '0').slice(0, 3))\n\n return ((hour * 60 + minute) * 60 + second) * 1000 + millisecond\n}\n\n/**\n * Reads an offset such as `1.5s`, `2m`, `500ms`, or `1h`.\n */\nconst parseOffset = (value: string): number | undefined => {\n const match = OFFSET_PATTERN.exec(value)\n if (!match) {\n return undefined\n }\n\n const amount = Number(match[1])\n if (!Number.isFinite(amount)) {\n return undefined\n }\n\n switch ((match[2] ?? '').toLowerCase()) {\n case 'h':\n return Math.round(amount * 3600000)\n case 'm':\n return Math.round(amount * 60000)\n case 'ms':\n return Math.round(amount)\n default:\n return Math.round(amount * 1000)\n }\n}\n\n/**\n * Reports whether a value reads as a time.\n */\nexport const isTime = (body: string) => {\n return parseTime(body) !== undefined\n}\n\n/**\n * Reads a time value into milliseconds.\n *\n * A clock reading accepts `ss.fraction`, `mm:ss.fraction`, and `hh:mm:ss.fraction`.\n * An offset accepts a number with an `h`, `m`, `s`, or `ms` suffix.\n * A bare number returns `undefined`, and so does anything else.\n */\nexport const parseTime = (body: string): number | undefined => {\n const trimmed = body.trim()\n if (trimmed === '') {\n return undefined\n }\n\n return parseOffset(trimmed) ?? parseClock(trimmed)\n}\n\n/**\n * Tokens `h`, `m`, `s`, and `S` select hours, minutes, seconds, and fractional seconds.\n * Repeated unit tokens set minimum width; `S` tokens select up to three fractional digits.\n *\n * Omitting hours carries them into minutes; negative or non-finite input writes zero fields.\n */\nexport const formatTime = (time: number, format = 'mm:ss.SSS') => {\n if (!Number.isFinite(time) || time < 0) {\n return format.replace(/h+|m+|s+|S+/g, (run) => '0'.repeat(run.length))\n }\n\n const total = Math.floor(time)\n const totalSeconds = Math.floor(total / 1000)\n const totalMinutes = Math.floor(totalSeconds / 60)\n const millisecond = String(total % 1000).padStart(3, '0')\n\n const fields: Record<string, string> = {\n h: String(Math.floor(totalMinutes / 60)),\n m: String(format.includes('h') ? totalMinutes % 60 : totalMinutes),\n s: String(totalSeconds % 60),\n S: millisecond,\n }\n\n return format.replace(/h+|m+|s+|S+/g, (run) => {\n const field = fields[run[0] as string] as string\n\n if (run[0] === 'S') {\n return field.slice(0, run.length)\n }\n\n return field.padStart(run.length, '0')\n })\n}\n","export const XmlNodeType = {\n element: 1,\n text: 2,\n cdata: 3,\n} as const\n\nexport interface XmlAttribute {\n /** Name exactly as written, namespace prefix included, such as `ttm:agent`. */\n qualifiedName: string\n /** Decoded value, with entity references already resolved. */\n value: string\n /** Namespace prefix such as `ttm`, empty when the name carries none. */\n namespacePrefix: string\n /** Name without its prefix, such as `agent`, which is what format lookups match. */\n localName: string\n}\n\nexport interface XmlElement {\n /** Discriminant that tells this node apart from text and CDATA. */\n nodeType: typeof XmlNodeType.element\n /** Name exactly as written, namespace prefix included, such as `ttm:name`. */\n qualifiedName: string\n /** Namespace prefix such as `ttm`, empty when the name carries none. */\n namespacePrefix: string\n /** Name without its prefix, such as `name`, which is what format lookups match. */\n localName: string\n /** Attributes in document order, which keeps a source's own ordering readable. */\n attributes: XmlAttribute[]\n /** Child nodes in document order, text and CDATA included. */\n children: XmlNode[]\n}\n\nexport interface XmlText {\n /** Discriminant that tells this node apart from elements and CDATA. */\n nodeType: typeof XmlNodeType.text\n /** Decoded text, with entity references already resolved. */\n text: string\n}\n\nexport interface XmlCdata {\n /** Discriminant that tells this node apart from elements and text. */\n nodeType: typeof XmlNodeType.cdata\n /** Raw text between the CDATA markers, which is never decoded. */\n text: string\n}\n\nexport type XmlNode = XmlElement | XmlText | XmlCdata\n\n/**\n * Only prefixed names need their split cached, since an unprefixed one keeps its name as the local part.\n * The cache is bounded, which keeps an adversarial document from growing it without limit.\n */\nconst prefixedNames = new Map<string, { namespacePrefix: string; localName: string }>()\n\n/**\n * Splits a qualified name into its namespace prefix and local name.\n */\nconst splitXmlName = (name: string) => {\n const cached = prefixedNames.get(name)\n if (cached !== undefined) {\n return cached\n }\n\n const colon = name.indexOf(':')\n const split = colon === -1 ? { namespacePrefix: '', localName: name } : { namespacePrefix: name.slice(0, colon), localName: name.slice(colon + 1) }\n if (split.namespacePrefix !== '' && prefixedNames.size < 1024) {\n prefixedNames.set(name, split)\n }\n\n return split\n}\n\n/**\n * Writes the prefix and local part onto a node that already carries the full name.\n */\nexport const applyXmlNameParts = (name: string, target: { namespacePrefix: string; localName: string }) => {\n if (name.indexOf(':') === -1) {\n target.namespacePrefix = ''\n target.localName = name\n return\n }\n\n const split = splitXmlName(name)\n target.namespacePrefix = split.namespacePrefix\n target.localName = split.localName\n}\n\n/**\n * Creates an attribute from its full name and value.\n */\nexport const createXmlAttribute = (qualifiedName: string, value: string): XmlAttribute => {\n const attribute: XmlAttribute = { qualifiedName, value, namespacePrefix: '', localName: qualifiedName }\n applyXmlNameParts(qualifiedName, attribute)\n\n return attribute\n}\n\n/**\n * Creates an element from its full name, with attributes and children in the order given.\n */\nexport const createXmlElement = (\n qualifiedName: string,\n attributes: readonly (readonly [string, string])[] = [],\n children: readonly XmlNode[] = [],\n): XmlElement => {\n const element: XmlElement = {\n nodeType: XmlNodeType.element,\n qualifiedName,\n namespacePrefix: '',\n localName: qualifiedName,\n attributes: attributes.map(([name, value]) => {\n return createXmlAttribute(name, value)\n }),\n children: [...children],\n }\n applyXmlNameParts(qualifiedName, element)\n\n return element\n}\n\n/**\n * Creates a text node, whose value the writer escapes.\n */\nexport const createXmlText = (text: string): XmlText => {\n return { nodeType: XmlNodeType.text, text }\n}\n\n/**\n * Creates a CDATA node, whose value the writer emits verbatim.\n */\nexport const createXmlCdata = (text: string): XmlCdata => {\n return { nodeType: XmlNodeType.cdata, text }\n}\n\n/**\n * Returns the value of one attribute, matched by its local name so the prefix never has to be known.\n */\nexport const getXmlAttribute = (element: XmlElement, localName: string): string | undefined => {\n const attributes = element.attributes\n for (let index = 0; index < attributes.length; index += 1) {\n const attribute = attributes[index]\n if (attribute !== undefined && attribute.localName === localName) {\n return attribute.value\n }\n }\n\n return undefined\n}\n\n/**\n * Returns the value of one attribute, matched by its full name including the prefix.\n */\nexport const getXmlQualifiedAttribute = (element: XmlElement, qualifiedName: string): string | undefined => {\n const attributes = element.attributes\n for (let index = 0; index < attributes.length; index += 1) {\n const attribute = attributes[index]\n if (attribute !== undefined && attribute.qualifiedName === qualifiedName) {\n return attribute.value\n }\n }\n\n return undefined\n}\n\n/**\n * Returns the direct element children carrying one local name.\n */\nexport const getXmlChildElements = (element: XmlElement, localName: string): XmlElement[] => {\n const result: XmlElement[] = []\n const children = element.children\n\n for (let index = 0; index < children.length; index += 1) {\n const child = children[index]\n if (child !== undefined && child.nodeType === XmlNodeType.element && child.localName === localName) {\n result.push(child)\n }\n }\n\n return result\n}\n\n/**\n * Reports whether the element has a direct child with one local name.\n */\nexport const hasXmlChildElement = (element: XmlElement, localName: string): boolean => {\n const children = element.children\n\n for (let index = 0; index < children.length; index += 1) {\n const child = children[index]\n if (child !== undefined && child.nodeType === XmlNodeType.element && child.localName === localName) {\n return true\n }\n }\n\n return false\n}\n\n/**\n * Returns the first element carrying one local name, the element itself included, in document order.\n */\nexport const findXmlElement = (element: XmlElement, localName: string): XmlElement | undefined => {\n const stack: XmlNode[] = [element]\n\n while (stack.length > 0) {\n const node = stack.pop()\n if (node === undefined || node.nodeType !== XmlNodeType.element) {\n continue\n }\n if (node.localName === localName) {\n return node\n }\n // Pushing in reverse makes the stack pop children in document order.\n for (let index = node.children.length - 1; index >= 0; index -= 1) {\n const child = node.children[index]\n if (child !== undefined) {\n stack.push(child)\n }\n }\n }\n\n return undefined\n}\n\n/**\n * Returns every element carrying one local name, the element itself included, in document order.\n */\nexport const findXmlElements = (element: XmlElement, localName: string): XmlElement[] => {\n const result: XmlElement[] = []\n const stack: XmlNode[] = [element]\n\n while (stack.length > 0) {\n const node = stack.pop()\n if (node === undefined || node.nodeType !== XmlNodeType.element) {\n continue\n }\n if (node.localName === localName) {\n result.push(node)\n }\n for (let index = node.children.length - 1; index >= 0; index -= 1) {\n const child = node.children[index]\n if (child !== undefined) {\n stack.push(child)\n }\n }\n }\n\n return result\n}\n\n/**\n * Groups every element carrying one of the given local names, in document order.\n */\nexport const groupXmlElements = (element: XmlElement, localNames: readonly string[]): Map<string, XmlElement[]> => {\n const wanted = new Set(localNames)\n const result = new Map<string, XmlElement[]>()\n const stack: XmlNode[] = [element]\n\n while (stack.length > 0) {\n const node = stack.pop()\n if (node === undefined || node.nodeType !== XmlNodeType.element) {\n continue\n }\n\n if (wanted.has(node.localName)) {\n const group = result.get(node.localName)\n if (group === undefined) {\n result.set(node.localName, [node])\n } else {\n group.push(node)\n }\n }\n\n for (let index = node.children.length - 1; index >= 0; index -= 1) {\n const child = node.children[index]\n if (child !== undefined) {\n stack.push(child)\n }\n }\n }\n\n return result\n}\n\n/**\n * Joins the text and CDATA content of a node and its descendants, in document order.\n */\nexport const getXmlTextContent = (node: XmlNode): string => {\n if (node.nodeType !== XmlNodeType.element) {\n return node.text\n }\n\n const parts: string[] = []\n const stack: XmlNode[] = [node]\n\n while (stack.length > 0) {\n const current = stack.pop()\n if (current === undefined) {\n continue\n }\n if (current.nodeType === XmlNodeType.element) {\n for (let index = current.children.length - 1; index >= 0; index -= 1) {\n const child = current.children[index]\n if (child !== undefined) {\n stack.push(child)\n }\n }\n continue\n }\n parts.push(current.text)\n }\n\n return parts.join('')\n}\n","import type { XmlAttribute, XmlElement } from './utils'\n\nimport { applyXmlNameParts, createXmlAttribute, XmlNodeType } from './utils'\n\nconst LESS_THAN = 0x3c\nconst GREATER_THAN = 0x3e\nconst SLASH = 0x2f\nconst EQUALS = 0x3d\nconst DOUBLE_QUOTE = 0x22\nconst SINGLE_QUOTE = 0x27\nconst EXCLAMATION = 0x21\nconst QUESTION = 0x3f\nconst AMPERSAND = 0x26\nconst HASH = 0x23\nconst HYPHEN = 0x2d\nconst LOWER_X = 0x78\nconst UPPER_X = 0x58\nconst BYTE_ORDER_MARK = 0xfeff\n\nconst ROOT_NAME = '#document'\n\nconst DECIMAL_DIGITS = /^\\d+$/\nconst HEX_DIGITS = /^[0-9a-fA-F]+$/\n\n/**\n * Resolves one entity reference body, leaving anything unrecognized as written.\n */\nconst decodeReference = (body: string): string => {\n switch (body) {\n case 'lt':\n return '<'\n case 'gt':\n return '>'\n case 'amp':\n return '&'\n case 'quot':\n return '\"'\n case 'apos':\n return \"'\"\n }\n\n if (body.charCodeAt(0) !== HASH) {\n return `&${body};`\n }\n\n const hexadecimal = body.charCodeAt(1) === LOWER_X || body.charCodeAt(1) === UPPER_X\n const digits = hexadecimal ? body.slice(2) : body.slice(1)\n if (!(hexadecimal ? HEX_DIGITS : DECIMAL_DIGITS).test(digits)) {\n return `&${body};`\n }\n\n const code = Number.parseInt(digits, hexadecimal ? 16 : 10)\n // Anything outside the Unicode range cannot be written back, so the reference stays literal.\n if (!Number.isFinite(code) || code > 0x10ffff) {\n return `&${body};`\n }\n\n return String.fromCodePoint(code)\n}\n\n/**\n * Replaces entity references in one run of text, leaving bare ampersands alone.\n */\nconst decodeText = (text: string): string => {\n const parts: string[] = []\n const length = text.length\n let start = 0\n let cursor = 0\n\n while (cursor < length) {\n if (text.charCodeAt(cursor) !== AMPERSAND) {\n cursor += 1\n continue\n }\n\n const end = text.indexOf(';', cursor + 1)\n if (end === -1) {\n cursor += 1\n continue\n }\n\n parts.push(text.slice(start, cursor), decodeReference(text.slice(cursor + 1, end)))\n cursor = end + 1\n start = cursor\n }\n\n if (parts.length === 0) {\n return text\n }\n\n parts.push(text.slice(start))\n return parts.join('')\n}\n\n/**\n * Reads a document into a tree, or returns `null` when it is not well formed.\n */\nexport const parseXml = (source: string): XmlElement | null => {\n const length = source.length\n let position = length > 0 && source.charCodeAt(0) === BYTE_ORDER_MARK ? 1 : 0\n let valid = true\n const stack: XmlElement[] = []\n\n const root: XmlElement = {\n nodeType: XmlNodeType.element,\n qualifiedName: ROOT_NAME,\n namespacePrefix: '',\n localName: ROOT_NAME,\n attributes: [],\n children: [],\n }\n stack.push(root)\n\n /**\n * Appends a text node to the element being read.\n */\n const readTextNode = (end: number) => {\n const start = position\n position = end\n if (start === end) {\n return\n }\n\n const parent = stack[stack.length - 1]\n if (parent === undefined) {\n valid = false\n return\n }\n\n const raw = source.slice(start, end)\n parent.children.push({ nodeType: XmlNodeType.text, text: raw.indexOf('&') === -1 ? raw : decodeText(raw) })\n }\n\n /**\n * Reads one quoted value, starting at `position`, and leaves `position` after the closing quote.\n */\n const readQuotedValue = (): string => {\n const quote = source.charCodeAt(position)\n if (quote !== DOUBLE_QUOTE && quote !== SINGLE_QUOTE) {\n // XML requires a quoted value.\n valid = false\n return ''\n }\n\n position += 1\n const end = source.indexOf(quote === DOUBLE_QUOTE ? '\"' : \"'\", position)\n if (end === -1) {\n valid = false\n return ''\n }\n\n const raw = source.slice(position, end)\n position = end + 1\n return raw.indexOf('&') === -1 ? raw : decodeText(raw)\n }\n\n /**\n * Reads a start tag with its attributes, and opens the element unless it closes itself.\n */\n const readStartTag = () => {\n let cursor = position + 1\n const nameStart = cursor\n\n while (cursor < length) {\n const code = source.charCodeAt(cursor)\n if (code <= 32 || code === GREATER_THAN || code === SLASH || code === EQUALS) {\n break\n }\n cursor += 1\n }\n\n if (cursor === nameStart) {\n valid = false\n return\n }\n\n const qualifiedName = source.slice(nameStart, cursor)\n const attributes: XmlAttribute[] = []\n const element: XmlElement = {\n nodeType: XmlNodeType.element,\n qualifiedName,\n namespacePrefix: '',\n localName: qualifiedName,\n attributes,\n children: [],\n }\n applyXmlNameParts(qualifiedName, element)\n\n let closed = false\n let selfClosing = false\n\n while (cursor < length) {\n let code = source.charCodeAt(cursor)\n while (code <= 32) {\n cursor += 1\n if (cursor >= length) {\n break\n }\n code = source.charCodeAt(cursor)\n }\n if (cursor >= length) {\n break\n }\n\n if (code === GREATER_THAN) {\n cursor += 1\n closed = true\n break\n }\n if (code === SLASH) {\n cursor += 1\n if (cursor < length && source.charCodeAt(cursor) === GREATER_THAN) {\n cursor += 1\n closed = true\n selfClosing = true\n break\n }\n valid = false\n return\n }\n\n const attributeStart = cursor\n do {\n cursor += 1\n if (cursor >= length) {\n break\n }\n code = source.charCodeAt(cursor)\n } while (code > 32 && code !== GREATER_THAN && code !== SLASH && code !== EQUALS)\n if (cursor === attributeStart) {\n valid = false\n return\n }\n\n const attributeName = source.slice(attributeStart, cursor)\n\n while (cursor < length && source.charCodeAt(cursor) <= 32) {\n cursor += 1\n }\n if (cursor >= length || source.charCodeAt(cursor) !== EQUALS) {\n // XML requires every attribute to carry a quoted value.\n valid = false\n return\n }\n cursor += 1\n while (cursor < length && source.charCodeAt(cursor) <= 32) {\n cursor += 1\n }\n\n position = cursor\n const value = readQuotedValue()\n if (!valid) {\n return\n }\n cursor = position\n\n for (let index = 0; index < attributes.length; index += 1) {\n if (attributes[index]?.qualifiedName === attributeName) {\n // A repeated attribute name is not well formed.\n valid = false\n return\n }\n }\n\n attributes.push(createXmlAttribute(attributeName, value))\n }\n\n if (!closed) {\n valid = false\n return\n }\n\n position = cursor\n\n const parent = stack[stack.length - 1]\n if (parent === undefined) {\n valid = false\n return\n }\n parent.children.push(element)\n\n // A self-closing tag is complete as written.\n if (!selfClosing) {\n stack.push(element)\n }\n }\n\n /**\n * Closes the innermost element, which has to be the one the tag names.\n */\n const readEndTag = () => {\n let cursor = position + 2\n const nameStart = cursor\n\n while (cursor < length) {\n const code = source.charCodeAt(cursor)\n if (code <= 32 || code === GREATER_THAN) {\n break\n }\n cursor += 1\n }\n\n const name = source.slice(nameStart, cursor)\n const end = source.indexOf('>', cursor)\n if (end === -1) {\n valid = false\n return\n }\n position = end + 1\n\n // The root never closes, so a close tag here has no open element to match.\n if (stack.length <= 1) {\n valid = false\n return\n }\n\n const top = stack[stack.length - 1]\n if (top === undefined || top.qualifiedName !== name) {\n valid = false\n return\n }\n\n stack.pop()\n }\n\n /**\n * Skips a comment, CDATA section, or any other declaration such as a DOCTYPE.\n */\n const readMarkupDeclaration = () => {\n if (source.charCodeAt(position + 2) === HYPHEN && source.charCodeAt(position + 3) === HYPHEN) {\n const end = source.indexOf('-->', position + 4)\n if (end === -1) {\n valid = false\n return\n }\n position = end + 3\n return\n }\n\n if (source.startsWith('[CDATA[', position + 2)) {\n const start = position + 9\n const end = source.indexOf(']]>', start)\n if (end === -1) {\n valid = false\n return\n }\n\n const parent = stack[stack.length - 1]\n if (parent === undefined) {\n valid = false\n return\n }\n\n parent.children.push({ nodeType: XmlNodeType.cdata, text: source.slice(start, end) })\n position = end + 3\n return\n }\n\n // Any other declaration, a DOCTYPE with an internal subset included, runs to its closing bracket.\n let cursor = position + 2\n let depth = 1\n while (cursor < length && depth > 0) {\n const code = source.charCodeAt(cursor)\n if (code === LESS_THAN) {\n depth += 1\n } else if (code === GREATER_THAN) {\n depth -= 1\n }\n cursor += 1\n }\n\n if (depth > 0) {\n valid = false\n return\n }\n\n position = cursor\n }\n\n /**\n * Skips a processing instruction, which carries nothing the kit reads.\n */\n const readProcessingInstruction = () => {\n const end = source.indexOf('?>', position + 2)\n if (end === -1) {\n valid = false\n return\n }\n position = end + 2\n }\n\n /**\n * Reads content until the end of input or the end tag of the innermost element.\n */\n const readNodes = () => {\n while (position < length) {\n const open = source.indexOf('<', position)\n if (open === -1) {\n readTextNode(length)\n return\n }\n if (open > position) {\n readTextNode(open)\n if (!valid) {\n return\n }\n }\n\n position = open\n const next = source.charCodeAt(open + 1)\n\n if (next === SLASH) {\n readEndTag()\n } else if (next === EXCLAMATION) {\n readMarkupDeclaration()\n } else if (next === QUESTION) {\n readProcessingInstruction()\n } else {\n readStartTag()\n }\n\n if (!valid) {\n return\n }\n }\n }\n\n readNodes()\n\n // An unclosed element leaves more than the root on the stack.\n if (!valid || stack.length !== 1) {\n return null\n }\n\n for (let index = 0; index < root.children.length; index += 1) {\n if (root.children[index]?.nodeType === XmlNodeType.element) {\n return root\n }\n }\n\n return null\n}\n","export const FORMAT = 'lrc'\n\nexport interface LrcContent {\n original: string\n translations?: string\n romans?: string\n}\n\nexport const readLrcContent = (input: unknown): LrcContent | null => {\n if (typeof input === 'string') {\n return { original: input }\n }\n if (typeof input !== 'object' || input === null) {\n return null\n }\n\n const source = input as Partial<LrcContent>\n if (typeof source.original !== 'string') {\n return null\n }\n\n return {\n original: source.original,\n translations: typeof source.translations === 'string' ? source.translations : undefined,\n romans: typeof source.romans === 'string' ? source.romans : undefined,\n }\n}\n","import { isTime, parseTime } from '@root/utils'\n\nconst WORD_OFFSET_PATTERN = /^(\\d+)(?:,(\\d+))?$/\n\nconst WORD_CLOCK_DURATION_PATTERN = /^([^,]+),(\\d+)$/\n\nconst WORD_TAG_PATTERN = /<([^>]*)>/g\n\nexport const TAG_PATTERN = /\\[([^\\]]*)\\]/g\n\nexport const META_PATTERN = /^([A-Za-z0-9_-]+)\\s*:\\s*([\\s\\S]*)$/\n\n/**\n * Integer tags use `offset[,duration]` in milliseconds relative to the line start.\n * Clock tags are absolute and may append a duration in milliseconds, as in `<00:01.50,500>`.\n * A missing or zero duration leaves the word end unspecified.\n */\nexport const parseWordTime = (body: string, lineStart: number) => {\n if (isTime(body)) {\n const start = parseTime(body)\n\n return start === undefined ? undefined : { start, end: undefined }\n }\n\n const match = WORD_OFFSET_PATTERN.exec(body.trim())\n if (match) {\n const start = lineStart + Number(match[1])\n const duration = Number(match[2] ?? 0)\n\n return { start, end: duration > 0 ? start + duration : undefined }\n }\n\n const clock = WORD_CLOCK_DURATION_PATTERN.exec(body.trim())\n if (clock && isTime(clock[1] ?? '')) {\n const start = parseTime(clock[1] ?? '')\n const duration = Number(clock[2] ?? 0)\n\n return start === undefined ? undefined : { start, end: duration > 0 ? start + duration : undefined }\n }\n\n return undefined\n}\n\n/**\n * Tags precede their text, as in `<0,500>hel`.\n */\nexport interface WordPiece {\n text: string\n tag?: string\n}\n\n/**\n * Untagged leading text is preserved; tags without following text produce no piece.\n * A trailing tag still yields an empty piece so it can close the preceding word.\n */\nexport const splitWordTags = (content: string) => {\n const pieces: WordPiece[] = []\n let tag: string | undefined\n let cursor = 0\n\n for (const match of content.matchAll(WORD_TAG_PATTERN)) {\n const at = match.index\n const before = content.slice(cursor, at)\n\n if (before) {\n pieces.push({ text: before, tag })\n }\n\n tag = match[1] ?? ''\n cursor = at + match[0].length\n }\n\n const tail = content.slice(cursor)\n if (tail) {\n pieces.push({ text: tail, tag })\n } else if (tag !== undefined) {\n pieces.push({ text: '', tag })\n }\n\n return pieces\n}\n","import { isTime } from '@root/utils'\n\nimport { readLrcContent } from './common'\nimport { TAG_PATTERN } from './utils'\n\n/**\n * A single readable time tag is enough for recognition; metadata alone is not.\n * This is a format probe, not a validity check, so an input of another shape is simply not LRC.\n */\nexport const detectLrc = (input: unknown) => {\n const content = readLrcContent(input)\n if (!content) {\n return false\n }\n\n const lines = content.original.replace(/\\r\\n?/g, '\\n').split('\\n')\n return lines.some((line) => [...line.matchAll(TAG_PATTERN)].some((match) => isTime(match[1] ?? '')))\n}\n","import type { runtime } from '@gondola-lyrics/model'\nimport type { LrcContent } from './common'\n\nimport { LineType, WordType } from '@gondola-lyrics/model/runtime'\n\nimport { formatTime } from '@root/utils'\n\nexport type LrcGenerateResult = LrcContent\n\nexport interface LrcGenerateOptions {\n /**\n * Word-level timestamp style; defaults to `angle`.\n *\n * `angle` is the enhanced LRC form and round-trips through this parser.\n *\n * `bracket` matches line timestamps, so this parser reads it as new lines rather than words.\n */\n wordTiming?: 'angle' | 'bracket' | 'none'\n /** Clock format for every timestamp; defaults to `mm:ss.SSS`. */\n timeFormat?: string\n /** Emit the metadata header; defaults to `true`. */\n includeMeta?: boolean\n /** Append the word duration to its tag, as in `<00:01.00,500>`. */\n includeWordEnd?: boolean\n /** Emit instrumental lines as a timestamp with no text; defaults to `true`. */\n includeInstrumental?: boolean\n}\n\ntype LrcGenerateOptionsRequired = Required<LrcGenerateOptions>\n\nconst resolveOptions = (options: LrcGenerateOptions = {}): LrcGenerateOptionsRequired => {\n return {\n wordTiming: options.wordTiming ?? 'angle',\n timeFormat: options.timeFormat ?? 'mm:ss.SSS',\n includeMeta: options.includeMeta ?? true,\n includeWordEnd: options.includeWordEnd ?? false,\n includeInstrumental: options.includeInstrumental ?? true,\n }\n}\n\nconst renderTimeTag = (time: number, options: LrcGenerateOptionsRequired) => {\n return `[${formatTime(time, options.timeFormat)}]`\n}\n\nconst renderWordTag = (start: number, end: number | undefined, options: LrcGenerateOptionsRequired) => {\n if (options.wordTiming === 'none') {\n return ''\n }\n\n const open = options.wordTiming === 'bracket' ? '[' : '<'\n const close = options.wordTiming === 'bracket' ? ']' : '>'\n // A duration of zero still states where the word stops, so it is written when the end is known.\n const duration = options.includeWordEnd && end !== undefined ? `,${Math.max(0, end - start)}` : ''\n\n return `${open}${formatTime(start, options.timeFormat)}${duration}${close}`\n}\n\n/**\n * Plain text joins words as written, including the separators stored as space words.\n */\nconst renderPlainText = (line: runtime.Line) => {\n const content = line.content\n if (!content) {\n return ''\n }\n if (content.text !== undefined) {\n return content.text\n }\n\n return content.words.map((word) => word.text).join('')\n}\n\n/**\n * Word-level lines interleave tags with their text; untimed words stay untagged.\n */\nconst renderSyllableText = (line: runtime.Line, options: LrcGenerateOptionsRequired) => {\n const content = line.content\n if (!content?.words.length) {\n return renderPlainText(line)\n }\n\n return content.words\n .map((word) => {\n if (word.type === WordType.SPACE) {\n return word.text\n }\n const start = word.time?.start\n if (start === undefined) {\n return word.text\n }\n return `${renderWordTag(start, word.time?.end, options)}${word.text}`\n })\n .join('')\n}\n\nconst renderMetaRows = (meta: runtime.Meta, options: LrcGenerateOptionsRequired) => {\n const rows: string[] = []\n\n for (const title of meta.titles) {\n rows.push(`[ti:${title.text}]`)\n }\n for (const artist of meta.artists) {\n rows.push(`[ar:${artist.text}]`)\n }\n for (const album of meta.albums) {\n rows.push(`[al:${album.text}]`)\n }\n for (const author of meta.authors) {\n rows.push(`[by:${author.text}]`)\n }\n if (meta.offset) {\n rows.push(`[offset:${meta.offset}]`)\n }\n if (meta.duration !== undefined) {\n rows.push(`[length:${formatTime(meta.duration, options.timeFormat)}]`)\n }\n for (const isrc of meta.isrcs) {\n rows.push(`[isrc:${isrc}]`)\n }\n for (const unknown of meta.unknowns) {\n rows.push(`[${unknown.key}:${unknown.value}]`)\n }\n\n return rows\n}\n\n/**\n * Returns `undefined` for a line the options leave out, so callers can drop it rather than emit a blank row.\n */\nconst renderLineRow = (line: runtime.Line, options: LrcGenerateOptionsRequired, wordLevel: boolean) => {\n if (line.type === LineType.INSTRUMENTAL && !options.includeInstrumental) {\n return undefined\n }\n\n // An untimed lyric still needs a stamp, so it starts at zero like any other line.\n const time = renderTimeTag(line.time?.start ?? 0, options)\n\n // An instrumental line carries no content, so its timestamp alone marks the gap.\n if (line.type === LineType.INSTRUMENTAL) {\n return time\n }\n\n const text = wordLevel && line.content?.words.length ? renderSyllableText(line, options) : renderPlainText(line)\n return `${time}${text}`\n}\n\nconst renderAnnotationRows = (lines: runtime.Line[], options: LrcGenerateOptionsRequired, pick: (line: runtime.Line) => { text: string }[]) => {\n const rows: string[] = []\n\n for (const line of lines) {\n const start = line.time?.start\n if (start === undefined) {\n continue\n }\n for (const item of pick(line)) {\n rows.push(`${renderTimeTag(start, options)}${item.text}`)\n }\n }\n\n return rows\n}\n\nconst joinBlocks = (header: string[], body: string[]) => {\n const blocks: string[] = []\n if (header.length) {\n blocks.push(header.join('\\n'))\n }\n if (body.length) {\n blocks.push(body.join('\\n'))\n }\n\n return blocks.join('\\n\\n')\n}\n\n/**\n * Generates the LRC text plus the annotations a single file cannot hold.\n *\n * Word tags appear only for a word-timed lyric when `wordTiming` is not `none`; otherwise every line stays whole.\n */\nexport const generateLrc = (lyric: runtime.Lyric, options: LrcGenerateOptions = {}): LrcGenerateResult => {\n const resolved = resolveOptions(options)\n const header = resolved.includeMeta && lyric.meta ? renderMetaRows(lyric.meta, resolved) : []\n\n const hasWords = lyric.lines.some((line) => (line.content?.words.length ?? 0) > 0)\n const wordLevel = hasWords && resolved.wordTiming !== 'none'\n\n const body = lyric.lines\n .map((line) => {\n return renderLineRow(line, resolved, wordLevel)\n })\n .filter((row) => row !== undefined)\n\n const result: LrcGenerateResult = {\n original: joinBlocks(header, body),\n }\n\n const translations = renderAnnotationRows(lyric.lines, resolved, (line) => line.annotation?.translations ?? [])\n if (translations.length) {\n result.translations = joinBlocks(header, translations)\n }\n\n const romans = renderAnnotationRows(lyric.lines, resolved, (line) => line.annotation?.romans ?? [])\n if (romans.length) {\n result.romans = joinBlocks(header, romans)\n }\n\n return result\n}\n","import type { runtime } from '@gondola-lyrics/model'\nimport type { Diagnostic } from '@gondola-lyrics/model/runtime'\n\nimport { canonicalizeLyric, validateLyric } from '@gondola-lyrics/model/runtime'\n\n/**\n * A parsed lyric in canonical form, with every rule it breaks.\n */\nexport interface ParseResult {\n lyric: runtime.Lyric\n diagnostics: Diagnostic[]\n}\n\n/**\n * Canonicalizes and validates one parser's output, so every format hands back the same shape.\n *\n * Canonicalization runs first because validation reports paths against the canonical tree.\n */\nexport const finalizeLyric = (input: runtime.Lyric): ParseResult => {\n const lyric = canonicalizeLyric(input)\n const diagnostics = validateLyric(lyric)\n\n return { lyric, diagnostics }\n}\n","import type { runtime } from '@gondola-lyrics/model'\nimport type { ParseResult } from '../result'\nimport type { LrcContent } from './common'\n\nimport {\n Timing,\n LyricStatus,\n makeLineContentText,\n makeLineContentWords,\n makeLineNormal,\n makeLyric,\n makeTimeRange,\n makeMeta,\n makeText,\n makeUnknownEntry,\n makeWordNormal,\n makeWordSpace,\n deriveLanguageUsages,\n} from '@gondola-lyrics/model/runtime'\n\nimport { attachLineAnnotations, isTime, parseTime } from '@root/utils'\n\nimport { finalizeLyric } from '../result'\nimport { FORMAT, readLrcContent } from './common'\nimport { META_PATTERN, TAG_PATTERN, parseWordTime, splitWordTags } from './utils'\n\nexport interface LrcParseOptions {\n /**\n * How far an annotation's timestamp may sit from a line's start, in milliseconds.\n * Defaults to `0`, which requires an exact match.\n */\n annotationTolerance?: number\n}\n\nexport type LrcParseInput = LrcContent\n\ninterface TimedLine {\n text: string\n start: number\n}\n\ninterface MetaTag {\n key: string\n value: string\n}\n\nconst isBareNumber = (value: string) => {\n return /^\\d+$/.test(value.trim())\n}\n\n/**\n * A trailing tag alone still makes the line word-level, so it can close the last word.\n * Unreadable angle-bracket text stays plain lyric content.\n */\nconst isWordTimed = (text: string) => {\n return splitWordTags(text).some((piece) => piece.tag !== undefined && parseWordTime(piece.tag, 0) !== undefined)\n}\n\nconst readMeta = (body: string) => {\n const match = META_PATTERN.exec(body)\n if (!match) {\n return undefined\n }\n\n return { key: (match[1] ?? '').toLowerCase(), value: (match[2] ?? '').trim() }\n}\n\nconst readGroup = (tags: string[], text: string, timed: TimedLine[], meta: MetaTag[]) => {\n for (const tag of tags) {\n const start = isTime(tag) ? parseTime(tag) : undefined\n if (start !== undefined) {\n timed.push({ text, start })\n continue\n }\n\n const pair = readMeta(tag)\n if (pair) {\n meta.push(pair)\n }\n }\n}\n\n/**\n * Stacked timestamps share the following text, as in `[00:10][00:20]twice`.\n * Text before the first tag is discarded because it has no timing association.\n */\nconst readTimedLines = (text: string) => {\n const timed: TimedLine[] = []\n const meta: MetaTag[] = []\n\n for (const raw of text.replace(/\\r\\n?/g, '\\n').split('\\n')) {\n const line = raw.trim()\n if (!line) {\n continue\n }\n\n const matches = [...line.matchAll(TAG_PATTERN)]\n let group: string[] = []\n\n for (const [index, match] of matches.entries()) {\n const at = match.index + match[0].length\n const next = matches[index + 1]?.index ?? line.length\n const shared = line.slice(at, next)\n\n group.push(match[1] ?? '')\n\n if (!shared.trim()) {\n continue\n }\n\n readGroup(group, shared, timed, meta)\n group = []\n }\n\n // Trailing timestamps still represent empty timed lines.\n readGroup(group, '', timed, meta)\n }\n\n return { timed, meta }\n}\n\n/**\n * Unmapped tags and unparseable durations remain in `unknowns` to avoid discarding metadata.\n * Keys are lowercased and values trimmed, so original tag spelling is not preserved.\n */\nconst readMetaTags = (tags: MetaTag[]) => {\n const meta = makeMeta()\n\n for (const tag of tags) {\n switch (tag.key) {\n case 'length':\n case 'duration': {\n // Duration tags accept both clock notation and integer milliseconds.\n const duration = parseTime(tag.value) ?? (isBareNumber(tag.value) ? Number(tag.value) : Number.NaN)\n if (Number.isFinite(duration)) {\n meta.duration = Math.trunc(duration)\n } else {\n meta.unknowns.push(makeUnknownEntry({ key: tag.key, value: tag.value }))\n }\n break\n }\n case 'offset':\n meta.offset = Math.trunc(Number(tag.value)) || 0\n break\n case 'ti':\n case 'title':\n meta.titles.push(makeText({ text: tag.value }))\n break\n case 'ar':\n case 'artist':\n meta.artists.push(makeText({ text: tag.value }))\n break\n case 'al':\n case 'album':\n meta.albums.push(makeText({ text: tag.value }))\n break\n case 'by':\n case 'author':\n meta.authors.push(makeText({ text: tag.value }))\n break\n case 'isrc':\n meta.isrcs.push(tag.value)\n break\n default:\n meta.unknowns.push(makeUnknownEntry({ key: tag.key, value: tag.value }))\n break\n }\n }\n\n return meta\n}\n\n/**\n * Whitespace becomes a space word preserving the source run, as in `lo World`.\n * A tag opens a segment; untagged words continue from the previous end so the timeline has no holes.\n * Explicit word ends are kept as written, so duration tags survive unchanged.\n */\nconst readWords = (text: string, lineStart: number) => {\n const drafts: { text: string; start?: number; end?: number; space?: boolean }[] = []\n const open = new Set<{ start?: number; end?: number }>()\n\n const closeSegment = (boundary: number | undefined) => {\n if (boundary === undefined) {\n return\n }\n for (const draft of open) {\n if (draft.start !== undefined && draft.end === undefined && boundary >= draft.start) {\n draft.end = boundary\n }\n }\n open.clear()\n }\n\n let segmentStart: number | undefined = lineStart\n let cursor: number | undefined = lineStart\n\n for (const piece of splitWordTags(text)) {\n const time = piece.tag === undefined ? undefined : parseWordTime(piece.tag, lineStart)\n\n // A tag, even without following text, closes the segment it opens.\n if (time?.start !== undefined) {\n closeSegment(time.start)\n segmentStart = time.start\n cursor = time.start\n }\n\n if (!piece.text) {\n continue\n }\n\n let applied = false\n\n for (const run of piece.text.split(/(\\s+)/)) {\n if (!run) {\n continue\n }\n\n // A separator keeps its own text and never carries a tag.\n if (!run.trim()) {\n if (drafts.length) {\n drafts.push({ text: run, space: true })\n }\n continue\n }\n\n const tagged = !applied && time?.start !== undefined\n applied = true\n\n const start = tagged ? segmentStart : cursor\n const end = tagged ? time?.end : undefined\n const draft = { text: run, start, end }\n\n drafts.push(draft)\n if (end !== undefined) {\n cursor = end\n } else {\n open.add(draft)\n }\n }\n }\n\n return { drafts, closeSegment }\n}\n\n/**\n * A line ends at the next line's start; without one, it ends at its final word's end.\n * An open word is closed by the next timed word, or by the line end, so every sung word stays inside the line.\n */\nconst readLine = (text: string, start: number, nextStart: number | undefined) => {\n const wordTimed = isWordTimed(text)\n const { drafts, closeSegment } = wordTimed ? readWords(text, start) : { drafts: [], closeSegment: () => {} }\n\n let end = nextStart !== undefined && nextStart >= start ? nextStart : undefined\n\n // Without a later line, a final word carrying its own end bounds the line; otherwise it stays open.\n if (end === undefined) {\n const last = [...drafts].reverse().find((draft) => !draft.space && draft.start !== undefined)\n end = last?.end\n }\n\n // Earlier segments were closed by their next tag, so this only closes the final one.\n closeSegment(end)\n\n const words = drafts.map((draft) => {\n if (draft.space) {\n return makeWordSpace({ text: draft.text })\n }\n // A missing start omits the range instead of inventing a word timestamp.\n const time = draft.start === undefined ? undefined : makeTimeRange({ start: draft.start, end: draft.end })\n return makeWordNormal({ text: draft.text, time })\n })\n\n const time = makeTimeRange({ start, end })\n const content = wordTimed ? makeLineContentWords(words) : makeLineContentText(text.trim())\n\n return makeLineNormal({ time, content })\n}\n\n/**\n * Builds one line per timed row, so an annotation track reuses the original text's reading unchanged.\n * An empty timed row still yields a line, since it marks a gap rather than a parse failure.\n */\nconst readLines = (timed: TimedLine[]): runtime.Line[] => {\n const lines: runtime.Line[] = []\n\n for (const [index, item] of timed.entries()) {\n const next = timed[index + 1]?.start\n // LRC has no line ends; only a nondecreasing next timestamp can supply one.\n lines.push(readLine(item.text, item.start, next))\n }\n\n return lines\n}\n\nconst parseLrcText = (text: string): runtime.Lyric => {\n const { timed, meta } = readTimedLines(text)\n const metaTags = readMetaTags(meta)\n\n if (!timed.length) {\n return makeLyric({ format: FORMAT, status: LyricStatus.INVALID, meta: metaTags })\n }\n\n const lines = readLines(timed)\n const wordTimed = lines.some((line) => (line.content?.words.length ?? 0) > 0)\n\n return makeLyric({\n format: FORMAT,\n status: LyricStatus.VALID,\n timing: wordTimed ? Timing.WORD : Timing.LINE,\n meta: metaTags,\n languages: deriveLanguageUsages(lines),\n lines,\n })\n}\n\n/**\n * Source order, empty timed lines, and missing word times are preserved for model validation.\n *\n * Annotation tracks are read as lines too and attached by timestamp, and rows that match no line are dropped.\n *\n * `INVALID` means no readable timed lines; the returned diagnostics carry whatever the canonical tree breaks.\n */\nexport const parseLrc = (input: string | LrcParseInput, options: LrcParseOptions = {}): ParseResult => {\n const source = readLrcContent(input) ?? { original: '' }\n const lyric = parseLrcText(source.original)\n\n if (lyric.status === LyricStatus.INVALID) {\n return finalizeLyric(lyric)\n }\n\n const translations = source.translations === undefined ? [] : readLines(readTimedLines(source.translations).timed)\n const romans = source.romans === undefined ? [] : readLines(readTimedLines(source.romans).timed)\n if (translations.length === 0 && romans.length === 0) {\n return finalizeLyric(lyric)\n }\n\n const lines = attachLineAnnotations(lyric.lines, { translations, romans }, { tolerance: options.annotationTolerance })\n\n return finalizeLyric({ ...lyric, lines })\n}\n","export interface TtmlContent {\n original: string\n}\n\nexport type TtmlProfile = 'amll' | 'itunes'\n\nexport const FORMAT = 'ttml'\n\nexport const AMLL_HINT_PATTERN = /xmlns:amll|amll:meta/iu\n\nexport const ITUNES_HINT_PATTERN = /xmlns:itunes|iTunesMetadata|itunes:timing=/iu\n\nexport const readTtmlContent = (input: unknown): TtmlContent | null => {\n if (typeof input === 'string') {\n return { original: input }\n }\n if (!input || typeof input !== 'object') {\n return null\n }\n\n const source = input as Partial<TtmlContent>\n if (typeof source.original !== 'string') {\n return null\n }\n\n return { original: source.original }\n}\n\nexport const readTtmlProfile = (source: string): TtmlProfile => {\n return AMLL_HINT_PATTERN.test(source) ? 'amll' : 'itunes'\n}\n","import { parseXml, XmlNodeType } from '@root/utils'\n\nimport { AMLL_HINT_PATTERN, ITUNES_HINT_PATTERN, readTtmlContent } from './common'\n\n/**\n * Recognition needs a profile hint, since plain TTML says nothing about which profile wrote it.\n *\n * A bare `tt` root also counts, which is what a hand-written document has.\n * This is a format probe, not a validity check, so an input of another shape is simply not TTML.\n */\nexport const detectTtml = (input: unknown) => {\n const content = readTtmlContent(input)\n if (!content) {\n return false\n }\n\n if (AMLL_HINT_PATTERN.test(content.original) || ITUNES_HINT_PATTERN.test(content.original)) {\n return true\n }\n\n // A document without a hint still counts when its root is a tt element.\n const root = parseXml(content.original)\n if (!root) {\n return false\n }\n\n for (const child of root.children) {\n if (child.nodeType === XmlNodeType.element && child.localName === 'tt') {\n return true\n }\n }\n\n return false\n}\n","export const ROLE_BACKGROUND = 'x-bg'\nexport const ROLE_TRANSLATION = 'x-translation'\nexport const ROLE_ROMAN = 'x-roman'\n\nexport const RUBY_CONTAINER = 'container'\nexport const RUBY_BASE = 'base'\nexport const RUBY_TEXT_CONTAINER = 'textContainer'\nexport const RUBY_TEXT = 'text'\n\nexport const TIMING_WORD = 'Word'\nexport const TIMING_LINE = 'Line'\n","import type { runtime } from '@gondola-lyrics/model'\nimport type { XmlElement } from '@root/utils'\nimport type { ParseResult } from '../result'\nimport type { TtmlContent } from './common'\n\nimport {\n AgentType,\n CreditRole,\n LyricStatus,\n Timing,\n deriveLanguageUsages,\n makeAgent,\n makeLineAnnotation,\n makeLineAnnotationRoman,\n makeLineAnnotationTranslation,\n makeLineBackground,\n makeLineContentText,\n makeLineContentWords,\n makeLineNormal,\n makeLyric,\n makeMeta,\n makeMetaCredit,\n makeText,\n makeTimeRange,\n makeUnknownEntry,\n makeWordAnnotation,\n makeWordAnnotationRuby,\n makeWordAnnotationToken,\n makeWordNormal,\n makeWordSpace,\n} from '@gondola-lyrics/model/runtime'\n\nimport {\n XmlNodeType,\n findXmlElement,\n findXmlElements,\n getXmlAttribute,\n getXmlChildElements,\n getXmlQualifiedAttribute,\n getXmlTextContent,\n hasXmlChildElement,\n lowerTag,\n parseTime,\n parseXml,\n} from '@root/utils'\n\nimport { finalizeLyric } from '../result'\nimport { FORMAT, readTtmlContent, readTtmlProfile } from './common'\nimport {\n ROLE_BACKGROUND,\n ROLE_ROMAN,\n ROLE_TRANSLATION,\n RUBY_BASE,\n RUBY_CONTAINER,\n RUBY_TEXT,\n RUBY_TEXT_CONTAINER,\n TIMING_LINE,\n TIMING_WORD,\n} from './utils'\n\nexport interface TtmlParseOptions {\n /** Language tag the lyric declares; the document's own `xml:lang` wins when it states one. */\n language?: string\n}\n\ninterface TokenDraft {\n start: number\n end: number\n text: string\n}\n\ninterface RubyDraft {\n start: number\n end: number\n tokens: TokenDraft[]\n}\n\ninterface WordDraft {\n text: string\n space: boolean\n start?: number\n end?: number\n rubies: RubyDraft[]\n}\n\ninterface AnnotationDraft {\n language: string | undefined\n text: string\n}\n\ninterface LineDraft {\n start: number\n end: number | undefined\n agentId: string | undefined\n key: string | undefined\n words: WordDraft[]\n translations: AnnotationDraft[]\n romans: AnnotationDraft[]\n backgrounds: LineDraft[]\n wordLevel: boolean\n}\n\n/**\n * Reads the span time range, which a word needs both ends of to be sung.\n */\nconst readSpanTime = (element: XmlElement) => {\n const begin = getXmlAttribute(element, 'begin')\n const end = getXmlAttribute(element, 'end')\n if (!begin || !end) {\n return undefined\n }\n\n const start = parseTime(begin)\n const finish = parseTime(end)\n if (start === undefined || finish === undefined) {\n return undefined\n }\n\n return { start, end: finish }\n}\n\n/**\n * Reads the line time range, which may leave its end open for the next line to supply.\n */\nconst readLineTime = (element: XmlElement) => {\n const begin = getXmlAttribute(element, 'begin')\n if (!begin) {\n return undefined\n }\n\n const start = parseTime(begin)\n if (start === undefined) {\n return undefined\n }\n\n const end = getXmlAttribute(element, 'end')\n\n return { start, end: end ? parseTime(end) : undefined }\n}\n\n/**\n * Turns a run of text into drafts, keeping the separators as space words.\n *\n * A span's own trailing space is part of the word it follows, so leading and trailing runs both survive.\n */\nconst splitWords = (text: string): WordDraft[] => {\n const words: WordDraft[] = []\n\n for (const run of text.split(/(\\s+)/)) {\n if (run !== '') {\n words.push({ text: run, space: !run.trim(), rubies: [] })\n }\n }\n\n return words\n}\n\n/**\n * Reads an AMLL ruby container into one word plus its per-syllable annotation.\n */\nconst readRuby = (container: XmlElement): WordDraft | undefined => {\n let base = ''\n const tokens: TokenDraft[] = []\n\n for (const child of getXmlChildElements(container, 'span')) {\n const kind = getXmlAttribute(child, 'ruby')\n if (kind === RUBY_BASE) {\n base = getXmlTextContent(child).trim()\n continue\n }\n if (kind !== RUBY_TEXT_CONTAINER) {\n continue\n }\n\n for (const text of getXmlChildElements(child, 'span')) {\n if (getXmlAttribute(text, 'ruby') !== RUBY_TEXT) {\n continue\n }\n const time = readSpanTime(text)\n const content = getXmlTextContent(text).trim()\n if (time === undefined || !content) {\n continue\n }\n tokens.push({ start: time.start, end: time.end, text: content })\n }\n }\n\n const first = tokens[0]\n const last = tokens[tokens.length - 1]\n if (!base || first === undefined || last === undefined) {\n return undefined\n }\n\n return {\n text: base,\n space: false,\n start: first.start,\n end: last.end,\n rubies: [{ start: first.start, end: last.end, tokens }],\n }\n}\n\n/**\n * Reports whether a text run holds nothing but formatting whitespace, which only a pretty-printed source produces.\n */\nconst isFormattingWhitespace = (text: string) => {\n return text.trim() === '' && text.includes('\\n')\n}\n\n/**\n * Collects the words of one span-bearing element.\n *\n * Text between spans carries the separators, and a span without both ends contributes its text untimed.\n * A span carrying a role is annotation rather than a word, so it is skipped here.\n * Indentation between tags is dropped, since the words themselves keep their own trailing spaces.\n */\nconst readWords = (element: XmlElement): { words: WordDraft[]; wordLevel: boolean } => {\n const words: WordDraft[] = []\n let wordLevel = false\n\n for (const child of element.children) {\n if (child.nodeType === XmlNodeType.text) {\n if (!isFormattingWhitespace(child.text)) {\n words.push(...splitWords(child.text))\n }\n continue\n }\n if (child.nodeType !== XmlNodeType.element || child.localName !== 'span') {\n continue\n }\n\n const role = getXmlAttribute(child, 'role')\n if (role !== undefined) {\n continue\n }\n\n if (getXmlAttribute(child, 'ruby') === RUBY_CONTAINER) {\n const ruby = readRuby(child)\n if (ruby !== undefined) {\n words.push(ruby)\n wordLevel = true\n }\n continue\n }\n\n const text = getXmlTextContent(child).trim()\n if (!text) {\n continue\n }\n\n const time = readSpanTime(child)\n if (time === undefined) {\n words.push({ text, space: false, rubies: [] })\n continue\n }\n\n wordLevel = true\n words.push({ text, space: false, start: time.start, end: time.end, rubies: [] })\n }\n\n return { words, wordLevel }\n}\n\n/**\n * Reads one paragraph, pulling its inline annotations and background lines out of the word stream.\n */\nconst readLine = (element: XmlElement): LineDraft | undefined => {\n const time = readLineTime(element)\n if (time === undefined) {\n return undefined\n }\n\n const draft: LineDraft = {\n start: time.start,\n end: time.end,\n agentId: getXmlAttribute(element, 'agent'),\n key: getXmlAttribute(element, 'key'),\n words: [],\n translations: [],\n romans: [],\n backgrounds: [],\n wordLevel: false,\n }\n\n if (!hasXmlChildElement(element, 'span')) {\n const plain = getXmlTextContent(element).trim()\n if (!plain) {\n return undefined\n }\n\n draft.words = splitWords(plain)\n return draft\n }\n\n const read = readWords(element)\n draft.words = read.words\n draft.wordLevel = read.wordLevel\n\n for (const span of getXmlChildElements(element, 'span')) {\n const role = getXmlAttribute(span, 'role')\n if (!role) {\n continue\n }\n\n if (role === ROLE_BACKGROUND) {\n const background = readBackground(span)\n if (background) {\n draft.backgrounds.push(background)\n }\n continue\n }\n\n const text = getXmlTextContent(span).trim()\n if (!text) {\n continue\n }\n\n const item = { language: lowerTag(getXmlAttribute(span, 'lang')), text }\n if (role === ROLE_TRANSLATION) {\n draft.translations.push(item)\n } else if (role === ROLE_ROMAN) {\n draft.romans.push(item)\n }\n }\n\n return draft\n}\n\n/**\n * Reads a background line, whose end may stay open because its parent's end covers it.\n *\n * A background line states no time of its own in AMLL files, so its range comes from its words.\n */\nconst readBackground = (element: XmlElement): LineDraft | undefined => {\n const read = readWords(element)\n if (read.words.length === 0) {\n return undefined\n }\n\n const begin = getXmlAttribute(element, 'begin')\n const end = getXmlAttribute(element, 'end')\n const first = read.words.find((word) => {\n return !word.space && word.start !== undefined\n })\n const last = read.words.findLast((word) => {\n return !word.space && word.start !== undefined\n })\n\n const start = begin === undefined ? (first?.start ?? 0) : parseTime(begin)\n if (start === undefined) {\n return undefined\n }\n\n const finish = end === undefined ? last?.end : parseTime(end)\n\n return {\n start,\n end: finish,\n agentId: getXmlAttribute(element, 'agent'),\n key: getXmlAttribute(element, 'key'),\n words: read.words,\n translations: [],\n romans: [],\n backgrounds: [],\n wordLevel: read.wordLevel,\n }\n}\n\n/**\n * Builds the model words of one draft line.\n */\nconst buildWords = (draft: LineDraft): runtime.Word[] => {\n const words: runtime.Word[] = []\n\n for (const word of draft.words) {\n if (word.space) {\n words.push(makeWordSpace({ text: word.text }))\n continue\n }\n\n const time = word.start === undefined ? undefined : makeTimeRange({ start: word.start, end: word.end })\n if (word.rubies.length === 0) {\n words.push(makeWordNormal({ text: word.text, time }))\n continue\n }\n\n const rubies = word.rubies.map((ruby) => {\n return makeWordAnnotationRuby({\n time: makeTimeRange({ start: ruby.start, end: ruby.end }),\n tokens: ruby.tokens.map((token) => {\n return makeWordAnnotationToken({ text: token.text, time: makeTimeRange({ start: token.start, end: token.end }) })\n }),\n })\n })\n\n words.push(makeWordNormal({ text: word.text, time, annotation: makeWordAnnotation({ rubies }) }))\n }\n\n return words\n}\n\n/**\n * Builds a line's annotation container, which the model only keeps when it holds something.\n */\nconst buildLineAnnotation = (draft: LineDraft): runtime.LineAnnotation | undefined => {\n if (draft.translations.length === 0 && draft.romans.length === 0) {\n return undefined\n }\n\n return makeLineAnnotation({\n translations: draft.translations.map((item) => {\n return makeLineAnnotationTranslation({ text: item.text, language: item.language })\n }),\n romans: draft.romans.map((item) => {\n return makeLineAnnotationRoman({ text: item.text, language: item.language })\n }),\n })\n}\n\n/**\n * Builds one background line.\n */\nconst buildBackground = (draft: LineDraft): runtime.LineBackground => {\n return makeLineBackground({\n id: draft.key,\n time: makeTimeRange({ start: draft.start, end: draft.end }),\n agents: draft.agentId === undefined ? [] : [draft.agentId],\n content: makeLineContentWords(buildWords(draft)),\n annotation: buildLineAnnotation(draft),\n })\n}\n\n/**\n * Builds one normal line, keeping plain text whole unless the source timed its words.\n */\nconst buildLine = (draft: LineDraft): runtime.Line => {\n const content = draft.wordLevel\n ? makeLineContentWords(buildWords(draft))\n : makeLineContentText(\n draft.words\n .map((word) => {\n return word.text\n })\n .join('')\n .trim(),\n )\n\n return makeLineNormal({\n id: draft.key,\n time: makeTimeRange({ start: draft.start, end: draft.end }),\n agents: draft.agentId === undefined ? [] : [draft.agentId],\n content,\n annotation: buildLineAnnotation(draft),\n backgrounds: draft.backgrounds.map(buildBackground),\n })\n}\n\n/**\n * Maps one iTunes agent type onto the model's.\n *\n * The model requires `raw` on `OTHER`, since it cannot tell whether the source stated that word or the reader gave up.\n */\nconst readAgentType = (raw: string | undefined): { type: AgentType; raw: string | undefined } => {\n switch (raw) {\n case 'person':\n return { type: AgentType.PERSON, raw: undefined }\n case 'group':\n return { type: AgentType.GROUP, raw: undefined }\n default:\n return { type: AgentType.OTHER, raw: raw ?? 'other' }\n }\n}\n\n/**\n * Reads the head's agents, which lines reference by id.\n */\nconst readAgents = (metadata: XmlElement | undefined): runtime.Agent[] => {\n if (!metadata) {\n return []\n }\n\n const agents: runtime.Agent[] = []\n for (const element of findXmlElements(metadata, 'agent')) {\n const id = getXmlQualifiedAttribute(element, 'xml:id') ?? getXmlAttribute(element, 'id')\n if (!id) {\n continue\n }\n\n const names = getXmlChildElements(element, 'name')\n .map((name) => {\n return getXmlTextContent(name).trim()\n })\n .filter((name) => {\n return name !== ''\n })\n\n const kind = readAgentType(getXmlAttribute(element, 'type'))\n agents.push(\n makeAgent({\n id,\n type: kind.type,\n raw: kind.raw,\n names: names.map((name) => {\n return makeText({ text: name })\n }),\n }),\n )\n }\n\n return agents\n}\n\n/**\n * Reads the head's songwriters as one lyricist credit, since TTML does not split the role further.\n *\n * AMLL wraps these in `iTunesMetadata`, so the whole head is searched rather than its direct children.\n */\nconst readCredits = (metadata: XmlElement | undefined, meta: runtime.Meta) => {\n if (!metadata) {\n return\n }\n\n const names = findXmlElements(metadata, 'songwriter')\n .map((element) => {\n return getXmlTextContent(element).trim()\n })\n .filter((name) => {\n return name !== ''\n })\n\n if (names.length === 0) {\n return\n }\n\n meta.credits.push(\n makeMetaCredit({\n role: CreditRole.LYRICIST,\n names: names.map((name) => {\n return makeText({ text: name })\n }),\n }),\n )\n}\n\n/**\n * Reads one `amll:meta` entry, keeping an unknown key as written so a round trip loses nothing.\n */\nconst applyAmllMeta = (meta: runtime.Meta, element: XmlElement) => {\n const key = getXmlAttribute(element, 'key')\n const value = getXmlAttribute(element, 'value')?.trim()\n if (!key || !value) {\n return\n }\n\n switch (key) {\n case 'musicName':\n meta.titles.push(makeText({ text: value }))\n return\n case 'artists':\n meta.artists.push(makeText({ text: value }))\n return\n case 'album':\n meta.albums.push(makeText({ text: value }))\n return\n case 'isrc':\n meta.isrcs.push(value)\n return\n case 'ttmlAuthorGithubLogin':\n meta.authors.push(makeText({ text: value }))\n return\n default:\n meta.unknowns.push(makeUnknownEntry({ key, value }))\n }\n}\n\n/**\n * Splits one annotation text into its main part and any background pieces it embeds.\n */\nconst splitBackgroundText = (element: XmlElement) => {\n let main = ''\n const backgrounds: { text: string; key: string | undefined }[] = []\n\n for (const child of element.children) {\n if (child.nodeType === XmlNodeType.element && child.localName === 'span' && getXmlAttribute(child, 'role') === ROLE_BACKGROUND) {\n backgrounds.push({ text: getXmlTextContent(child).trim(), key: getXmlAttribute(child, 'for') })\n continue\n }\n main += getXmlTextContent(child)\n }\n\n return { main: main.trim(), backgrounds }\n}\n\n/**\n * Attaches head translations and transliterations to the lines they name by `itunes:key`.\n *\n * A replacement translation rewrites the line's text, which is how a source swaps one script for another.\n */\nconst attachHeadAnnotations = (metadata: XmlElement | undefined, lineMap: Map<string, LineDraft>, backgroundMap: Map<string, LineDraft>) => {\n if (!metadata || lineMap.size === 0) {\n return\n }\n\n const attach = (blocks: XmlElement[], kind: 'translation' | 'roman') => {\n for (const block of blocks) {\n const language = lowerTag(getXmlAttribute(block, 'lang'))\n const type = getXmlQualifiedAttribute(block, 'type') ?? getXmlAttribute(block, 'type')\n\n for (const text of getXmlChildElements(block, 'text')) {\n const key = getXmlAttribute(text, 'for')\n const line = key ? lineMap.get(key) : undefined\n if (!line) {\n continue\n }\n\n const { main, backgrounds } = splitBackgroundText(text)\n if (kind === 'translation' && type === 'replacement') {\n if (main) {\n line.words = splitWords(main)\n line.wordLevel = false\n }\n continue\n }\n\n if (main) {\n const item = { language, text: main }\n if (kind === 'translation') {\n line.translations.push(item)\n } else {\n line.romans.push(item)\n }\n }\n\n for (const background of backgrounds) {\n if (!background.text) {\n continue\n }\n const target = background.key ? backgroundMap.get(background.key) : undefined\n if (!target) {\n continue\n }\n const item = { language, text: background.text }\n if (kind === 'translation') {\n target.translations.push(item)\n } else {\n target.romans.push(item)\n }\n }\n }\n }\n }\n\n attach(findXmlElements(metadata, 'translation'), 'translation')\n attach(findXmlElements(metadata, 'transliteration'), 'roman')\n}\n\n/**\n * Returns the paragraphs of a body, including any nested inside a div.\n */\nconst findParagraphs = (body: XmlElement): XmlElement[] => {\n const result: XmlElement[] = []\n const stack: XmlElement[] = [body]\n\n while (stack.length > 0) {\n const element = stack.pop()\n if (!element) {\n continue\n }\n if (element.localName === 'p') {\n result.push(element)\n continue\n }\n\n const children = element.children\n for (let index = children.length - 1; index >= 0; index -= 1) {\n const child = children[index]\n if (child && child.nodeType === XmlNodeType.element) {\n stack.push(child)\n }\n }\n }\n\n return result\n}\n\nconst readDeclaredTiming = (root: XmlElement): Timing | undefined => {\n switch (getXmlQualifiedAttribute(root, 'itunes:timing')) {\n case TIMING_WORD:\n return Timing.WORD\n case TIMING_LINE:\n return Timing.LINE\n default:\n return undefined\n }\n}\n\n/**\n * Reads a TTML document into the model.\n *\n * The profile only changes where metadata comes from, so one reader serves both.\n * A document that parses but holds no timed paragraph is `INVALID` rather than an error.\n */\nexport const parseTtml = (input: string | TtmlContent, options: TtmlParseOptions = {}): ParseResult => {\n const content = readTtmlContent(input)\n if (!content) {\n return finalizeLyric(makeLyric({ format: FORMAT, status: LyricStatus.INVALID, meta: makeMeta() }))\n }\n\n const extra = {\n profile: readTtmlProfile(content.original),\n }\n\n const document = parseXml(content.original)\n const root = document ? findXmlElement(document, 'tt') : undefined\n if (!root) {\n return finalizeLyric(makeLyric({ format: FORMAT, status: LyricStatus.INVALID, meta: makeMeta(), extra }))\n }\n\n const body = findXmlElement(root, 'body')\n const metadata = findXmlElement(root, 'metadata')\n const meta = makeMeta()\n\n const language = lowerTag(getXmlQualifiedAttribute(root, 'xml:lang')) ?? lowerTag(options.language)\n if (language !== undefined) {\n meta.language = language\n }\n\n readCredits(metadata, meta)\n for (const element of metadata ? findXmlElements(metadata, 'meta') : []) {\n applyAmllMeta(meta, element)\n }\n\n const drafts: LineDraft[] = []\n const lineMap = new Map<string, LineDraft>()\n const backgroundMap = new Map<string, LineDraft>()\n\n for (const element of body ? findParagraphs(body) : []) {\n const draft = readLine(element)\n if (!draft) {\n continue\n }\n\n drafts.push(draft)\n if (draft.key) {\n lineMap.set(draft.key, draft)\n }\n for (const background of draft.backgrounds) {\n if (background.key) {\n backgroundMap.set(background.key, background)\n }\n }\n }\n\n attachHeadAnnotations(metadata, lineMap, backgroundMap)\n\n if (drafts.length === 0) {\n return finalizeLyric(makeLyric({ format: FORMAT, status: LyricStatus.INVALID, meta, extra }))\n }\n\n const lines = drafts.map(buildLine)\n const wordLevel = drafts.some((draft) => {\n return draft.wordLevel\n })\n\n return finalizeLyric(\n makeLyric({\n format: FORMAT,\n status: LyricStatus.VALID,\n timing: readDeclaredTiming(root) ?? (wordLevel ? Timing.WORD : Timing.LINE),\n meta,\n extra,\n agents: readAgents(metadata),\n languages: deriveLanguageUsages(lines),\n lines,\n }),\n )\n}\n","import type { runtime } from '@gondola-lyrics/model'\nimport type { LyricFormat } from './format'\nimport type { LrcGenerateOptions, LrcGenerateResult, LrcParseInput, LrcParseOptions } from './lrc'\nimport type { ParseResult } from './result'\nimport type { TtmlContent, TtmlParseOptions } from './ttml'\n\nimport { detectLrc, generateLrc, parseLrc } from './lrc'\nimport { detectTtml, parseTtml } from './ttml'\n\n/**\n * Recognition reads only the original text; the annotation tracks never decide the format.\n *\n * Unrecognized text returns `undefined`.\n */\nexport const detectFormat = (input: unknown) => {\n if (detectLrc(input)) {\n return 'lrc'\n }\n if (detectTtml(input)) {\n return 'ttml'\n }\n\n return undefined\n}\n\n/**\n * Unrecognized text returns `null`; a recognized format returns its canonical lyric and diagnostics.\n * Annotation tracks travel with the source and are attached by the format's parser.\n */\nexport const parse = (input: unknown, options: LrcParseOptions & TtmlParseOptions = {}): ParseResult | null => {\n const format = detectFormat(input)\n if (!format) {\n return null\n }\n\n switch (format) {\n case 'lrc':\n return parseLrc(input as string | LrcParseInput, options)\n case 'ttml':\n return parseTtml(input as string | TtmlContent, options)\n }\n}\n\n/**\n * Generates text in the requested format, which need not match `lyric.format`.\n *\n * A format without a writer has no branch here, so adding one to `LyricFormat` makes this fail to compile.\n */\nexport const generate = (lyric: runtime.Lyric, target: LyricFormat, options: LrcGenerateOptions = {}): LrcGenerateResult => {\n switch (target) {\n case 'lrc':\n return generateLrc(lyric, options)\n case 'ttml':\n throw new Error('ttml generation is not implemented yet')\n }\n}\n\nexport type { ParseResult } from './result'\n\nexport type { LyricFormat } from './format'\n\nexport * from './lrc'\nexport * from './ttml'\n"],"mappings":"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"}
@@ -2,6 +2,8 @@ import { runtime } from '@gondola-lyrics/model';
2
2
 
3
3
  /**
4
4
  * Generates text in the requested format, which need not match `lyric.format`.
5
+ *
6
+ * A format without a writer has no branch here, so adding one to `LyricFormat` makes this fail to compile.
5
7
  */
6
8
  export declare const generate: (lyric: runtime.Lyric, target: LyricFormat, options?: LrcGenerateOptions) => LrcGenerateResult;
7
9
 
@@ -42,6 +44,6 @@ export declare type LrcGenerateResult = LrcContent;
42
44
  /**
43
45
  * Values match the lowercase format names stored in `Lyric.format`.
44
46
  */
45
- export declare type LyricFormat = 'lrc';
47
+ export declare type LyricFormat = 'lrc' | 'ttml';
46
48
 
47
49
  export { }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Diagnostic } from '@gondola-lyrics/model/runtime';
1
2
  import { runtime } from '@gondola-lyrics/model';
2
3
 
3
4
  /**
@@ -5,10 +6,12 @@ import { runtime } from '@gondola-lyrics/model';
5
6
  *
6
7
  * Unrecognized text returns `undefined`.
7
8
  */
8
- export declare const detectFormat: (input: unknown) => "lrc" | undefined;
9
+ export declare const detectFormat: (input: unknown) => "lrc" | "ttml" | undefined;
9
10
 
10
11
  /**
11
12
  * Generates text in the requested format, which need not match `lyric.format`.
13
+ *
14
+ * A format without a writer has no branch here, so adding one to `LyricFormat` makes this fail to compile.
12
15
  */
13
16
  export declare const generate: (lyric: runtime.Lyric, target: LyricFormat, options?: LrcGenerateOptions) => LrcGenerateResult;
14
17
 
@@ -59,21 +62,48 @@ export declare interface LrcParseOptions {
59
62
  /**
60
63
  * Values match the lowercase format names stored in `Lyric.format`.
61
64
  */
62
- export declare type LyricFormat = 'lrc';
65
+ export declare type LyricFormat = 'lrc' | 'ttml';
63
66
 
64
67
  /**
65
- * Unrecognized text returns `null`; a recognized format returns a lyric with its parser-assigned status.
68
+ * Unrecognized text returns `null`; a recognized format returns its canonical lyric and diagnostics.
66
69
  * Annotation tracks travel with the source and are attached by the format's parser.
67
70
  */
68
- export declare const parse: (input: unknown, options?: LrcParseOptions) => runtime.Lyric | null;
71
+ export declare const parse: (input: unknown, options?: LrcParseOptions & TtmlParseOptions) => ParseResult | null;
69
72
 
70
73
  /**
71
74
  * Source order, empty timed lines, and missing word times are preserved for model validation.
72
75
  *
73
76
  * Annotation tracks are read as lines too and attached by timestamp, and rows that match no line are dropped.
74
77
  *
75
- * `INVALID` means no readable timed lines; `VALID` does not imply model validation has passed.
78
+ * `INVALID` means no readable timed lines; the returned diagnostics carry whatever the canonical tree breaks.
79
+ */
80
+ export declare const parseLrc: (input: string | LrcParseInput, options?: LrcParseOptions) => ParseResult;
81
+
82
+ /**
83
+ * A parsed lyric in canonical form, with every rule it breaks.
76
84
  */
77
- export declare const parseLrc: (input: string | LrcParseInput, options?: LrcParseOptions) => runtime.Lyric;
85
+ export declare interface ParseResult {
86
+ lyric: runtime.Lyric;
87
+ diagnostics: Diagnostic[];
88
+ }
89
+
90
+ /**
91
+ * Reads a TTML document into the model.
92
+ *
93
+ * The profile only changes where metadata comes from, so one reader serves both.
94
+ * A document that parses but holds no timed paragraph is `INVALID` rather than an error.
95
+ */
96
+ export declare const parseTtml: (input: string | TtmlContent, options?: TtmlParseOptions) => ParseResult;
97
+
98
+ export declare interface TtmlContent {
99
+ original: string;
100
+ }
101
+
102
+ export declare interface TtmlParseOptions {
103
+ /** Language tag the lyric declares; the document's own `xml:lang` wins when it states one. */
104
+ language?: string;
105
+ }
106
+
107
+ export declare type TtmlProfile = 'amll' | 'itunes';
78
108
 
79
109
  export { }
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { detectFormat, generate, generateLrc, parse, parseLrc } from "./formats.js";
1
+ export { detectFormat, generate, generateLrc, parse, parseLrc, parseTtml } from "./formats.js";
2
2
  import "./parser.js";
3
3
  import "./generator.js";
package/dist/parser.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Diagnostic } from '@gondola-lyrics/model/runtime';
1
2
  import { runtime } from '@gondola-lyrics/model';
2
3
 
3
4
  /**
@@ -5,7 +6,7 @@ import { runtime } from '@gondola-lyrics/model';
5
6
  *
6
7
  * Unrecognized text returns `undefined`.
7
8
  */
8
- export declare const detectFormat: (input: unknown) => "lrc" | undefined;
9
+ export declare const detectFormat: (input: unknown) => "lrc" | "ttml" | undefined;
9
10
 
10
11
  declare interface LrcContent {
11
12
  original: string;
@@ -26,21 +27,48 @@ export declare interface LrcParseOptions {
26
27
  /**
27
28
  * Values match the lowercase format names stored in `Lyric.format`.
28
29
  */
29
- export declare type LyricFormat = 'lrc';
30
+ export declare type LyricFormat = 'lrc' | 'ttml';
30
31
 
31
32
  /**
32
- * Unrecognized text returns `null`; a recognized format returns a lyric with its parser-assigned status.
33
+ * Unrecognized text returns `null`; a recognized format returns its canonical lyric and diagnostics.
33
34
  * Annotation tracks travel with the source and are attached by the format's parser.
34
35
  */
35
- export declare const parse: (input: unknown, options?: LrcParseOptions) => runtime.Lyric | null;
36
+ export declare const parse: (input: unknown, options?: LrcParseOptions & TtmlParseOptions) => ParseResult | null;
36
37
 
37
38
  /**
38
39
  * Source order, empty timed lines, and missing word times are preserved for model validation.
39
40
  *
40
41
  * Annotation tracks are read as lines too and attached by timestamp, and rows that match no line are dropped.
41
42
  *
42
- * `INVALID` means no readable timed lines; `VALID` does not imply model validation has passed.
43
+ * `INVALID` means no readable timed lines; the returned diagnostics carry whatever the canonical tree breaks.
43
44
  */
44
- export declare const parseLrc: (input: string | LrcParseInput, options?: LrcParseOptions) => runtime.Lyric;
45
+ export declare const parseLrc: (input: string | LrcParseInput, options?: LrcParseOptions) => ParseResult;
46
+
47
+ /**
48
+ * A parsed lyric in canonical form, with every rule it breaks.
49
+ */
50
+ export declare interface ParseResult {
51
+ lyric: runtime.Lyric;
52
+ diagnostics: Diagnostic[];
53
+ }
54
+
55
+ /**
56
+ * Reads a TTML document into the model.
57
+ *
58
+ * The profile only changes where metadata comes from, so one reader serves both.
59
+ * A document that parses but holds no timed paragraph is `INVALID` rather than an error.
60
+ */
61
+ export declare const parseTtml: (input: string | TtmlContent, options?: TtmlParseOptions) => ParseResult;
62
+
63
+ export declare interface TtmlContent {
64
+ original: string;
65
+ }
66
+
67
+ export declare interface TtmlParseOptions {
68
+ /** Language tag the lyric declares; the document's own `xml:lang` wins when it states one. */
69
+ language?: string;
70
+ }
71
+
72
+ export declare type TtmlProfile = 'amll' | 'itunes';
45
73
 
46
74
  export { }
package/dist/parser.js CHANGED
@@ -1 +1 @@
1
- export { detectFormat, parse, parseLrc } from "./formats.js";
1
+ export { detectFormat, parse, parseLrc, parseTtml } from "./formats.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gondola-lyrics/kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Gondola lyrics toolkit",
5
5
  "keywords": [
6
6
  "gondola",