Skip to content

Commit ab5d1b7

Browse files
committed
Add parseDotInt
1 parent b148d85 commit ab5d1b7

5 files changed

Lines changed: 43 additions & 17 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ Keep these documents up to date when behaviour changes.
7979

8080
## Workflow
8181

82+
**Every step in this workflow is mandatory. After completing any task, go through
83+
the full list and confirm each step has been done before considering the task
84+
complete. Never skip a step silently — if a step is not applicable, state why.**
85+
8286
1. Before writing any code, search the codebase for similar patterns and match
8387
their style (see first section).
8488
2. Verify any assumption about Graphviz output by running the `dot` binary

src/parser.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ import {
99
import { formatValueForDiagnostics } from './utils.ts';
1010
import type { Diagnostic, OverrideAttributes } from './viz.ts';
1111

12-
// To make parser internally consistent, all characters are read as UTF-16:
13-
/* eslint-disable unicorn/prefer-code-point */
14-
1512
const Char = {
1613
'\t': 0x09,
1714
'\n': 0x0a,
@@ -163,10 +160,14 @@ class Lexer {
163160
}
164161

165162
#readChar(): number {
163+
// To make parser internally consistent, all characters are read as UTF-16:
164+
/* eslint-disable unicorn/prefer-code-point */
166165
return this.#dotStr.charCodeAt(this.#nextIndex++);
167166
}
168167

169168
#peekChar(): number {
169+
// To make parser internally consistent, all characters are read as UTF-16:
170+
/* eslint-disable unicorn/prefer-code-point */
170171
return this.#dotStr.charCodeAt(this.#nextIndex);
171172
}
172173

@@ -1107,4 +1108,10 @@ class Parser {
11071108
}
11081109
}
11091110

1111+
export function isNumberToken(str: string): boolean {
1112+
const lexer = new Lexer(str);
1113+
const token = lexer.nextToken();
1114+
return token.kind === Kind.Number && token.length === str.length;
1115+
}
1116+
11101117
export const parseDot = Parser.parseDot;

src/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
import {
2+
NormalizedAttributes,
3+
type NormalizedAttributeValue,
4+
} from './normalize-graph.ts';
5+
import { isNumberToken } from './parser.ts';
6+
17
export function formatValueForDiagnostics(value: string) {
28
const truncated = value.length > 20 ? value.slice(0, 17) + '...' : value;
39
return JSON.stringify(truncated)
410
.replaceAll(String.raw`\"`, '"')
511
.replaceAll(String.raw`\\`, '\\')
612
.slice(1, -1);
713
}
14+
15+
export function parseDotNumber(value: NormalizedAttributeValue): number {
16+
return NormalizedAttributes.isText(value) && isNumberToken(value.text)
17+
? Number.parseFloat(value.text)
18+
: Number.NaN;
19+
}

src/viz.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
normalizeGraph,
77
} from './normalize-graph.ts';
88
import { parseDot } from './parser.ts';
9-
import { formatValueForDiagnostics } from './utils.ts';
9+
import { formatValueForDiagnostics, parseDotNumber } from './utils.ts';
1010

1111
export interface OverrideAttributes {
1212
/** Sets the default graph attributes. This corresponds to the {@link https://www.graphviz.org/doc/info/command.html#-G | `-G`} Graphviz command-line option. */
@@ -258,9 +258,7 @@ export class Viz {
258258
let dotOutputMaxLineLength = MAX_dotOutputMaxLineLength;
259259
const linelengthValue = graph.graphAttributes.get('linelength');
260260
if (linelengthValue !== undefined) {
261-
const number = NormalizedAttributes.isText(linelengthValue)
262-
? Number(linelengthValue.text)
263-
: Number.NaN;
261+
const number = parseDotNumber(linelengthValue);
264262
if (!Number.isInteger(number) || !isMaxLineLength(number)) {
265263
return failureResult([
266264
new RenderingBackendError(

test/render.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,9 @@ describe('Viz', () => {
374374
const viz = await VizPackage.instance();
375375
const result = viz.renderDot(
376376
'graph { a[image="test.png"]; b[image="test.png"] }',
377-
{ images: { 'test.png': { width: 300, height: 200 } } },
377+
{
378+
images: { 'test.png': { width: 300, height: 200 } },
379+
},
378380
);
379381

380382
expectDot(result).toMatchInlineSnapshot(`
@@ -420,15 +422,18 @@ describe('Viz', () => {
420422
});
421423

422424
describe('returns an error for invalid `linelength` values', () => {
423-
it.for(['-1', '59', '129', '1.5', 'abc', '<0>'])('$0', async (value) => {
424-
const viz = await VizPackage.instance();
425-
const result = viz.renderDot(`graph { linelength=${value} }`);
426-
427-
expect(result.status).toBe('failure');
428-
expect(stringifyDiagnostics(result.diagnostics)).toBe(
429-
"RenderingBackendError: linelength must be '0' or an integer in the [60, 128] range",
430-
);
431-
});
425+
it.for(['-1', '59', '129', '60.5', '"0x60"', '"60a"', 'abc', '<0>'])(
426+
'$0',
427+
async (value) => {
428+
const viz = await VizPackage.instance();
429+
const result = viz.renderDot(`graph { linelength=${value} }`);
430+
431+
expect(result.status).toBe('failure');
432+
expect(stringifyDiagnostics(result.diagnostics)).toBe(
433+
"RenderingBackendError: linelength must be '0' or an integer in the [60, 128] range",
434+
);
435+
},
436+
);
432437
});
433438

434439
it('accepts URLs for image names', async () => {

0 commit comments

Comments
 (0)