@knowvah/dot-engine 2.6.6 → 2.6.8
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.
- package/dist/common/htmltable-attr-warn.d.ts +10 -4
- package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
- package/dist/common/htmltable-label.d.ts +37 -0
- package/dist/common/htmltable-label.d.ts.map +1 -0
- package/dist/common/htmltable-lex-attvalue.d.ts +59 -0
- package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -0
- package/dist/common/htmltable-lex-chardata.d.ts +37 -0
- package/dist/common/htmltable-lex-chardata.d.ts.map +1 -0
- package/dist/common/htmltable-lex-expat.d.ts +26 -13
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
- package/dist/common/htmltable-lex.d.ts.map +1 -1
- package/dist/common/htmltable-parse-attrs.d.ts +3 -1
- package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
- package/dist/common/htmltable-parse.d.ts +6 -5
- package/dist/common/htmltable-parse.d.ts.map +1 -1
- package/dist/common/htmltable-pos.d.ts +3 -26
- package/dist/common/htmltable-pos.d.ts.map +1 -1
- package/dist/common/htmltable-types.d.ts +5 -1
- package/dist/common/htmltable-types.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +3 -2
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/index.js +310 -144
- package/dist/index.js.map +4 -4
- package/dist/render.js +310 -144
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/htmltable-attr-warn.ts +24 -7
- package/src/common/htmltable-label.ts +109 -0
- package/src/common/htmltable-lex-attvalue.ts +141 -0
- package/src/common/htmltable-lex-chardata.ts +95 -0
- package/src/common/htmltable-lex-expat.ts +143 -48
- package/src/common/htmltable-lex.ts +41 -36
- package/src/common/htmltable-parse-attrs.ts +10 -13
- package/src/common/htmltable-parse.ts +46 -61
- package/src/common/htmltable-pos.ts +5 -88
- package/src/common/htmltable-types.ts +6 -1
- package/src/common/htmltable.ts +16 -16
- package/src/common/make-label.ts +8 -3
|
@@ -44,12 +44,11 @@ const MSG_TEXT_BEFORE_TABLE = 'Syntax error: non-space string used before <TABLE
|
|
|
44
44
|
/** @see lib/common/htmlparse.y:298 (table rule) */
|
|
45
45
|
const MSG_TEXT_AFTER_TABLE = 'Syntax error: non-space string used after </TABLE>';
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const nonSpace = (text: string): boolean => /[^ \t\n\r]/.test(text);
|
|
47
|
+
/** Any character other than a space (the lexer already dropped control characters). @see lib/common/htmlparse.y:nonSpace */
|
|
48
|
+
const nonSpace = (text: string): boolean => /[^ ]/.test(text);
|
|
49
|
+
|
|
50
|
+
/** Text in `ctx->xb` that no token carried, which the table rule's nonSpace also reads. */
|
|
51
|
+
const heldNonSpace = (tbl: Token | undefined): boolean => tbl?.type === 'open' && nonSpace(tbl.heldText ?? '');
|
|
53
52
|
|
|
54
53
|
/**
|
|
55
54
|
* The parse failure C reports through `htmlerror` when the lexer last returned
|
|
@@ -93,42 +92,40 @@ const applyBrTag = (
|
|
|
93
92
|
consumeClose(s, 'BR');
|
|
94
93
|
};
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
fontFace: t.attrs['face'],
|
|
104
|
-
fontSize: num(t.attrs['point-size']),
|
|
105
|
-
}));
|
|
95
|
+
/**
|
|
96
|
+
* `font text n_font` and the other font-style rules: `text` is one or more
|
|
97
|
+
* text items, and the matching close tag must follow it. Anything else, an
|
|
98
|
+
* empty element included, is a syntax error at the token found instead.
|
|
99
|
+
* @see lib/common/htmlparse.y:textitem
|
|
100
|
+
*/
|
|
101
|
+
const parseStyledText = (s: ParseState, tag: string, fontStack: FontState[], items: HtmlTextItem[]): void => {
|
|
106
102
|
const inner = parseText(s, fontStack);
|
|
107
103
|
fontStack.pop();
|
|
108
|
-
items.push(...inner.items);
|
|
109
104
|
const nxt = peek(s);
|
|
110
|
-
if (nxt?.type
|
|
105
|
+
if (inner.items.length === 0 || nxt?.type !== 'close' || nxt.tag !== tag) {
|
|
106
|
+
throw parseError(s, nxt, MSG_SYNTAX_ERROR);
|
|
107
|
+
}
|
|
108
|
+
items.push(...inner.items);
|
|
109
|
+
consume(s);
|
|
111
110
|
};
|
|
112
111
|
|
|
113
|
-
|
|
112
|
+
/** `font text n_font` / `bold text n_bold` / ...: push the tag's font, parse its text, pop. */
|
|
113
|
+
const applyStyleTag = (
|
|
114
114
|
s: ParseState, t: OpenToken, fontStack: FontState[], items: HtmlTextItem[],
|
|
115
115
|
): void => {
|
|
116
116
|
consume(s);
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
fontStack.
|
|
121
|
-
|
|
122
|
-
const nxt = peek(s);
|
|
123
|
-
if (nxt?.type === 'close' && nxt.tag === t.tag) consume(s);
|
|
117
|
+
const own = t.tag === 'FONT'
|
|
118
|
+
? { fontColor: t.attrs['color'], fontFace: t.attrs['face'], fontSize: num(t.attrs['point-size']) }
|
|
119
|
+
: inlineFontState(t.tag, t.attrs);
|
|
120
|
+
fontStack.push(mergeFontState(fontStack[fontStack.length - 1] ?? {}, own));
|
|
121
|
+
parseStyledText(s, t.tag, fontStack, items);
|
|
124
122
|
};
|
|
125
123
|
|
|
126
124
|
const applyOpenTag = (
|
|
127
125
|
s: ParseState, t: OpenToken, fontStack: FontState[], items: HtmlTextItem[],
|
|
128
126
|
): boolean => {
|
|
129
127
|
if (t.tag === 'BR') { applyBrTag(s, t, fontStack, items); return true; }
|
|
130
|
-
if (t.tag === 'FONT') {
|
|
131
|
-
if (isInlineTag(t.tag)) { applyInlineTag(s, t, fontStack, items); return true; }
|
|
128
|
+
if (t.tag === 'FONT' || isInlineTag(t.tag)) { applyStyleTag(s, t, fontStack, items); return true; }
|
|
132
129
|
return false;
|
|
133
130
|
};
|
|
134
131
|
|
|
@@ -194,7 +191,7 @@ const parseFontTable = (s: ParseState, open: OpenToken, fontStack: FontState[]):
|
|
|
194
191
|
fontStack.push(wrapperFont(open, fontStack));
|
|
195
192
|
const textBefore = skipStrings(s);
|
|
196
193
|
const tblTok = peek(s);
|
|
197
|
-
if (textBefore && tblTok !== undefined) throw parseError(s, tblTok, MSG_TEXT_BEFORE_TABLE);
|
|
194
|
+
if ((textBefore || heldNonSpace(tblTok)) && tblTok !== undefined) throw parseError(s, tblTok, MSG_TEXT_BEFORE_TABLE);
|
|
198
195
|
const table = parseTable(s, fontStack);
|
|
199
196
|
const textAfter = skipStrings(s);
|
|
200
197
|
const close = peek(s);
|
|
@@ -222,13 +219,11 @@ const rejectTableAfterTag = (s: ParseState, start: number): void => {
|
|
|
222
219
|
// Cell content helpers — mirrors htmlparse.y cell rule
|
|
223
220
|
// ---------------------------------------------------------------------------
|
|
224
221
|
|
|
225
|
-
const parseImgContent = (
|
|
226
|
-
s: ParseState, t: OpenToken, cellAttrs: Record<string, string>,
|
|
227
|
-
): HtmlImage => {
|
|
222
|
+
const parseImgContent = (s: ParseState, t: OpenToken): HtmlImage => {
|
|
228
223
|
consume(s);
|
|
229
224
|
const img: HtmlImage = {
|
|
230
225
|
kind: 'image',
|
|
231
|
-
src:
|
|
226
|
+
src: t.attrs['src'] ?? '',
|
|
232
227
|
scale: t.attrs['scale'],
|
|
233
228
|
};
|
|
234
229
|
consumeClose(s, 'IMG');
|
|
@@ -249,7 +244,7 @@ const scanCellTable = (
|
|
|
249
244
|
s: ParseState, t: Token, fontStack: FontState[], st: CellScan,
|
|
250
245
|
): HtmlCellContent[] => {
|
|
251
246
|
if (st.kind === 'image' || st.kind === 'table') throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
252
|
-
if (st.textNonSpace) throw parseError(s, t, MSG_TEXT_BEFORE_TABLE);
|
|
247
|
+
if (st.textNonSpace || heldNonSpace(t)) throw parseError(s, t, MSG_TEXT_BEFORE_TABLE);
|
|
253
248
|
const tbl = parseTable(s, fontStack);
|
|
254
249
|
st.kind = 'table';
|
|
255
250
|
return [tbl];
|
|
@@ -275,17 +270,15 @@ const scanAfterTable = (s: ParseState, t: Token, st: CellScan): HtmlCellContent[
|
|
|
275
270
|
};
|
|
276
271
|
|
|
277
272
|
/** `image` as the cell's only child. */
|
|
278
|
-
const scanCellImage = (
|
|
279
|
-
s: ParseState, t: OpenToken, cellAttrs: Record<string, string>, st: CellScan,
|
|
280
|
-
): HtmlCellContent[] => {
|
|
273
|
+
const scanCellImage = (s: ParseState, t: OpenToken, st: CellScan): HtmlCellContent[] => {
|
|
281
274
|
if (st.kind !== 'none') throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
282
275
|
st.kind = 'image';
|
|
283
|
-
return [parseImgContent(s, t
|
|
276
|
+
return [parseImgContent(s, t)];
|
|
284
277
|
};
|
|
285
278
|
|
|
286
279
|
/** One step of `cell: T_cell (fonttable | fonttext | image) T_end_cell`. */
|
|
287
280
|
const scanCellItem = (
|
|
288
|
-
s: ParseState, t: Token,
|
|
281
|
+
s: ParseState, t: Token, fontStack: FontState[], st: CellScan,
|
|
289
282
|
): HtmlCellContent[] => {
|
|
290
283
|
if (st.kind === 'fonttable') throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
291
284
|
if (st.kind === 'table') return scanAfterTable(s, t, st);
|
|
@@ -295,7 +288,7 @@ const scanCellItem = (
|
|
|
295
288
|
return [parseFontTable(s, wrapper, fontStack)];
|
|
296
289
|
}
|
|
297
290
|
if (t.type === 'open' && t.tag === 'TABLE') return scanCellTable(s, t, fontStack, st);
|
|
298
|
-
if (t.type === 'open' && t.tag === 'IMG') return scanCellImage(s, t,
|
|
291
|
+
if (t.type === 'open' && t.tag === 'IMG') return scanCellImage(s, t, st);
|
|
299
292
|
if (st.kind === 'image') throw parseError(s, t, MSG_SYNTAX_ERROR);
|
|
300
293
|
return scanCellText(s, t, fontStack, st);
|
|
301
294
|
};
|
|
@@ -308,14 +301,12 @@ const endsCell = (s: ParseState, t: Token, st: CellScan): boolean => {
|
|
|
308
301
|
throw parseError(s, t, afterText ? MSG_TEXT_AFTER_TABLE : MSG_SYNTAX_ERROR);
|
|
309
302
|
};
|
|
310
303
|
|
|
311
|
-
const parseCellContent = (
|
|
312
|
-
s: ParseState, cellAttrs: Record<string, string>, fontStack: FontState[],
|
|
313
|
-
): HtmlCellContent[] => {
|
|
304
|
+
const parseCellContent = (s: ParseState, fontStack: FontState[]): HtmlCellContent[] => {
|
|
314
305
|
const content: HtmlCellContent[] = [];
|
|
315
306
|
const st: CellScan = { kind: 'none', textNonSpace: false, afterNonSpace: false };
|
|
316
307
|
for (let t = peek(s); t !== undefined; t = peek(s)) {
|
|
317
308
|
if (endsCell(s, t, st)) break;
|
|
318
|
-
content.push(...scanCellItem(s, t,
|
|
309
|
+
content.push(...scanCellItem(s, t, fontStack, st));
|
|
319
310
|
}
|
|
320
311
|
const nxt = peek(s);
|
|
321
312
|
if (nxt?.type === 'close' && nxt.tag === 'TD') consume(s);
|
|
@@ -330,7 +321,7 @@ const parseCellContent = (
|
|
|
330
321
|
/** @see lib/common/htmlparse.y:cell rule / lib/common/htmllex.c:mkCell */
|
|
331
322
|
const parseCell = (
|
|
332
323
|
s: ParseState, attrs: Record<string, string>, fontStack: FontState[],
|
|
333
|
-
): HtmlCell => buildCell(parseCellContent(s,
|
|
324
|
+
): HtmlCell => buildCell(parseCellContent(s, fontStack), attrs);
|
|
334
325
|
|
|
335
326
|
// ---------------------------------------------------------------------------
|
|
336
327
|
// parseRow — parses <TR>...</TR>
|
|
@@ -479,7 +470,7 @@ const consumeRowHr = (s: ParseState, rows: HtmlRow[]): void => {
|
|
|
479
470
|
const findFirstRealToken = (tokens: Token[], start: number): Token | undefined => {
|
|
480
471
|
for (let i = start; i < tokens.length; i++) {
|
|
481
472
|
const tk = tokens[i];
|
|
482
|
-
if (tk.type !== 'text' || tk.value
|
|
473
|
+
if (tk.type !== 'text' || nonSpace(tk.value)) return tk;
|
|
483
474
|
}
|
|
484
475
|
return undefined;
|
|
485
476
|
};
|
|
@@ -487,7 +478,7 @@ const findFirstRealToken = (tokens: Token[], start: number): Token | undefined =
|
|
|
487
478
|
const skipLeadingWs = (s: ParseState): void => {
|
|
488
479
|
while (true) {
|
|
489
480
|
const nxt = peek(s);
|
|
490
|
-
if (!nxt || nxt.type !== 'text' || nxt.value
|
|
481
|
+
if (!nxt || nxt.type !== 'text' || nonSpace(nxt.value)) break;
|
|
491
482
|
consume(s);
|
|
492
483
|
}
|
|
493
484
|
};
|
|
@@ -518,6 +509,7 @@ const parseFontTableLabel = (s: ParseState, open: OpenToken, fontStack: FontStat
|
|
|
518
509
|
|
|
519
510
|
const parseTableLabel = (s: ParseState, fontStack: FontState[]): HtmlLabel => {
|
|
520
511
|
skipLeadingWs(s);
|
|
512
|
+
if (heldNonSpace(peek(s))) throw parseError(s, peek(s), MSG_TEXT_BEFORE_TABLE);
|
|
521
513
|
const table = parseTable(s, fontStack);
|
|
522
514
|
finishTableLabel(s);
|
|
523
515
|
return { kind: 'table', table };
|
|
@@ -562,21 +554,16 @@ export interface ParsedHtmlLabel {
|
|
|
562
554
|
}
|
|
563
555
|
|
|
564
556
|
/**
|
|
565
|
-
* Parse an HTML-like label string,
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* (already stripped).
|
|
557
|
+
* Parse an HTML-like label string, writing the lexer's warnings to `stderr` as
|
|
558
|
+
* the parser reaches their tokens. The caller owns `stderr` and flushes it: a
|
|
559
|
+
* warning without a newline continues into the caller's next write. Input is
|
|
560
|
+
* the content between the outer `<` `>` delimiters (already stripped).
|
|
569
561
|
*
|
|
570
562
|
* @see lib/common/htmlparse.y:parseHTML
|
|
571
563
|
*/
|
|
572
|
-
export const parseHtmlLabelRv = (
|
|
573
|
-
src: string,
|
|
574
|
-
writeLine: (line: string) => void = (line) => console.warn(line),
|
|
575
|
-
): ParsedHtmlLabel => {
|
|
564
|
+
export const parseHtmlLabelRv = (src: string, stderr: StderrLines): ParsedHtmlLabel => {
|
|
576
565
|
const { tokens, error, eof } = tokenizeDeferred(`<HTML>${src}</HTML>`, { start: 6, end: 6 + src.length });
|
|
577
|
-
const s: ParseState = {
|
|
578
|
-
tokens, pos: 0, pending: error, stderr: new StderrLines(writeLine), emitted: new WeakSet(), warn: false, eof,
|
|
579
|
-
};
|
|
566
|
+
const s: ParseState = { tokens, pos: 0, pending: error, stderr, emitted: new WeakSet(), warn: false, eof };
|
|
580
567
|
try {
|
|
581
568
|
const label = parseTokens(s);
|
|
582
569
|
if (error !== undefined) throw error;
|
|
@@ -585,8 +572,6 @@ export const parseHtmlLabelRv = (
|
|
|
585
572
|
// Running off the truncated stream means C's lexer would have hit the error.
|
|
586
573
|
if (error !== undefined && s.pos >= tokens.length) { emitEof(s); throw error; }
|
|
587
574
|
throw e;
|
|
588
|
-
} finally {
|
|
589
|
-
s.stderr?.flush();
|
|
590
575
|
}
|
|
591
576
|
};
|
|
592
577
|
|
|
@@ -596,4 +581,4 @@ export const parseHtmlLabelRv = (
|
|
|
596
581
|
* the sync pass prints.
|
|
597
582
|
*/
|
|
598
583
|
export const parseHtmlLabel = (src: string): HtmlLabel =>
|
|
599
|
-
parseHtmlLabelRv(src, () => undefined).label;
|
|
584
|
+
parseHtmlLabelRv(src, new StderrLines(() => undefined)).label;
|
|
@@ -16,14 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { Box } from '../model/geom.js';
|
|
19
|
-
import type { TextlabelT } from './types.js';
|
|
20
19
|
import type { TextMeasurer } from './textmeasure.js';
|
|
21
|
-
import type { HtmlCell, HtmlImage, HtmlLabel, HtmlTable, HtmlText
|
|
20
|
+
import type { HtmlCell, HtmlImage, HtmlLabel, HtmlTable, HtmlText } from './htmltable-types.js';
|
|
22
21
|
import {
|
|
23
|
-
layoutHtmlTable, cellPad, cellBorder,
|
|
22
|
+
layoutHtmlTable, cellPad, cellBorder, withTableFont,
|
|
24
23
|
} from './htmltable.js';
|
|
25
|
-
import { makeLabel } from './make-label.js';
|
|
26
|
-
import { HtmlLexError } from './htmltable-lex.js';
|
|
27
24
|
import { TOP, BOTTOM, LEFT, RIGHT } from './splines-constants.js';
|
|
28
25
|
import type { PlacedLine, HtmlFontInfo } from './htmltable-pos-runs.js';
|
|
29
26
|
import {
|
|
@@ -171,15 +168,14 @@ export interface PlacedHtml {
|
|
|
171
168
|
* of pos_html_cell: LEFT flushes the box right edge in (`ur.x -= delx`), RIGHT
|
|
172
169
|
* flushes the left edge in, default splits (center); VALIGN BOTTOM/TOP flush,
|
|
173
170
|
* default centers on BOTH axes. Parallels the existing `alignImageBox`, but
|
|
174
|
-
* text default-centers vertically where an image does not.
|
|
175
|
-
*
|
|
176
|
-
* exactly what C does for every non-TEXT cell.)
|
|
171
|
+
* text default-centers vertically where an image does not. ALIGN="TEXT" keeps
|
|
172
|
+
* the box's width: lines then align across the whole cell.
|
|
177
173
|
* @see lib/common/htmltable.c:pos_html_cell (text branch, :1487-1526)
|
|
178
174
|
*/
|
|
179
175
|
export function alignContentBox(box: Box, cell: HtmlCell, w: number, h: number): Box {
|
|
180
176
|
const out: Box = { ll: { ...box.ll }, ur: { ...box.ur } };
|
|
181
177
|
const delx = out.ur.x - out.ll.x - w;
|
|
182
|
-
if (delx > 0) {
|
|
178
|
+
if (delx > 0 && cell.align !== 'text') {
|
|
183
179
|
if (cell.align === 'left') out.ur.x -= delx;
|
|
184
180
|
else if (cell.align === 'right') out.ll.x += delx;
|
|
185
181
|
else { out.ll.x += delx / 2; out.ur.x -= delx / 2; }
|
|
@@ -520,82 +516,3 @@ export function posHtmlLabel(
|
|
|
520
516
|
if (label.kind === 'table') return withTableFont(env, label.table.font, 'emit', () => posHtmlTable(label.table, env, measurer));
|
|
521
517
|
return posTextLabel(label, finfo, measurer);
|
|
522
518
|
}
|
|
523
|
-
|
|
524
|
-
/** parseHTML's rv for a label that failed to parse (`ctx->error`). */
|
|
525
|
-
const HTML_PARSE_FATAL = 3;
|
|
526
|
-
|
|
527
|
-
/** A built HTML label plus make_html_label's "problem" return value. */
|
|
528
|
-
export interface HtmlLabelResult {
|
|
529
|
-
label: TextlabelT;
|
|
530
|
-
rv: number;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* make_html_label: parse, size and position an HTML-like label. Returns C's
|
|
535
|
-
* rv too (nonzero: the caller appends `in label of ...`, labels.c:make_label).
|
|
536
|
-
* Parse diagnostics print here, once per label, as parseHTML does. The
|
|
537
|
-
* optional `imageSizer` resolves `<IMG SRC>`; absent means C's missing image.
|
|
538
|
-
* @see lib/common/htmltable.c:make_html_label
|
|
539
|
-
*/
|
|
540
|
-
export function makeHtmlLabelRv(
|
|
541
|
-
content: string,
|
|
542
|
-
finfo: HtmlFontInfo & { imageSizer?: ImageSizer; pencolor?: string },
|
|
543
|
-
measurer: TextMeasurer,
|
|
544
|
-
): HtmlLabelResult {
|
|
545
|
-
const { fontname, fontsize, fontcolor, imageSizer, pencolor } = finfo;
|
|
546
|
-
let lbl: HtmlLabel, lexWarn: boolean; // lexWarn: htmllex.c's ctx->warn, rv bit 1
|
|
547
|
-
try {
|
|
548
|
-
({ label: lbl, warn: lexWarn } = parseHtmlLabelRv(content));
|
|
549
|
-
} catch (e) {
|
|
550
|
-
// C aborts the HTML parse on a syntax error (YYABORT): parseHTML returns
|
|
551
|
-
// rv 3 (ctx->error) with no label, and make_html_label returns it with
|
|
552
|
-
// lp->html cleared and no spans, so the label renders empty.
|
|
553
|
-
// @see lib/common/htmltable.c:1880 (rv == 3), htmlparse.y (YYABORT)
|
|
554
|
-
if (e instanceof HtmlLexError) for (const line of e.diagnostics) console.error(line);
|
|
555
|
-
return { label: makeLabel('', fontname, fontsize, fontcolor, measurer), rv: HTML_PARSE_FATAL };
|
|
556
|
-
}
|
|
557
|
-
// A top-level table with no explicit COLOR inherits the object's pen color,
|
|
558
|
-
// which then flows table->cell->nested-table.
|
|
559
|
-
// @see lib/common/htmltable.c:1911 (getPenColor), :1406/:1556 (inheritance)
|
|
560
|
-
if (lbl.kind === 'table') inheritTablePenColor(lbl.table, pencolor);
|
|
561
|
-
const rv = Number(lexWarn) | sizeHtmlLabel(lbl, measurer, {
|
|
562
|
-
fontsize, fontname, imageSizer,
|
|
563
|
-
});
|
|
564
|
-
const placed = posHtmlLabel(lbl, { fontname, fontsize, fontcolor }, measurer);
|
|
565
|
-
const dim = lbl.dimen ?? { w: 0, h: 0 };
|
|
566
|
-
// C replaces table-label text: "this may be used for the title and
|
|
567
|
-
// alt fields in image maps" — anchors inherit it as default tooltip.
|
|
568
|
-
// @see lib/common/htmltable.c:make_html_label (line 1935)
|
|
569
|
-
const text = lbl.kind === 'table' ? '<TABLE>' : content;
|
|
570
|
-
return { label: htmlTextLabel(text, { fontname, fontsize, fontcolor }, placed, dim), rv };
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/** {@link makeHtmlLabelRv} without the problem flag. */
|
|
574
|
-
export function makeHtmlLabel(
|
|
575
|
-
content: string,
|
|
576
|
-
finfo: HtmlFontInfo & { imageSizer?: ImageSizer; pencolor?: string },
|
|
577
|
-
measurer: TextMeasurer,
|
|
578
|
-
): TextlabelT {
|
|
579
|
-
return makeHtmlLabelRv(content, finfo, measurer).label;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/** Build the TextlabelT wrapper around a positioned HTML label. */
|
|
583
|
-
function htmlTextLabel(
|
|
584
|
-
text: string,
|
|
585
|
-
finfo: HtmlFontInfo,
|
|
586
|
-
placed: PlacedHtml,
|
|
587
|
-
dim: { w: number; h: number },
|
|
588
|
-
): TextlabelT {
|
|
589
|
-
return {
|
|
590
|
-
text,
|
|
591
|
-
fontname: finfo.fontname, fontcolor: finfo.fontcolor, charset: 0,
|
|
592
|
-
fontsize: finfo.fontsize,
|
|
593
|
-
dimen: { x: dim.w, y: dim.h },
|
|
594
|
-
space: { x: dim.w, y: dim.h },
|
|
595
|
-
pos: { x: 0, y: 0 },
|
|
596
|
-
u: { kind: 'html', html: placed },
|
|
597
|
-
valign: 'c'.charCodeAt(0),
|
|
598
|
-
set: false,
|
|
599
|
-
html: true,
|
|
600
|
-
};
|
|
601
|
-
}
|
|
@@ -40,6 +40,9 @@ export class HtmlParseError extends DotEngineError {
|
|
|
40
40
|
/** @see lib/common/htmltable.h HALIGN_* flags */
|
|
41
41
|
export type HtmlAlign = 'left' | 'center' | 'right';
|
|
42
42
|
|
|
43
|
+
/** A cell's ALIGN: TEXT (HALIGN_TEXT) leaves the text block the cell's width. @see lib/common/htmllex.c:cell_halignfn */
|
|
44
|
+
export type HtmlCellAlign = HtmlAlign | 'text';
|
|
45
|
+
|
|
43
46
|
/** @see lib/common/htmltable.h VALIGN_* flags */
|
|
44
47
|
export type HtmlVAlign = 'top' | 'middle' | 'bottom';
|
|
45
48
|
|
|
@@ -132,7 +135,7 @@ export interface HtmlCell {
|
|
|
132
135
|
*/
|
|
133
136
|
vruled?: boolean;
|
|
134
137
|
port?: string;
|
|
135
|
-
align?:
|
|
138
|
+
align?: HtmlCellAlign;
|
|
136
139
|
valign?: HtmlVAlign;
|
|
137
140
|
balign?: HtmlAlign;
|
|
138
141
|
bgcolor?: string;
|
|
@@ -275,6 +278,8 @@ export interface OpenToken extends LexerOutput {
|
|
|
275
278
|
type: 'open';
|
|
276
279
|
tag: string;
|
|
277
280
|
attrs: Record<string, string>;
|
|
281
|
+
/** On a TABLE: `ctx->xb` text no token carried, read by the table rule's nonSpace. */
|
|
282
|
+
heldText?: string;
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
/** @see lib/common/htmllex.c:endElement */
|
package/src/common/htmltable.ts
CHANGED
|
@@ -20,13 +20,13 @@ export type { TextMeasurer } from './textmeasure.js';
|
|
|
20
20
|
|
|
21
21
|
import type {
|
|
22
22
|
HtmlCell, HtmlCellContent, HtmlImage, HtmlLabel, HtmlTable, HtmlText,
|
|
23
|
-
HtmlTextItem,
|
|
23
|
+
HtmlTextItem, ImageSizer,
|
|
24
24
|
} from './htmltable-types.js';
|
|
25
|
-
import type { ImageSizer } from './htmltable-types.js';
|
|
26
25
|
import { findImageSize } from '../gvc/usershape.js';
|
|
27
26
|
import { PointSet } from './pointset.js';
|
|
28
27
|
import type { TextMeasurer } from './textmeasure.js';
|
|
29
28
|
import { withTableFont } from './htmltable-fontinfo.js';
|
|
29
|
+
import { agerrLine, type StderrLines } from './htmltable-attr-warn.js';
|
|
30
30
|
|
|
31
31
|
export { withTableFont } from './htmltable-fontinfo.js';
|
|
32
32
|
|
|
@@ -85,6 +85,7 @@ export interface HtmlSizeEnv {
|
|
|
85
85
|
fontsize: number;
|
|
86
86
|
fontname: string;
|
|
87
87
|
imageSizer?: ImageSizer;
|
|
88
|
+
stderr?: StderrLines; // the label's stderr; none: straight to the console
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
const defaultSizeEnv = (): HtmlSizeEnv => ({ fontsize: DEFAULT_FONTSIZE, fontname: '' });
|
|
@@ -230,21 +231,20 @@ const sizeTextContent = (
|
|
|
230
231
|
/** Images whose sizing failed: size_html_img's rv = 1 on every later call. */
|
|
231
232
|
const missingImages = new WeakSet<HtmlImage>();
|
|
232
233
|
|
|
233
|
-
const sizeHtmlImg = (img: HtmlImage, imageSizer
|
|
234
|
+
const sizeHtmlImg = (img: HtmlImage, { imageSizer, stderr }: HtmlSizeEnv): number => {
|
|
234
235
|
// C sizes each image once (processTbl); the port's size and pos passes
|
|
235
236
|
// share layoutHtmlTable, so guard against re-sizing (and re-warning). The
|
|
236
237
|
// returned rv still follows C on every call.
|
|
237
238
|
if (img.width !== undefined && img.height !== undefined) return missingImages.has(img) ? 1 : 0;
|
|
238
239
|
const sz = imageSizer !== undefined ? imageSizer(img.src) : findImageSize(img.src);
|
|
239
240
|
if (sz !== null) {
|
|
240
|
-
img.width = sz.w;
|
|
241
|
-
img.height = sz.h;
|
|
241
|
+
img.width = sz.w; img.height = sz.h;
|
|
242
242
|
return 0;
|
|
243
243
|
}
|
|
244
244
|
// Missing-image: zero size + warning.
|
|
245
245
|
// C: agerrorf("No or improper image file=\"%s\"\n", img->src) — agerr
|
|
246
246
|
// prefixes "Error: " on stderr.
|
|
247
|
-
|
|
247
|
+
agerrLine(stderr, `Error: No or improper image file="${img.src}"`);
|
|
248
248
|
img.width = 0;
|
|
249
249
|
img.height = 0;
|
|
250
250
|
missingImages.add(img);
|
|
@@ -262,7 +262,7 @@ const sizeContentItem = (
|
|
|
262
262
|
return { ...(item.dimen !== undefined ? item.dimen : { w: 0, h: 0 }), rv };
|
|
263
263
|
}
|
|
264
264
|
if (item.kind === 'image') {
|
|
265
|
-
const rv = sizeHtmlImg(item, env
|
|
265
|
+
const rv = sizeHtmlImg(item, env);
|
|
266
266
|
return { w: item.width ?? 0, h: item.height ?? 0, rv };
|
|
267
267
|
}
|
|
268
268
|
return { w: 0, h: 0, rv: 0 };
|
|
@@ -327,15 +327,15 @@ const sizeWarned = new WeakSet<object>();
|
|
|
327
327
|
* @see lib/common/htmltable.c:1141 (size_html_cell FIXED_FLAG)
|
|
328
328
|
*/
|
|
329
329
|
const fixedCellCheck = (
|
|
330
|
-
cell: HtmlCell, sz: { w: number; h: number }, warn: boolean,
|
|
330
|
+
cell: HtmlCell, sz: { w: number; h: number }, warn: boolean, out: StderrLines | undefined,
|
|
331
331
|
): { rv: number; zero: boolean } => {
|
|
332
332
|
if (!cell.fixedsize) return { rv: 0, zero: false };
|
|
333
333
|
if (cell.width && cell.height) {
|
|
334
334
|
const small = (cell.width < sz.w || cell.height < sz.h) && cell.content[0]?.kind !== 'image';
|
|
335
|
-
if (small && warn)
|
|
335
|
+
if (small && warn) agerrLine(out, WARN_CELL_TOO_SMALL);
|
|
336
336
|
return { rv: small ? 1 : 0, zero: true };
|
|
337
337
|
}
|
|
338
|
-
if (warn)
|
|
338
|
+
if (warn) agerrLine(out, WARN_CELL_UNSPECIFIED);
|
|
339
339
|
return { rv: 1, zero: false };
|
|
340
340
|
};
|
|
341
341
|
|
|
@@ -360,7 +360,7 @@ const getCellSize = (
|
|
|
360
360
|
let szy = ch + margin;
|
|
361
361
|
const warn = !sizeWarned.has(cell);
|
|
362
362
|
sizeWarned.add(cell);
|
|
363
|
-
const fixed = fixedCellCheck(cell, { w: szx, h: szy }, warn);
|
|
363
|
+
const fixed = fixedCellCheck(cell, { w: szx, h: szy }, warn, env.stderr);
|
|
364
364
|
rv |= fixed.rv;
|
|
365
365
|
if (fixed.zero) { szx = 0; szy = 0; }
|
|
366
366
|
const bw = cell.width !== undefined ? cell.width : 0;
|
|
@@ -504,16 +504,16 @@ export const layoutHtmlTable = (
|
|
|
504
504
|
* table, the returned `rv = 1` follows C on every call.
|
|
505
505
|
* @see lib/common/htmltable.c:size_html_tbl (:1683, :1689)
|
|
506
506
|
*/
|
|
507
|
-
const warnFixedTable = (tbl: HtmlTable, dims: { wd: number; ht: number }): number => {
|
|
507
|
+
const warnFixedTable = (tbl: HtmlTable, dims: { wd: number; ht: number }, out: StderrLines | undefined): number => {
|
|
508
508
|
const warn = !sizeWarned.has(tbl);
|
|
509
509
|
sizeWarned.add(tbl);
|
|
510
510
|
if (tbl.fixedsize !== true) return 0;
|
|
511
511
|
if (tbl.width && tbl.height) {
|
|
512
512
|
if (tbl.width >= dims.wd && tbl.height >= dims.ht) return 0;
|
|
513
|
-
if (warn)
|
|
513
|
+
if (warn) agerrLine(out, WARN_TABLE_TOO_SMALL);
|
|
514
514
|
return 1;
|
|
515
515
|
}
|
|
516
|
-
if (warn)
|
|
516
|
+
if (warn) agerrLine(out, WARN_TABLE_UNSPECIFIED);
|
|
517
517
|
return 1;
|
|
518
518
|
};
|
|
519
519
|
|
|
@@ -525,7 +525,7 @@ const sizeTableBody = (
|
|
|
525
525
|
): number => {
|
|
526
526
|
const { entries, widths, heights, spacing, border, rv } = layoutHtmlTable(tbl, measurer, env);
|
|
527
527
|
if (entries.length === 0) {
|
|
528
|
-
const fixed = warnFixedTable(tbl, sumTableDims({ widths, heights, ncols: 0, nrows: 0, spacing, border }));
|
|
528
|
+
const fixed = warnFixedTable(tbl, sumTableDims({ widths, heights, ncols: 0, nrows: 0, spacing, border }), env.stderr);
|
|
529
529
|
tbl.dimen = { w: 0, h: 0 };
|
|
530
530
|
return rv | fixed;
|
|
531
531
|
}
|
|
@@ -535,7 +535,7 @@ const sizeTableBody = (
|
|
|
535
535
|
const { wd, ht } = sumTableDims({ widths, heights, ncols, nrows, spacing, border });
|
|
536
536
|
const baseWd = tbl.width !== undefined ? tbl.width : 0;
|
|
537
537
|
const baseHt = tbl.height !== undefined ? tbl.height : 0;
|
|
538
|
-
const fixed = warnFixedTable(tbl, { wd, ht });
|
|
538
|
+
const fixed = warnFixedTable(tbl, { wd, ht }, env.stderr);
|
|
539
539
|
// C zeroes only the CONTENT-derived term for a FIXEDSIZE table that has BOTH
|
|
540
540
|
// width and height, so the box becomes fmax(0, width)=width / fmax(0,height)=
|
|
541
541
|
// height (the explicit size). A fixed table missing a dimension warns and
|
package/src/common/make-label.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import type { TextlabelT } from './types.js';
|
|
13
13
|
import type { TextMeasurer, TextSize } from './textmeasure.js';
|
|
14
14
|
import type { TextSpan } from './emit-types.js';
|
|
15
|
-
import {
|
|
15
|
+
import { makeHtmlLabelTo } from './htmltable-label.js';
|
|
16
|
+
import { labelStderr } from './htmltable-attr-warn.js';
|
|
16
17
|
import { substObj, type GraphObj } from './subst.js';
|
|
17
18
|
import { Edge } from '../model/edge.js';
|
|
18
19
|
import { Graph } from '../model/graph.js';
|
|
@@ -205,8 +206,12 @@ export function makeAnyLabel(
|
|
|
205
206
|
// it (htmltable.c:1911 getPenColor). @see getPenColor
|
|
206
207
|
// ADR-2: a per-context sizer (when set) wins over the global one.
|
|
207
208
|
const imageSizer = obj !== undefined ? rootOf(obj).info.gvc?.imageSizer : undefined;
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
// One stderr for the label: doInt's unterminated warning runs into the
|
|
210
|
+
// next write, `in label of ...` included (agerr AGPREV).
|
|
211
|
+
const stderr = labelStderr();
|
|
212
|
+
const { label, rv } = makeHtmlLabelTo(content, { ...font, pencolor: getPenColor(obj), imageSizer }, measurer, stderr);
|
|
213
|
+
if (rv !== 0 && obj !== undefined) stderr.write(`${inLabelOf(obj)}\n`);
|
|
214
|
+
stderr.flush();
|
|
210
215
|
return label;
|
|
211
216
|
}
|
|
212
217
|
// Plain path: resolve \G \N \E \T \H \L against the owning object
|