Om geneste tekst tussen haakjes om te zetten in een arrayboom, scan je deze van links naar rechts. Maak je voor elke openingshaak een array en keer je bij elke sluithaak terug naar de bovenliggende array. Deze handleiding biedt een complete JavaScript-parser die je lokaal in Safari op een Mac kunt uitvoeren.
De taak hier is om een kleine, zelf geschreven lijst met labels om te zetten in geneste arrays, waarbij de volgorde, geciteerde tekst en lege groepen behouden blijven. Alle voorbeelden gebruiken synthetische tekenreeksen. De parser bevat expliciete grammaticaregels en controles, zodat je kunt nagaan of de resulterende structuur overeenkomt met wat je bedoelde.
Bepaal wat haakjes en geciteerde labels betekenen
Maak voordat je iets omzet onderscheid tussen structurele haakjes en haakjes die deel uitmaken van een label. In dit voorbeeld vormen de buitenste haakjes een groep, terwijl de haakjes in het tussen dubbele aanhalingstekens geplaatste label tekst blijven:
(desk ("blue (round)" "say \"hello\"") () cards)
De bedoelde arraystructuur is:
[
[
"desk",
["blue (round)", "say \"hello\""],
[],
"cards"
]
]
De extra buitenste array is bewust opgenomen: deze stelt het hele document voor. Elke groep tussen haakjes wordt één array daarin. Zo blijft het retourtype consistent, of de invoer nu één groep, meerdere groepen of labels zonder omringende haakjes bevat.
De volgende regels definiëren de invoerindeling van dit artikel. Het zijn ontwerpkeuzes voor deze parser, geen ingebouwde JavaScript-indeling voor tekst tussen haakjes.
| Invoerfunctie | Betekenis in de resulterende boom |
|---|---|
( en ) buiten dubbele aanhalingstekens |
Starten en beëindigen een geneste array |
| Witruimte buiten dubbele aanhalingstekens | Scheidt labels; het scheidingsteken blijft niet behouden |
Een token zonder aanhalingstekens, zoals cards |
Levert een tekenreeks op |
Een tekenreeks tussen dubbele aanhalingstekens, zoals "blue tile" |
Levert één tekenreeks op, inclusief de interne spaties |
() |
Levert een lege array op |
"" |
Levert een lege tekenreeks op |
12, true of null buiten aanhalingstekens |
Behoudt de schrijfwijze als tekenreeks |
| Komma's en enkele aanhalingstekens | Worden behandeld als gewone tekens in een token zonder aanhalingstekens |
Haakjes scheiden ook tokens, dus a(b)c wordt ["a", ["b"], "c"]. Twee aangrenzende labels moeten door witruimte worden gescheiden: "a"b wordt afgewezen. Als een label spaties, dubbele aanhalingstekens of letterlijke haakjes bevat, gebruik je de vorm met dubbele aanhalingstekens.
Tokens tussen dubbele aanhalingstekens volgen de JSON-tekenreekssyntaxis. De parser geeft elk volledig geciteerd token door aan JSON.parse(), dat JSON-tekst accepteert en bij ongeldige syntaxis een SyntaxError genereert. Zo wordt escape-validatie overgelaten aan de gedocumenteerde parser. ECMAScript-specificatie: JSON.parse
Open een lokale JavaScript-werkruimte in Safari
Web Inspector van Safari biedt de JavaScript-prompt die nodig is voor deze desktopbewerking. Je hebt geen pakketinstallatie of apart project nodig.
- Kies in Safari
Safari→Instellingen…→Geavanceerd. - Schakel
Toon functies voor webontwikkelaarsin. - Open een tabblad en voer
about:blankin de adresbalk in. - Kies
Ontwikkel→Toon Web Inspector, of druk opOption–Command–I. - Selecteer het tabblad
Consoleen gebruik de invoerprompt.
WebKit documenteert de ontwikkelaarsinstelling en de sneltoets voor de inspector in Web Inspector inschakelen. De referentie Web Inspector-interface legt uit dat de consoleprompt JavaScript evalueert en de resultaten weergeeft.
Plak het volledige onderstaande programma in die prompt en voer het uit. Het bevat de parser, een zelf geschreven voorbeeld, zes controles voor geldige invoer en zeven controles voor ongeldige invoer. Imports zijn niet nodig. Het programma is met succes uitgevoerd in Node.js 20.19.2; de stappen voor de Safari-interface zijn gedocumenteerd, maar zijn voor dit artikel niet onafhankelijk getest.
Voer de volledige parser voor tekst tussen haakjes uit
De parser houdt een stapel van geopende arrays bij. Een opening haakje maakt een onderliggende array aan en plaatst die op de stapel. Een sluithaakje haalt de huidige array van de stapel. Labels worden toegevoegd aan de array die op dat moment geopend is.
function parenthesizedToTree(text) {
if (typeof text !== "string") {
throw new TypeError("Expected a string");
}
const root = [];
const stack = [{ items: root, start: -1 }];
const separator = c => /\s|[()]/u.test(c);
let i = 0;
function fail(message, at = i) {
throw new SyntaxError(message + " at UTF-16 offset " + at);
}
while (i < text.length) {
const c = text[i];
const current = stack[stack.length - 1].items;
if (/\s/u.test(c)) {
i++;
} else if (c === "(") {
const child = [];
current.push(child);
stack.push({ items: child, start: i });
i++;
} else if (c === ")") {
if (stack.length === 1) fail("Unexpected closing parenthesis");
stack.pop();
i++;
} else if (c === '"') {
const start = i++;
let closed = false;
while (i < text.length) {
if (text[i] === "\\") {
i += 2;
} else if (text[i] === '"') {
i++;
closed = true;
break;
} else {
i++;
}
}
if (!closed) fail("Unclosed quoted string", start);
let value;
try {
value = JSON.parse(text.slice(start, i));
} catch {
fail("Invalid JSON string", start);
}
if (i < text.length && !separator(text[i])) {
fail("Expected whitespace or a parenthesis");
}
current.push(value);
} else {
const start = i;
while (i < text.length && !separator(text[i])) {
if (text[i] === '"') fail("Quote inside an unquoted token");
i++;
}
current.push(text.slice(start, i));
}
}
if (stack.length !== 1) {
fail("Unclosed opening parenthesis", stack[stack.length - 1].start);
}
return root;
}
const input = String.raw`(desk ("blue (round)" "say \"hello\"") () cards)`;
console.log(JSON.stringify(parenthesizedToTree(input), null, 2));
const validCases = [
["", []],
["()", [[]]],
["a(b)c", ["a", ["b"], "c"]],
['("a b" "")', [["a b", ""]]],
["(12 true)", [["12", "true"]]],
[String.raw`("a\\b" "\u0061" "$&")`, [["a\\b", "a", "$&"]]]
];
for (const [source, expected] of validCases) {
const actual = parenthesizedToTree(source);
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error("Unexpected tree for " + source);
}
}
const invalidCases = [
"(a", "a)", '("a)', String.raw`("\q")`,
'"a"b', 'a"b"', '("line\nbreak")'
];
for (const source of invalidCases) {
let rejected = false;
try {
parenthesizedToTree(source);
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
rejected = true;
}
if (!rejected) throw new Error("Expected rejection for " + source);
}
console.log("All 13 checks passed.");
Het eerste afgedrukte resultaat is de eerder getoonde geneste structuur, opgemaakt over meerdere regels. Het laatste bericht hoort All 13 checks passed. te zijn. De functie retourneert een echte JavaScript-array; JSON.stringify() zet die waarde om in afdrukbare tekst en het derde argument ervan bepaalt de inspringing. ECMAScript-specificatie: JSON.stringify
Als je een andere synthetische lijst wilt omzetten, wijzig je alleen het voorbeeld dat aan input is toegewezen en voer je het programma opnieuw uit. Laat de controles ongewijzigd, zodat ze het bedoelde gedrag van de parser blijven verifiëren. Kopieer de afgedrukte arraytekst naar je werkdocument wanneer je een tekstuele weergave nodig hebt.
Behoud de betekenis van aanhalingstekens en escapes
Er zijn twee lagen om rekening mee te houden: de JavaScript-broncode met het voorbeeld en de te parseren tekst tussen haakjes. Het voorbeeld gebruikt een String.raw-gemarkeerde template, zodat de backslashes de parser bereiken zonder dat ze op JavaScript-bronniveau volgens de gewone tekenreeksen-escaperegels worden verwerkt. De gedocumenteerde bewerking leest de ruwe segmenten van de template. ECMAScript-specificatie: String.raw
Laat de backtickscheidingstekens rond het opgegeven voorbeeld staan. Maak voor deze eenvoudige bewerkingsworkflow voorbeeldtekst zonder backticks of de opener voor template-interpolatie ${; die hebben een betekenis in de JavaScript-templatesyntaxis.
Binnen een invoertoken tussen dubbele aanhalingstekens stelt \" één letterlijk dubbel aanhalingsteken voor, stelt \\ één backslash voor en stelt \u0061 a voor. Dit zijn gedecodeerde tekenreekswaarden: de boom behoudt de betekenis van het label, terwijl de oorspronkelijke schrijfwijze van escapes en de omringende aanhalingstekens worden weggegooid. Buiten dubbele aanhalingstekens behandelt de aangepaste parser backslashes als gewone tekens in een token.
De laatste controle voor geldige invoer bevestigt bijvoorbeeld dat de invoertokens "a\\b", "\u0061" en "$&" veranderen in een label met één backslash, het label a en het letterlijke label $&. Als je de oorspronkelijke spaties en schrijfwijze van escapes exact wilt reproduceren, volstaat een array met gedecodeerde tekenreeksen niet; de weergave moet dan ook de brontekst of tokenposities bewaren.
Waarom een algemene vervanging het resultaat verandert
Elke openingshaak vervangen door een vierkante haak en elke sluithaak door een andere vierkante haak voert deze grammatica niet uit. Ook zouden de haakjes binnen "blue (round)" veranderen, terwijl onomsloten labels en ontbrekende scheidingstekens tussen items onopgelost blijven. Die conclusie volgt rechtstreeks uit het zelf geschreven voorbeeld.
Bij vervangen is nog een ander detail van belang: een tekenreeks als vervangingsargument interpreteert speciale reeksen zoals $&, die de gevonden tekst invoegt. Een vervangingsfunctie levert de geretourneerde tekst zonder die substitutiestap. ECMAScript-specificatie: replaceAll en vervangingssubstitutie
Dit onafhankelijke voorbeeld laat het verschil zien:
const label = "tile";
console.log(label.replaceAll("tile", "$&")); // tile
console.log(label.replaceAll("tile", () => "$&")); // $&
Beide resultaten zijn tijdens de voorbereiding geverifieerd. De boomparser vermijdt vervangingssemantiek volledig: hij voegt gedecodeerde tekenreeksen rechtstreeks toe aan arrays. Als je later een stap toevoegt om labels te hernoemen, vervang dan tekst in de geparseerde labels en kies expliciet of vervangingstekst moet worden geïnterpreteerd of letterlijk moet worden ingevoegd.
Spoor onverwachte uitvoer op voordat je de boom opnieuw gebruikt
De opgenomen controles dienen als een beknopt hulpmiddel bij het bepalen van de oorzaak. Ze behandelen lege invoer, lege groepen, aangrenzende groepen en labels, spaties in geciteerde tekst, waarden die alleen uit tekenreeksen bestaan, escapes en ongeldige syntaxis. Gebruik de volgende symptomen om een verschil te vinden tussen je bedoelde lijst en de gedefinieerde grammatica.
| Symptoom | Uitleg en volgende actie |
|---|---|
| Het resultaat heeft een extra buitenste array | De root stelt het document voor. Behoud deze, tenzij de volgende stap precies één groep op het hoogste niveau vereist. |
| Een woordgroep wordt meerdere tekenreeksen | Witruimte scheidt labels zonder aanhalingstekens. Zet de woordgroep tussen dubbele aanhalingstekens. |
| Een komma blijft in een label staan | Komma's zijn hier gewone tekens. Gebruik witruimte tussen afzonderlijke labels. |
| Haakjes worden onverwacht geneste arrays | Zet letterlijke haakjes in een label tussen dubbele aanhalingstekens. |
Unexpected closing parenthesis |
Een sluithaakje heeft geen bijbehorende opening. Controleer de opgegeven positie. |
Unclosed opening parenthesis |
Aan het einde staat nog een groep open. De positie geeft aan waar het binnenste niet-gesloten openingshaakje staat. |
Invalid JSON string |
Controleer het geciteerde token op een ongeldige escape of een niet-geëscapet controleteken. |
| Een scheidingsfout met betrekking tot aanhalingstekens | Scheid aangrenzende labels met witruimte of zet het hele bedoelde label tussen dubbele aanhalingstekens. |
De offsets in deze implementatie beginnen bij nul en tellen UTF-16-code-eenheden, overeenkomstig de indexering van JavaScript-tekenreeksen. Het zijn geen regelnummers en ze tellen ook geen zichtbare tekens. Bij validatiefouten in geciteerde tokens is de gemelde positie het begin van het token; controleer daarom het volledige geciteerde label.
Ongeldige invoer genereert een uitzondering voordat de functie iets retourneert. Hoewel arrays tijdens het scannen intern worden opgebouwd, ontvangt de aanroeper geen gedeeltelijke boom. De parser gebruikt een expliciete stapel in plaats van recursieve functieaanroepen, maar het voorbeeld stelt geen maximale ondersteunde documentgrootte of nestingsdiepte vast.
Voor een kleine desktoplijst is de praktische volgorde: leg de grammatica vast, voer het ongewijzigde voorbeeld en de controles uit en vervang daarna de tekst door je zelf geschreven tekst. Controleer geciteerde labels, lege groepen en de documentroot voordat je het resultaat opnieuw gebruikt; die drie details bepalen of de arrayboom de structuur behoudt die je bedoelde.
Bronnen
- Web Inspector inschakelen — WebKit.
- Web Inspector-interface — WebKit.
- ECMAScript® 2027 Language Specification: Structured Data — Ecma International / TC39, levende ontwerpversie.
- ECMAScript® 2027 Language Specification: Text Processing — Ecma International / TC39, levende ontwerpversie.