Files
job-search/web/src/lib/jsonTokens.ts
T
bwilliams f8648d765a Added a dialog with each opportunity's details and releases
Every row has a details icon under the Find a Tender link. It opens a
dialog with the opportunity's details from the list, and fetches the
opportunity with its releases, shown as tabs, newest first. Each tab
summarises the release's tender, awards and contracts, links to its
Find a Tender notice where it is one, and has the release's JSON,
pretty-printed and highlighted, in a section laid out only when it's
opened.

The JSON is highlighted by splitting it into tokens shown as text, not
HTML, as releases come from outside sources. The README's saved
preferences now include the filters.
2026-10-19 10:37:27 +01:00

47 lines
1.6 KiB
TypeScript

/** What a piece of JSON text is, for highlighting. */
export type JsonTokenKind = 'key' | 'string' | 'number' | 'literal' | 'plain';
/** A run of JSON text of one kind. */
export interface JsonToken {
kind: JsonTokenKind;
text: string;
}
// A string (a key when a colon follows), a number, a literal, or anything
// else (punctuation and spaces), which is plain.
const TOKEN =
/("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false|null)\b/g;
/**
* The tokens of JSON text, in order, so it can be highlighted as text rather
* than as HTML. Neighbouring tokens of the same kind are joined, so a
* document gives as few as possible. Text that isn't valid JSON still comes
* back whole, with what doesn't match as plain.
*/
export function tokeniseJson(text: string): JsonToken[] {
const tokens: JsonToken[] = [];
const push = (kind: JsonTokenKind, part: string): void => {
if (part === '') return;
const last = tokens.at(-1);
if (last?.kind === kind) last.text += part;
else tokens.push({ kind, text: part });
};
let end = 0;
for (const match of text.matchAll(TOKEN)) {
push('plain', text.slice(end, match.index));
const [whole, string, colon, number, literal] = match;
if (string !== undefined) {
push(colon === undefined ? 'string' : 'key', string);
push('plain', colon ?? '');
} else if (number !== undefined) {
push('number', number);
} else if (literal !== undefined) {
push('literal', literal);
}
end = match.index + whole.length;
}
push('plain', text.slice(end));
return tokens;
}