Aby zamienić zagnieżdżony tekst w nawiasach na drzewo tablic, skanuj go od lewej do prawej, tworząc tablicę przy każdym nawiasie otwierającym i wracając do tablicy nadrzędnej przy każdym nawiasie zamykającym. Ten poradnik zawiera kompletny parser JavaScript, który możesz uruchomić lokalnie w Safari na Macu.
Zadanie polega na przekształceniu niewielkiego, ręcznie przygotowanego konspektu etykiet w zagnieżdżone tablice z zachowaniem ich kolejności, tekstu w cudzysłowach i pustych grup. Wszystkie przykłady używają syntetycznych ciągów znaków. Parser zawiera jawne reguły gramatyczne i kontrole, dzięki czemu możesz sprawdzić, czy otrzymana struktura odpowiada Twoim zamierzeniom.
Określ znaczenie nawiasów i etykiet w cudzysłowach
Zanim cokolwiek przekształcisz, odróżnij nawiasy strukturalne od nawiasów należących do etykiety. W tym przykładzie nawiasy zewnętrzne tworzą grupę, a nawiasy wewnątrz etykiety ujętej w podwójny cudzysłów pozostają tekstem:
(desk ("blue (round)" "say \"hello\"") () cards)
Zamierzona struktura tablicy wygląda tak:
[
[
"desk",
["blue (round)", "say \"hello\""],
[],
"cards"
]
]
Dodatkowa tablica zewnętrzna jest celowa: reprezentuje cały dokument. Każda grupa ujęta w nawiasy staje się jedną tablicą wewnątrz niej. Dzięki temu typ zwracanej wartości jest zawsze taki sam, niezależnie od tego, czy wejście zawiera jedną grupę, kilka grup, czy etykiety bez otaczających je nawiasów.
Poniższe reguły określają format wejściowy używany w tym artykule. Są to decyzje projektowe tego parsera, a nie wbudowany format tekstu w nawiasach dostępny w JavaScript.
| Element wejściowy | Znaczenie w wynikowym drzewie |
|---|---|
( i ) poza podwójnym cudzysłowem |
Rozpoczęcie i zakończenie zagnieżdżonej tablicy |
| Białe znaki poza podwójnym cudzysłowem | Oddzielają etykiety; separator nie jest zachowywany |
Nieujęty w cudzysłów token, taki jak cards |
Tworzy ciąg znaków |
Ciąg ujęty w podwójny cudzysłów, taki jak "blue tile" |
Tworzy jeden ciąg znaków, wraz z zawartymi w nim spacjami |
() |
Tworzy pustą tablicę |
"" |
Tworzy pusty ciąg znaków |
12, true lub null poza cudzysłowem |
Zachowuje zapis jako ciąg znaków |
| Przecinki i apostrofy | Traktuje jako zwykłe znaki w nieujętym w cudzysłów tokenie |
Nawiasy również rozdzielają tokeny, więc a(b)c staje się ["a", ["b"], "c"]. Dwie sąsiadujące etykiety muszą być rozdzielone białym znakiem: zapis "a"b zostanie odrzucony. Jeśli etykieta zawiera spacje, cudzysłowy lub nawiasy będące częścią tekstu, użyj wersji ujętej w podwójny cudzysłów.
Tokeny ujęte w podwójny cudzysłów muszą być zgodne ze składnią ciągów JSON. Parser przekazuje każdy kompletny token w cudzysłowie do JSON.parse(), które akceptuje tekst JSON i zgłasza SyntaxError przy nieprawidłowej składni. Walidację znaków ucieczki powierzono udokumentowanemu parserowi. Specyfikacja ECMAScript: JSON.parse
Otwórz lokalne środowisko JavaScript w Safari
Inspektor WWW w Safari udostępnia konsolę JavaScript potrzebną do tej operacji na komputerze. Nie musisz instalować pakietu ani tworzyć osobnego projektu.
- W Safari wybierz
Safari→Ustawienia…→Zaawansowane. - Włącz opcję
Pokaż funkcje dla twórców stron. - Otwórz kartę i wpisz
about:blankw polu adresu. - Wybierz
Develop→Show Web Inspectoralbo naciśnijOption–Command–I. - Wybierz kartę
Consolei skorzystaj z pola wprowadzania poleceń.
WebKit opisuje ustawienie dla twórców stron i skrót otwierający inspektor w artykule Włączanie inspektora WWW. W dokumentacji Interfejs inspektora WWW wyjaśniono, że konsola wykonuje kod JavaScript wpisany w polu poleceń i wyświetla wyniki.
Wklej cały poniższy program do tego pola i uruchom go. Zawiera parser, przygotowany przykład, sześć kontroli poprawnych danych wejściowych i siedem kontroli niepoprawnych danych wejściowych. Nie wymaga importowania żadnych modułów. Program został pomyślnie uruchomiony w Node.js 20.19.2; opisano kroki dla interfejsu Safari, ale nie przetestowano ich niezależnie na potrzeby tego artykułu.
Uruchom kompletny parser tekstu w nawiasach
Parser przechowuje stos otwartych tablic. Nawias otwierający tworzy tablicę podrzędną i umieszcza ją na stosie. Nawias zamykający zdejmuje bieżącą tablicę ze stosu. Etykiety są dodawane do tablicy znajdującej się aktualnie na wierzchu stosu.
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.");
Pierwszy wydrukowany wynik to pokazana wcześniej zagnieżdżona struktura, sformatowana w wielu wierszach. Ostatni komunikat powinien mieć postać All 13 checks passed. Funkcja zwraca rzeczywistą tablicę JavaScript; JSON.stringify() zamienia tę wartość na tekst do wyświetlenia, a trzeci argument określa wcięcie. Specyfikacja ECMAScript: JSON.stringify
Aby przekształcić inny syntetyczny konspekt, zmień tylko przykład przypisany do input i uruchom program ponownie. Pozostaw kontrole bez zmian, by nadal sprawdzały zamierzone działanie parsera. Gdy potrzebujesz tekstowej reprezentacji, skopiuj wydrukowany tekst tablicy do dokumentu roboczego.
Zachowaj cudzysłowy i semantykę znaków ucieczki
Należy uwzględnić dwie warstwy: kod źródłowy JavaScript zawierający przykład oraz analizowany tekst w nawiasach. W przykładzie użyto szablonu znaków oznaczonego String.raw, dzięki czemu ukośniki odwrotne trafiają do parsera bez dekodowania zwykłych znaków ucieczki na poziomie kodu źródłowego JavaScript. Udokumentowana operacja odczytuje surowe segmenty szablonu. Specyfikacja ECMAScript: String.raw
Zachowaj znaki grawisu otaczające podany przykład. W tym prostym sposobie edycji przygotuj tekst przykładu bez grawisów i bez sekwencji rozpoczynającej interpolację szablonu ${, ponieważ mają one znaczenie w składni szablonów JavaScript.
W tokenie wejściowym ujętym w podwójny cudzysłów \" oznacza dosłowny cudzysłów, \\ oznacza jeden ukośnik odwrotny, a \u0061 oznacza a. Są to zdekodowane wartości ciągów znaków: drzewo zachowuje znaczenie etykiety, a jej pierwotny zapis z użyciem znaków ucieczki i otaczające ją cudzysłowy zostają odrzucone. Poza podwójnym cudzysłowem parser traktuje ukośniki odwrotne jako zwykłe znaki tokenu.
Na przykład ostatnia kontrola poprawnych danych wejściowych potwierdza, że tokeny "a\\b", "\u0061" i "$&" stają się etykietą zawierającą jeden ukośnik odwrotny, etykietą a oraz dosłowną etykietą $&. Jeśli musisz dokładnie odtworzyć pierwotne odstępy i zapis znaków ucieczki, tablica z odkodowanymi ciągami znaków nie wystarczy; reprezentacja musiałaby także zachowywać tekst źródłowy lub pozycje tokenów.
Dlaczego globalne zastępowanie zmienia wynik
Zastąpienie każdego nawiasu otwierającego nawiasem kwadratowym i każdego nawiasu zamykającego innym nawiasem kwadratowym nie realizuje tej gramatyki. Zmieniłoby również nawiasy wewnątrz "blue (round)", a nieujęte w cudzysłów etykiety i brakujące separatory elementów pozostałyby nierozwiązane. Wniosek ten wynika bezpośrednio z przygotowanego przykładu.
Przy zastępowaniu istotny jest jeszcze jeden szczegół: argument będący ciągiem znaków przekazany do metody zastępowania interpretuje sekwencje specjalne, takie jak $&, które wstawiają dopasowany tekst. Funkcja zastępująca zwraca tekst bez tego etapu podstawiania. Specyfikacja ECMAScript: replaceAll i podstawianie w zastępowanym tekście
Ten niezależny przykład pokazuje różnicę:
const label = "tile";
console.log(label.replaceAll("tile", "$&")); // tile
console.log(label.replaceAll("tile", () => "$&")); // $&
Oba wyniki zostały zweryfikowane podczas przygotowania artykułu. Parser drzewa całkowicie unika semantyki zastępowania: dodaje zdekodowane ciągi znaków bezpośrednio do tablic. Jeśli później dodasz etap zmiany nazw etykiet, wykonaj zastępowanie na przetworzonych etykietach i świadomie wybierz, czy tekst zastępujący ma być interpretowany, czy wstawiany dosłownie.
Zdiagnozuj nieoczekiwany wynik przed ponownym użyciem drzewa
Dołączone kontrole stanowią zwięzłą pomoc w podejmowaniu decyzji. Obejmują puste dane wejściowe, puste grupy, sąsiadujące grupy i etykiety, spacje w cudzysłowach, same ciągi znaków, znaki ucieczki i niepoprawną składnię. Skorzystaj z poniższych objawów, aby znaleźć niezgodność między zamierzonym konspektem a zdefiniowaną gramatyką.
| Objaw | Wyjaśnienie i dalsze działanie |
|---|---|
| Wynik zawiera dodatkową zewnętrzną tablicę | Korzeń reprezentuje dokument. Zachowaj go, chyba że kolejny krok wymaga dokładnie jednej grupy najwyższego poziomu. |
| Fraza staje się kilkoma ciągami znaków | Białe znaki rozdzielają etykiety nieujęte w cudzysłów. Umieść frazę w podwójnym cudzysłowie. |
| Przecinek pozostaje częścią etykiety | W tym formacie przecinki są zwykłymi znakami. Oddzielaj osobne etykiety białymi znakami. |
| Nawiasy niespodziewanie tworzą zagnieżdżone tablice | Umieść nawiasy będące częścią tekstu etykiety w podwójnym cudzysłowie. |
Unexpected closing parenthesis |
Nawias zamykający nie ma odpowiadającej mu otwartej grupy. Sprawdź zgłoszoną pozycję. |
Unclosed opening parenthesis |
Grupa pozostaje otwarta na końcu. Pozycja wskazuje najbardziej wewnętrzny niezamknięty nawias otwierający. |
Invalid JSON string |
Sprawdź, czy token w cudzysłowie nie zawiera niepoprawnego znaku ucieczki lub niezabezpieczonego znaku sterującego. |
| Błąd separatora związany z cudzysłowem | Oddziel sąsiadujące etykiety białym znakiem albo ujmij całą zamierzoną etykietę w jeden ciąg w cudzysłowie. |
Pozycje w tej implementacji zaczynają się od zera i są liczone w 16-bitowych jednostkach kodowych UTF-16, zgodnie z indeksowaniem ciągów JavaScript. Nie są to numery wierszy ani liczba widocznych znaków. W przypadku błędów walidacji tokenu w cudzysłowie zgłoszona pozycja wskazuje początek tokenu, więc sprawdź całą etykietę w cudzysłowie.
Niepoprawne dane wejściowe powodują zgłoszenie wyjątku, zanim funkcja zwróci wynik. Chociaż tablice są wewnętrznie budowane w trakcie skanowania, wywołujący nie otrzymuje częściowego drzewa. Parser używa jawnego stosu zamiast rekurencyjnych wywołań funkcji, ale przykład nie określa maksymalnego rozmiaru dokumentu ani maksymalnej głębokości zagnieżdżenia.
W przypadku niewielkiego konspektu na komputerze praktyczna kolejność działań jest następująca: uzgodnij gramatykę, uruchom niezmieniony przykład i kontrole, a następnie podstaw własny tekst. Sprawdź etykiety w cudzysłowach, puste grupy i korzeń dokumentu przed ponownym użyciem wyniku; te trzy elementy decydują o tym, czy drzewo tablic zachowuje zamierzoną strukturę.
Źródła
- Włączanie inspektora WWW — WebKit.
- Interfejs inspektora WWW — WebKit.
- Specyfikacja języka ECMAScript® 2027: dane strukturalne — Ecma International / TC39, aktualizowany projekt.
- Specyfikacja języka ECMAScript® 2027: przetwarzanie tekstu — Ecma International / TC39, aktualizowany projekt.