@knowvah/dot-engine 2.6.2 → 2.6.3
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-lex.d.ts +38 -0
- package/dist/common/htmltable-lex.d.ts.map +1 -1
- package/dist/common/htmltable-parse-attrs.d.ts +41 -0
- package/dist/common/htmltable-parse-attrs.d.ts.map +1 -0
- package/dist/common/htmltable-parse.d.ts +3 -0
- package/dist/common/htmltable-parse.d.ts.map +1 -1
- package/dist/common/htmltable-pos.d.ts +17 -0
- package/dist/common/htmltable-pos.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +13 -1
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/spline-midpoint.d.ts +8 -0
- package/dist/common/spline-midpoint.d.ts.map +1 -1
- package/dist/index.js +617 -322
- package/dist/index.js.map +4 -4
- package/dist/label/node.d.ts +7 -1
- package/dist/label/node.d.ts.map +1 -1
- package/dist/layout/circo/position-helpers.d.ts +15 -3
- package/dist/layout/circo/position-helpers.d.ts.map +1 -1
- package/dist/layout/circo/position.d.ts +17 -1
- package/dist/layout/circo/position.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/pack/poly-pack.d.ts +2 -18
- package/dist/layout/pack/poly-pack.d.ts.map +1 -1
- package/dist/pathplan/shortest.d.ts.map +1 -1
- package/dist/render.js +770 -475
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/htmltable-lex.ts +265 -5
- package/src/common/htmltable-parse-attrs.ts +166 -0
- package/src/common/htmltable-parse.ts +262 -242
- package/src/common/htmltable-pos.ts +37 -38
- package/src/common/htmltable.ts +139 -45
- package/src/common/make-label.ts +19 -3
- package/src/common/spline-midpoint.ts +30 -9
- package/src/label/node.ts +8 -2
- package/src/layout/circo/position-helpers.ts +28 -17
- package/src/layout/circo/position.ts +28 -7
- package/src/layout/dot/ortho-adapter.ts +20 -9
- package/src/layout/neato/multispline.ts +17 -5
- package/src/layout/pack/poly-pack.ts +5 -31
- package/src/pathplan/shortest.ts +15 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.3",
|
|
4
4
|
"description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphviz",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { HtmlParseError } from './htmltable-types.js';
|
|
15
15
|
import type { Token } from './htmltable-types.js';
|
|
16
|
-
import { htmlEntityUTF8 } from './html-entities.js';
|
|
16
|
+
import { HTML_ENTITIES, htmlEntityUTF8 } from './html-entities.js';
|
|
17
17
|
|
|
18
18
|
export type { Token } from './htmltable-types.js';
|
|
19
19
|
|
|
@@ -140,12 +140,272 @@ function parseOpenToken(inner: string) {
|
|
|
140
140
|
return { type: 'open' as const, tag: normalizeTag(tag), attrs: parseAttrs(inner.slice(sp + 1)) };
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
143
|
+
/**
|
|
144
|
+
* expat `XML_ErrorString` texts for the two errors the port detects. Taken
|
|
145
|
+
* from native dot's stderr (graphviz 16.1.0): expat's xmlparse.c is not
|
|
146
|
+
* vendored with the graphviz sources.
|
|
147
|
+
* @see lib/common/htmllex.c:htmllex (XML_ErrorString at the XML_Parse failure)
|
|
148
|
+
*/
|
|
149
|
+
const XML_ERROR_UNDEFINED_ENTITY = 'undefined entity';
|
|
150
|
+
const XML_ERROR_TAG_MISMATCH = 'mismatched tag';
|
|
151
|
+
const XML_ERROR_INVALID_TOKEN = 'not well-formed (invalid token)';
|
|
152
|
+
const XML_ERROR_DUPLICATE_ATTRIBUTE = 'duplicate attribute';
|
|
153
|
+
|
|
154
|
+
/** Entities expat knows without a DTD; `scanEntity` leaves them as written. */
|
|
155
|
+
const XML_PREDEFINED_ENTITIES = new Set(['lt', 'gt', 'amp', 'quot', 'apos']);
|
|
156
|
+
|
|
157
|
+
const XML_NAME = /^[A-Za-z_:][\w.:-]*$/;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A label failure C reports on stderr before dropping the HTML label
|
|
161
|
+
* (`ctx->error`, parseHTML rv 3). `diagnostics` holds the lines in print
|
|
162
|
+
* order, each already carrying its `Error: ` prefix where C's agerrorf adds
|
|
163
|
+
* one; the caller prints them (one `console.error` each) and then C's
|
|
164
|
+
* `in label of ...` continuation (labels.c:make_label).
|
|
165
|
+
* @see lib/common/htmllex.c:htmllex
|
|
166
|
+
*/
|
|
167
|
+
export class HtmlLexError extends HtmlParseError {
|
|
168
|
+
readonly diagnostics: readonly string[];
|
|
169
|
+
|
|
170
|
+
constructor(tag: string, message: string, diagnostics: readonly string[]) {
|
|
171
|
+
super(tag);
|
|
172
|
+
this.message = message;
|
|
173
|
+
this.diagnostics = diagnostics;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* `agerrorf("%s in line %lu \n", msg, lineno)` followed by `error_context`.
|
|
179
|
+
* @see lib/common/htmllex.c:htmlerror
|
|
180
|
+
* @see lib/common/htmllex.c:error_context
|
|
181
|
+
*/
|
|
182
|
+
export function htmlErrorInLine(message: string, line: number, context: string): HtmlLexError {
|
|
183
|
+
const text = `${message} in line ${line} `;
|
|
184
|
+
return new HtmlLexError(message, text, [`Error: ${text}`, `... ${context} ...`]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The C lexer's current and previous "token" (raw chunk) plus open elements. */
|
|
188
|
+
interface ChunkState {
|
|
189
|
+
stack: string[];
|
|
190
|
+
prev: string;
|
|
191
|
+
curr: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Where a token came from: the raw chunk and the C `error_context` pair. */
|
|
195
|
+
interface ChunkInfo {
|
|
196
|
+
src: string;
|
|
197
|
+
pos: number;
|
|
198
|
+
end: number;
|
|
199
|
+
context: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const chunkInfo = new WeakMap<Token, ChunkInfo>();
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* expat's line number at `pos`: `\n`, `\r\n` and a lone `\r` each end a line.
|
|
206
|
+
* @see lib/common/htmllex.c:htmllineno
|
|
207
|
+
*/
|
|
208
|
+
function lineAt(src: string, pos: number): number {
|
|
209
|
+
let line = 1;
|
|
210
|
+
for (let k = 0; k < pos; k++) {
|
|
211
|
+
if (src[k] === '\n' || (src[k] === '\r' && src[k + 1] !== '\n')) line++;
|
|
212
|
+
}
|
|
213
|
+
return line;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* `XML_GetCurrentLineNumber` after the chunk that produced `tok`: a tag's own
|
|
218
|
+
* line, or for character data the line of expat's last data piece (pieces
|
|
219
|
+
* split at each newline).
|
|
220
|
+
*/
|
|
221
|
+
function chunkLine(info: ChunkInfo): number {
|
|
222
|
+
if (info.src[info.pos] === '<') return lineAt(info.src, info.pos);
|
|
223
|
+
const last = info.src.lastIndexOf('\n', info.end - 1);
|
|
224
|
+
if (last < info.pos) return lineAt(info.src, info.pos);
|
|
225
|
+
return lineAt(info.src, last === info.end - 1 ? last : last + 1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* C's `htmlerror` (yacc error) as it reads when the lexer last returned
|
|
230
|
+
* `tok`; undefined when the token has no recorded chunk.
|
|
231
|
+
* @see lib/common/htmllex.c:htmlerror
|
|
232
|
+
*/
|
|
233
|
+
export function htmlErrorAtToken(message: string, tok: Token | undefined): HtmlLexError | undefined {
|
|
234
|
+
const info = tok === undefined ? undefined : chunkInfo.get(tok);
|
|
235
|
+
if (info === undefined) return undefined;
|
|
236
|
+
return htmlErrorInLine(message, chunkLine(info), info.context);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Extent of the next chunk: `<..>` or the characters up to the next `<`. */
|
|
240
|
+
function chunkEnd(src: string, i: number): number {
|
|
241
|
+
const j = src[i] === '<' ? src.indexOf('>', i + 1) : src.indexOf('<', i);
|
|
242
|
+
if (j < 0) return src.length;
|
|
243
|
+
return src[i] === '<' ? j + 1 : j;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Offset of the first `&name;` that scanEntity leaves for expat to reject. */
|
|
247
|
+
function findUndefinedEntity(text: string): number {
|
|
248
|
+
for (let t = text.indexOf('&'); t >= 0; t = text.indexOf('&', t + 1)) {
|
|
249
|
+
if (text[t + 1] === '#') continue;
|
|
250
|
+
const semi = text.indexOf(';', t + 1);
|
|
251
|
+
if (semi < 0) continue;
|
|
252
|
+
const key = text.slice(t + 1, semi);
|
|
253
|
+
if (XML_NAME.test(key) && !Object.hasOwn(HTML_ENTITIES, key) && !XML_PREDEFINED_ENTITIES.has(key)) {
|
|
254
|
+
return t;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return -1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface TagChunk {
|
|
261
|
+
close: boolean;
|
|
262
|
+
selfClose: boolean;
|
|
263
|
+
name: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Element name of a complete `<..>` chunk; undefined for comments etc. */
|
|
267
|
+
function parseTagChunk(chunk: string): TagChunk | undefined {
|
|
268
|
+
if (!chunk.endsWith('>')) return undefined;
|
|
269
|
+
const body = chunk.slice(1, -1);
|
|
270
|
+
if (body.startsWith('!')) return undefined;
|
|
271
|
+
const close = body.startsWith('/');
|
|
272
|
+
const name = /^[^\s/>]*/.exec(close ? body.slice(1) : body)?.[0] ?? '';
|
|
273
|
+
return { close, selfClose: !close && body.endsWith('/'), name };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface AttrError {
|
|
277
|
+
message: string;
|
|
278
|
+
offset: number;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ATTR_NAME = /^[A-Za-z_:][\w.:-]*/;
|
|
282
|
+
const isSpace = (c: string | undefined): boolean => c !== undefined && /\s/.test(c);
|
|
283
|
+
|
|
284
|
+
const invalidToken = (offset: number): AttrError => ({ message: XML_ERROR_INVALID_TOKEN, offset });
|
|
285
|
+
|
|
286
|
+
function skipSpace(chunk: string, i: number): number {
|
|
287
|
+
let j = i;
|
|
288
|
+
while (isSpace(chunk[j])) j++;
|
|
289
|
+
return j;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface AttrPair {
|
|
293
|
+
name: string;
|
|
294
|
+
next: number;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The `name = "value"` pair at `i`, or the error expat raises there;
|
|
299
|
+
* undefined for an unterminated quote, which is not modelled.
|
|
300
|
+
*/
|
|
301
|
+
function readAttrPair(chunk: string, i: number): AttrPair | AttrError | undefined {
|
|
302
|
+
const name = ATTR_NAME.exec(chunk.slice(i))?.[0];
|
|
303
|
+
if (name === undefined) return invalidToken(i);
|
|
304
|
+
let j = skipSpace(chunk, i + name.length);
|
|
305
|
+
if (chunk[j] !== '=') return invalidToken(j);
|
|
306
|
+
j = skipSpace(chunk, j + 1);
|
|
307
|
+
const q = chunk[j];
|
|
308
|
+
if (q !== '"' && q !== "'") return invalidToken(j);
|
|
309
|
+
const end = chunk.indexOf(q, j + 1);
|
|
310
|
+
return end < 0 ? undefined : { name, next: end + 1 };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Walk the `name="value"` pairs of an open tag as expat does and return the
|
|
315
|
+
* first well-formedness error with its offset in the chunk. A duplicate is
|
|
316
|
+
* only reported when no invalid token follows it, as expat checks duplicates
|
|
317
|
+
* after tokenizing the whole tag.
|
|
318
|
+
*/
|
|
319
|
+
function findAttrError(chunk: string, from: number): AttrError | undefined {
|
|
320
|
+
const seen = new Set<string>();
|
|
321
|
+
let dup: AttrError | undefined;
|
|
322
|
+
let i = skipSpace(chunk, from);
|
|
323
|
+
while (chunk[i] !== '>' && !(chunk[i] === '/' && chunk[i + 1] === '>')) {
|
|
324
|
+
const pair = readAttrPair(chunk, i);
|
|
325
|
+
if (pair === undefined) return undefined;
|
|
326
|
+
if ('message' in pair) return pair;
|
|
327
|
+
if (seen.has(pair.name) && dup === undefined) dup = { message: XML_ERROR_DUPLICATE_ATTRIBUTE, offset: i };
|
|
328
|
+
seen.add(pair.name);
|
|
329
|
+
i = skipSpace(chunk, pair.next);
|
|
330
|
+
}
|
|
331
|
+
return dup;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Raise the error C hits for this tag chunk: expat's mismatched tag first
|
|
336
|
+
* (it is detected before the end handler runs), then `lexerror`'s unknown
|
|
337
|
+
* element.
|
|
338
|
+
* @see lib/common/htmllex.c:lexerror
|
|
339
|
+
*/
|
|
340
|
+
function checkTagChunk(src: string, start: number, st: ChunkState): void {
|
|
341
|
+
const tag = parseTagChunk(st.curr);
|
|
342
|
+
if (tag === undefined) return;
|
|
343
|
+
if (tag.close) {
|
|
344
|
+
if (st.stack.length > 0 && st.stack.pop() !== tag.name) {
|
|
345
|
+
throw htmlErrorInLine(XML_ERROR_TAG_MISMATCH, lineAt(src, start), st.prev + st.curr);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
const bad = findAttrError(st.curr, 1 + tag.name.length);
|
|
349
|
+
if (bad !== undefined) {
|
|
350
|
+
throw htmlErrorInLine(bad.message, lineAt(src, start + bad.offset), st.prev + st.curr);
|
|
351
|
+
}
|
|
352
|
+
if (!tag.selfClose) st.stack.push(tag.name);
|
|
353
|
+
}
|
|
354
|
+
if (KNOWN_TAGS.has(tag.name.toUpperCase())) return;
|
|
355
|
+
const text = `Unknown HTML element <${tag.name}> on line ${lineAt(src, start)} `;
|
|
356
|
+
throw new HtmlLexError(tag.name.toUpperCase(), text, [`Error: ${text}`]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Raise the expat or lexer error C would hit while parsing this chunk. */
|
|
360
|
+
function checkChunk(src: string, start: number, st: ChunkState): void {
|
|
361
|
+
if (st.curr.startsWith('<')) {
|
|
362
|
+
checkTagChunk(src, start, st);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const off = findUndefinedEntity(st.curr);
|
|
366
|
+
if (off >= 0) {
|
|
367
|
+
throw htmlErrorInLine(XML_ERROR_UNDEFINED_ENTITY, lineAt(src, start + off), st.prev + st.curr);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Tokens produced before the first lexical error, plus that error. */
|
|
372
|
+
export interface TokenizeResult {
|
|
373
|
+
tokens: Token[];
|
|
374
|
+
error?: HtmlParseError;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Tokenize until the first lexical error and return both. C interleaves
|
|
379
|
+
* lexing with the yacc parse, so a syntax error in the tokens before the
|
|
380
|
+
* failing chunk is reported in preference to the lexical error; callers that
|
|
381
|
+
* parse the partial stream get that precedence.
|
|
382
|
+
* @see lib/common/htmllex.c:findNext
|
|
383
|
+
*/
|
|
384
|
+
export function tokenizeDeferred(src: string): TokenizeResult {
|
|
145
385
|
const tokens: Token[] = [];
|
|
386
|
+
const st: ChunkState = { stack: [], prev: '', curr: '' };
|
|
146
387
|
let i = 0;
|
|
147
|
-
|
|
148
|
-
|
|
388
|
+
try {
|
|
389
|
+
while (i < src.length) {
|
|
390
|
+
const first = tokens.length;
|
|
391
|
+
const start = i;
|
|
392
|
+
st.prev = st.curr;
|
|
393
|
+
st.curr = src.slice(i, chunkEnd(src, i));
|
|
394
|
+
checkChunk(src, i, st);
|
|
395
|
+
i = src[i] === '<' ? scanTag(src, i, tokens) : scanText(src, i, tokens);
|
|
396
|
+
const info: ChunkInfo = { src, pos: start, end: i, context: st.prev + st.curr };
|
|
397
|
+
for (let k = first; k < tokens.length; k++) chunkInfo.set(tokens[k]!, info);
|
|
398
|
+
}
|
|
399
|
+
} catch (e) {
|
|
400
|
+
if (e instanceof HtmlParseError) return { tokens, error: e };
|
|
401
|
+
throw e;
|
|
149
402
|
}
|
|
403
|
+
return { tokens };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** @see lib/common/htmllex.c:findNext */
|
|
407
|
+
export function tokenize(src: string): Token[] {
|
|
408
|
+
const { tokens, error } = tokenizeDeferred(src);
|
|
409
|
+
if (error !== undefined) throw error;
|
|
150
410
|
return tokens;
|
|
151
411
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Attribute interpretation for the HTML-like label parser: the values
|
|
4
|
+
* mkTbl/mkCell/mkBR/mkFont read off a tag, and the table/cell records built
|
|
5
|
+
* from them.
|
|
6
|
+
*
|
|
7
|
+
* @see lib/common/htmllex.c:mkTbl
|
|
8
|
+
* @see lib/common/htmllex.c:mkCell
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
HtmlAlign,
|
|
13
|
+
HtmlCell,
|
|
14
|
+
HtmlCellContent,
|
|
15
|
+
HtmlRow,
|
|
16
|
+
HtmlTable,
|
|
17
|
+
HtmlVAlign,
|
|
18
|
+
} from './htmltable-types.js';
|
|
19
|
+
import {
|
|
20
|
+
BORDER_LEFT,
|
|
21
|
+
BORDER_TOP,
|
|
22
|
+
BORDER_RIGHT,
|
|
23
|
+
BORDER_BOTTOM,
|
|
24
|
+
BORDER_MASK,
|
|
25
|
+
} from './htmltable-types.js';
|
|
26
|
+
import { htmlEntityUTF8 } from './html-entities.js';
|
|
27
|
+
|
|
28
|
+
/** Decode XML/HTML entities in an anchor attribute value (href/target), as C's
|
|
29
|
+
* HTML parser does. The stored value is the real character (`&` → `&`), so
|
|
30
|
+
* imap emits it raw and cmapx re-escapes it. Undefined passes through. */
|
|
31
|
+
function anchorAttr(v: string | undefined): string | undefined {
|
|
32
|
+
return v === undefined ? undefined : htmlEntityUTF8(v);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Attribute helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export const parseAlign = (v: string): HtmlAlign | undefined => {
|
|
40
|
+
const u = v.toUpperCase();
|
|
41
|
+
if (u === 'LEFT') return 'left';
|
|
42
|
+
if (u === 'RIGHT') return 'right';
|
|
43
|
+
if (u === 'CENTER') return 'center';
|
|
44
|
+
return undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const parseVAlign = (v: string): HtmlVAlign | undefined => {
|
|
48
|
+
const u = v.toUpperCase();
|
|
49
|
+
if (u === 'TOP') return 'top';
|
|
50
|
+
if (u === 'BOTTOM') return 'bottom';
|
|
51
|
+
if (u === 'MIDDLE') return 'middle';
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const num = (s: string | undefined): number | undefined => {
|
|
56
|
+
if (s === undefined) return undefined;
|
|
57
|
+
const n = parseInt(s, 10);
|
|
58
|
+
return isNaN(n) ? undefined : n;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const parseFixedSize = (a: Record<string, string>): true | undefined =>
|
|
62
|
+
a['fixedsize']?.toUpperCase() === 'TRUE' ? true : undefined;
|
|
63
|
+
|
|
64
|
+
export const parseTitle = (a: Record<string, string>): string | undefined =>
|
|
65
|
+
a['title'] !== undefined ? a['title'] : a['tooltip'];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse SIDES attribute into a bitmask matching C sidesfn().
|
|
69
|
+
* Unrecognised chars are silently skipped (C issues a warning but continues).
|
|
70
|
+
* C guard: if (flags != BORDER_MASK) p->flags |= flags — the full-mask case
|
|
71
|
+
* is a no-op (means "all sides", the default), so we return undefined for it.
|
|
72
|
+
* @see lib/common/htmllex.c:sidesfn
|
|
73
|
+
*/
|
|
74
|
+
export const parseSides = (v: string | undefined): number | undefined => {
|
|
75
|
+
if (v === undefined) return undefined;
|
|
76
|
+
let flags = 0;
|
|
77
|
+
for (const c of v.toLowerCase()) {
|
|
78
|
+
if (c === 'l') flags |= BORDER_LEFT;
|
|
79
|
+
else if (c === 't') flags |= BORDER_TOP;
|
|
80
|
+
else if (c === 'r') flags |= BORDER_RIGHT;
|
|
81
|
+
else if (c === 'b') flags |= BORDER_BOTTOM;
|
|
82
|
+
}
|
|
83
|
+
// C: if (flags != BORDER_MASK) p->flags |= flags
|
|
84
|
+
if (flags === 0 || flags === BORDER_MASK) return undefined;
|
|
85
|
+
return flags;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse GRADIENTANGLE attribute — integer in [0, 360].
|
|
90
|
+
* Out-of-range or non-integer values are ignored (C issues a warning).
|
|
91
|
+
* @see lib/common/htmllex.c:gradientanglefn
|
|
92
|
+
*/
|
|
93
|
+
export const parseGradientAngle = (v: string | undefined): number | undefined => {
|
|
94
|
+
if (v === undefined) return undefined;
|
|
95
|
+
const n = parseInt(v, 10);
|
|
96
|
+
if (isNaN(n) || String(n) !== v.trim()) return undefined;
|
|
97
|
+
if (n < 0 || n > 360) return undefined;
|
|
98
|
+
return n;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse COLUMNS attribute — only "*" is valid (sets vrule=true).
|
|
103
|
+
* @see lib/common/htmllex.c:columnsfn
|
|
104
|
+
*/
|
|
105
|
+
export const parseColumns = (v: string | undefined): true | undefined =>
|
|
106
|
+
v === '*' ? true : undefined;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse ROWS attribute — only "*" is valid (sets hrule=true).
|
|
110
|
+
* @see lib/common/htmllex.c:rowsfn
|
|
111
|
+
*/
|
|
112
|
+
export const parseRows = (v: string | undefined): true | undefined =>
|
|
113
|
+
v === '*' ? true : undefined;
|
|
114
|
+
|
|
115
|
+
export const buildCell = (content: HtmlCellContent[], a: Record<string, string>): HtmlCell => ({
|
|
116
|
+
kind: 'cell',
|
|
117
|
+
content,
|
|
118
|
+
port: a['port'],
|
|
119
|
+
align: parseAlign(a['align'] !== undefined ? a['align'] : ''),
|
|
120
|
+
valign: parseVAlign(a['valign'] !== undefined ? a['valign'] : ''),
|
|
121
|
+
balign: parseAlign(a['balign'] !== undefined ? a['balign'] : ''),
|
|
122
|
+
bgcolor: a['bgcolor'],
|
|
123
|
+
color: a['color'],
|
|
124
|
+
border: num(a['border']),
|
|
125
|
+
cellpadding: num(a['cellpadding']),
|
|
126
|
+
cellspacing: num(a['cellspacing']),
|
|
127
|
+
sides: parseSides(a['sides']),
|
|
128
|
+
gradientangle: parseGradientAngle(a['gradientangle']),
|
|
129
|
+
style: a['style'],
|
|
130
|
+
width: num(a['width']),
|
|
131
|
+
height: num(a['height']),
|
|
132
|
+
fixedsize: parseFixedSize(a),
|
|
133
|
+
colspan: num(a['colspan']),
|
|
134
|
+
rowspan: num(a['rowspan']),
|
|
135
|
+
href: anchorAttr(a['href']),
|
|
136
|
+
title: parseTitle(a),
|
|
137
|
+
target: anchorAttr(a['target']),
|
|
138
|
+
id: a['id'],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export const buildTable = (rows: HtmlRow[], a: Record<string, string>): HtmlTable => ({
|
|
142
|
+
kind: 'table' as const,
|
|
143
|
+
rows,
|
|
144
|
+
border: num(a['border']),
|
|
145
|
+
cellborder: num(a['cellborder']),
|
|
146
|
+
cellspacing: num(a['cellspacing']),
|
|
147
|
+
cellpadding: num(a['cellpadding']),
|
|
148
|
+
bgcolor: a['bgcolor'],
|
|
149
|
+
color: a['color'],
|
|
150
|
+
style: a['style'],
|
|
151
|
+
align: parseAlign(a['align'] !== undefined ? a['align'] : ''),
|
|
152
|
+
valign: parseVAlign(a['valign'] !== undefined ? a['valign'] : ''),
|
|
153
|
+
fixedsize: parseFixedSize(a),
|
|
154
|
+
width: num(a['width']),
|
|
155
|
+
height: num(a['height']),
|
|
156
|
+
href: anchorAttr(a['href']),
|
|
157
|
+
title: parseTitle(a),
|
|
158
|
+
target: anchorAttr(a['target']),
|
|
159
|
+
id: a['id'],
|
|
160
|
+
port: a['port'],
|
|
161
|
+
sides: parseSides(a['sides']),
|
|
162
|
+
gradientangle: parseGradientAngle(a['gradientangle']),
|
|
163
|
+
vrule: parseColumns(a['columns']),
|
|
164
|
+
hrule: parseRows(a['rows']),
|
|
165
|
+
});
|
|
166
|
+
|