Om du vill omvandla nästlad text med parenteser till ett arrayträd skannar du den från vänster till höger, skapar en array för varje inledande parentes och återgår till den överordnade arrayen vid varje avslutande parentes. Den här guiden innehåller en komplett JavaScript-parser som du kan köra lokalt i Safari på en Mac.
Uppgiften här är att omvandla en kort, handskriven översikt med etiketter till nästlade arrayer och samtidigt bevara ordningen, citerad text och tomma grupper. Alla exempel använder syntetiska strängar. Parsern innehåller uttryckliga grammatikregler och kontroller så att du kan se om den resulterande strukturen motsvarar det du avsåg.
Bestäm vad parenteser och citerade etiketter betyder
Innan du omvandlar något bör du skilja strukturella parenteser från parenteser som ingår i en etikett. I det här exemplet skapar de yttre parenteserna en grupp, medan parenteserna inuti den dubbelt citerade etiketten förblir text:
(desk ("blue (round)" "say \"hello\"") () cards)
Den avsedda arraystrukturen är:
[
[
"desk",
["blue (round)", "say \"hello\""],
[],
"cards"
]
]
Den extra yttre arrayen är avsiktlig: den representerar hela dokumentet. Varje grupp inom parentes blir en array inuti den. Då blir returtypen enhetlig oavsett om indata innehåller en grupp, flera grupper eller etiketter utan omgivande parenteser.
Följande regler definierar artikelns indataformat. De är designval för den här parsern, inte ett inbyggt JavaScript-format för text med parenteser.
| Indatafunktion | Betydelse i det resulterande trädet |
|---|---|
( och ) utanför dubbla citattecken |
Börja och avsluta en nästlad array |
| Blanktecken utanför dubbla citattecken | Avgränsar etiketter; separatorn behålls inte |
En ociterad token som cards |
Skapar en sträng |
En dubbelt citerad sträng som "blue tile" |
Skapar en sträng, inklusive mellanrummen i den |
() |
Skapar en tom array |
"" |
Skapar en tom sträng |
12, true eller null utanför citattecken |
Behåller stavningen som en sträng |
| Kommatecken och enkla citattecken | Behandlas som vanliga tecken i en ociterad token |
Parenteser avgränsar också token, så a(b)c blir ["a", ["b"], "c"]. Två etiketter som står intill varandra behöver blanktecken emellan: "a"b avvisas. Om en etikett innehåller blanksteg, dubbla citattecken eller bokstavliga parenteser använder du den dubbelt citerade formen.
Dubbelt citerade token följer JSON-strängsyntax. Parsern skickar varje fullständig citerad token till JSON.parse(), som godtar JSON-text och kastar ett SyntaxError vid ogiltig syntax. Därmed överlåts valideringen av escape-sekvenser till den dokumenterade parsern. ECMAScript-specifikationen: JSON.parse
Öppna en lokal JavaScript-arbetsyta i Safari
Safaris Web Inspector har den JavaScript-prompt som behövs för den här skrivbordsomvandlingen. Du behöver inte installera något paket eller skapa ett separat projekt.
- I Safari väljer du
Safari→Inställningar…→Avancerat. - Aktivera
Visa funktioner för webbutvecklare. - Öppna en flik och skriv
about:blanki adressfältet. - Välj
Utveckling→Visa Web Inspector, eller tryck påAlternativ–Kommando–I. - Välj fliken
Konsoloch använd inmatningsprompten.
WebKit dokumenterar utvecklarinställningen och genvägen till inspektören i Aktivera Web Inspector. Referensen Web Inspector-gränssnittet förklarar att konsolprompten utvärderar JavaScript och visar resultaten.
Klistra in hela programmet nedan i prompten och kör det. Det innehåller parsern, ett eget exempel, sex kontroller av giltig indata och sju kontroller av felaktig indata. Inga importer krävs. Programmet kördes utan problem i Node.js 20.19.2; stegen för Safari-gränssnittet är dokumenterade, men testades inte självständigt för den här artikeln.
Kör den fullständiga parsern för text med parenteser
Parsern håller reda på öppna arrayer i en stack. En inledande parentes skapar en underordnad array och lägger den på stacken. En avslutande parentes tar bort den aktuella arrayen från stacken. Etiketter läggs till i den array som för tillfället är öppen.
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.");
Det första utskrivna resultatet är den nästlade strukturen som visades tidigare, formaterad över flera rader. Det sista meddelandet bör vara All 13 checks passed. Funktionen returnerar en faktisk JavaScript-array; JSON.stringify() omvandlar värdet till utskrivbar text, och det tredje argumentet anger indraget. ECMAScript-specifikationen: JSON.stringify
Om du vill omvandla en annan syntetisk översikt ändrar du bara exemplet som tilldelas input och kör programmet igen. Låt kontrollerna vara oförändrade så att de fortsätter att verifiera parserns avsedda beteende. Kopiera den utskrivna arraytexten till ditt arbetsdokument när du behöver en textrepresentation.
Bevara citering och escape-semantik
Det finns två lager att ta hänsyn till: JavaScript-källkoden som innehåller exemplet och texten med parenteser som ska tolkas. Exemplet använder en String.raw-taggad mallsträng så att omvänt snedstreck når parsern utan att först avkodas som vanliga strängescaper på JavaScript-källkodslagret. Den dokumenterade operationen läser mallsträngens råa segment. ECMAScript-specifikationen: String.raw
Behåll de omgivande bakåtcitaten runt det medföljande exemplet. I det här enkla redigeringsflödet bör du skriva exempeltext utan bakåtcitat eller mallinterpoleringens inledare ${, eftersom de har betydelse i JavaScripts mallsträngssyntax.
Inuti en dubbelt citerad indatatoken står \" för ett bokstavligt dubbelt citattecken, \\ står för ett omvänt snedstreck och \u0061 står för a. Det här är avkodade strängvärden: trädet bevarar etikettens betydelse, medan den ursprungliga escape-skrivningen och de omgivande citattecknen tas bort. Utanför dubbla citattecken behandlar den anpassade parsern omvänt snedstreck som ett vanligt token-tecken.
Den sista kontrollen av giltig indata bekräftar till exempel att indata-tokenen "a\\b", "\u0061" och "$&" blir en etikett med ett omvänt snedstreck, etiketten a och den bokstavliga etiketten $&. Om du behöver återskapa de ursprungliga blankstegen och escape-skrivningen exakt räcker inte en array med avkodade strängar; representationen måste också bevara källtexten eller tokenpositionerna.
Varför en generell ersättning ändrar resultatet
Att ersätta varje inledande parentes med en hakparentes och varje avslutande parentes med en annan hakparentes implementerar inte den här grammatiken. Det skulle också ändra parenteserna i "blue (round)" och lämna ociterade etiketter och saknade avgränsare mellan objekt olösta. Den slutsatsen följer direkt av det egna exemplet.
Ersättning har en annan relevant detalj: ett strängargument för ersättning tolkar specialsekvenser som $&, som infogar den matchade texten. En ersättningsfunktion skickar tillbaka sin text utan det substitutionssteget. ECMAScript-specifikationen: replaceAll och ersättningssubstitution
Det här fristående exemplet visar skillnaden:
const label = "tile";
console.log(label.replaceAll("tile", "$&")); // tile
console.log(label.replaceAll("tile", () => "$&")); // $&
Båda resultaten verifierades under förberedelserna. Trädparsern undviker ersättningssemantik helt: den lägger till avkodade strängar direkt i arrayer. Om du senare lägger till ett steg för att byta namn på etiketter gör du ersättningarna på de tolkade etiketterna och väljer uttryckligen om ersättningstexten ska tolkas eller infogas bokstavligt.
Felsök oväntade resultat innan du använder trädet igen
De medföljande kontrollerna fungerar som ett kompakt beslutsstöd. De omfattar tom indata, tomma grupper, intilliggande grupper och etiketter, citerade mellanslag, enbart strängvärden, escape-sekvenser och felaktig syntax. Använd följande symtom för att hitta en skillnad mellan den översikt du avsåg och den definierade grammatiken.
| Symtom | Förklaring och nästa steg |
|---|---|
| Resultatet har en extra yttre array | Roten representerar dokumentet. Behåll den om inte nästa steg uttryckligen kräver exakt en grupp på toppnivå. |
| En fras blir flera strängar | Blanksteg avgränsar ociterade etiketter. Sätt frasen inom dubbla citattecken. |
| Ett kommatecken finns kvar i en etikett | Kommatecken är vanliga tecken här. Använd blanksteg mellan separata etiketter. |
| Parenteser blir oväntat nästlade arrayer | Sätt bokstavliga parenteser inuti en dubbelt citerad etikett. |
Unexpected closing parenthesis |
En avslutande parentes saknar en motsvarande öppen grupp. Granska den angivna positionen. |
Unclosed opening parenthesis |
En grupp är fortfarande öppen i slutet. Positionen anger den innersta oavslutade inledande parentesen. |
Invalid JSON string |
Kontrollera om den citerade tokenen innehåller en ogiltig escape-sekvens eller ett oescapat kontrolltecken. |
| Ett separatorfel som gäller citattecken | Avgränsa intilliggande etiketter med blanksteg eller omslut hela den avsedda etiketten med en enda citerad sträng. |
Positionerna i implementationen börjar på noll och räknar UTF-16-kodenheter, i enlighet med JavaScripts strängindexering. De är inte radnummer eller en räkning av synliga tecken. Vid valideringsfel för citerade token anger den rapporterade positionen tokenens början, så granska hela den citerade etiketten.
Felaktig indata kastar ett undantag innan funktionen returnerar. Även om arrayer byggs upp internt medan skanningen pågår får anroparen inget partiellt träd. Parsern använder en uttrycklig stack i stället för rekursiva funktionsanrop, men exemplet fastställer ingen maximal dokumentstorlek eller nästlingsnivå.
För en kort skrivbordsöversikt är det praktiska tillvägagångssättet att komma överens om grammatiken, köra det oförändrade exemplet och kontrollerna och sedan byta ut texten mot din egen. Kontrollera citerade etiketter, tomma grupper och dokumentroten innan du använder resultatet igen; de tre detaljerna avgör om arrayträdet bevarar den struktur du avsåg.
Källor
- Aktivera Web Inspector — WebKit.
- Web Inspector-gränssnittet — WebKit.
- ECMAScript® 2027 Language Specification: strukturerade data — Ecma International / TC39, levande utkast.
- ECMAScript® 2027 Language Specification: textbehandling — Ecma International / TC39, levande utkast.