Files
iptvnator/tools/nx/literal-source-lines.mjs
T
4grayandClaude Opus 5.5 2dff91c9f2 fix(ui): replace icon ligatures missing from the font and guard them (#1864)
* fix(ui): replace icon ligatures missing from the font and guard them

The download and recording queues showed the literal text "file_off" for a
missing file: the bundled Material Icons font (material-design-icons-iconfont
6.7.0) has no such ligature, so it rendered as text overflowing the icon box.
Use error_outline, which the missing state's tertiary palette keeps distinct
from the filled error glyph of a failed download.

Add `pnpm run styles:icon-ligatures:validate` (CI): it parses templates with
@angular/compiler and TypeScript with the compiler API, collects static
<mat-icon> text, the string results of its bindings, icon/*Icon inputs and
icon-named TypeScript values, and fails on a name missing from the font's
codepoints file. Two listed codepoints the font has no ligature for
(rounded_corner, stairs) are excluded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tools): check quoted inline templates in the icon guard

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tools): accept every codepoint and report escaped template lines

The first measurement rendered the names without the package's CSS. With
material-design-icons.css, all 2,193 codepoint names, rounded_corner and
stairs included, draw as one glyph, so the guard accepts the whole file.

Inline templates now map each cooked position back to the source through
escapes, line continuations and CRLF, so a name after `\n` in a quoted
template is reported on the line it is written on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:31:21 +02:00

74 lines
2.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Maps a position in a string or template literal's cooked text (what
* `literal.text` returns) to its line in the source file. Escapes such as
* `\n` or `\u{…}`, line continuations and CRLF make the cooked text differ
* from the source, so counting the cooked newlines would point at the wrong
* line, possibly outside the component.
*/
export function literalLineResolver(literal, sourceFile) {
const contentStart = literal.getStart(sourceFile) + 1;
const raw = sourceFile.text.slice(contentStart, literal.getEnd() - 1);
const rawOffsets = cookedToRawOffsets(raw);
const lineAt = (position) =>
sourceFile.getLineAndCharacterOfPosition(position).line;
if (rawOffsets.length !== literal.text.length + 1) {
// An escape this mapping does not model: keep to the first line.
const startLine = lineAt(contentStart);
return () => startLine;
}
return (offset) =>
lineAt(
contentStart +
rawOffsets[Math.min(Math.max(offset, 0), rawOffsets.length - 1)]
);
}
/** Offset in `raw` of each cooked UTF-16 unit, plus the end offset. */
function cookedToRawOffsets(raw) {
const offsets = [];
let index = 0;
while (index < raw.length) {
if (raw[index] === '\r' && raw[index + 1] === '\n') {
// A template literal cooks CRLF to one LF.
offsets.push(index);
index += 2;
} else if (raw[index] !== '\\') {
offsets.push(index);
index += 1;
} else {
const { length, units } = escapeAt(raw, index);
for (let unit = 0; unit < units; unit += 1) {
offsets.push(index);
}
index += length;
}
}
offsets.push(raw.length);
return offsets;
}
/** Source length and cooked UTF-16 length of the escape at `index`. */
function escapeAt(raw, index) {
const next = raw[index + 1];
if (next === '\r') {
// Line continuation: nothing reaches the cooked text.
return { length: raw[index + 2] === '\n' ? 3 : 2, units: 0 };
}
if (next === '\n' || next === '
' || next === '
') {
return { length: 2, units: 0 };
}
if (next === 'x') {
return { length: 4, units: 1 };
}
if (next === 'u' && raw[index + 2] === '{') {
const close = raw.indexOf('}', index);
const codePoint = parseInt(raw.slice(index + 3, close), 16);
return { length: close - index + 1, units: codePoint > 0xffff ? 2 : 1 };
}
if (next === 'u') {
return { length: 6, units: 1 };
}
return { length: 2, units: 1 };
}