Per trasformare testo nidificato con parentesi in un albero di array, scannerizzarlo da sinistra a destra, creando un array per ogni parentesi di apertura e tornando al suo genitore a ogni parentesi di chiusura. Questa guida fornisce un parser JavaScript completo che puoi eseguire localmente in Safari su Mac.

L'obiettivo qui è convertire un piccolo schema autoriale di etichette in array nidificati preservandone l'ordine, il testo quotato e i gruppi vuoti. Tutti gli esempi usano stringhe sintetiche. Il parser include regole grammaticali e controlli espliciti, così puoi verificare se la struttura risultante corrisponde a ciò che intendevi.

Decidere cosa significano le parentesi e le etichette quotate

Prima di convertire qualsiasi cosa, distingui le parentesi strutturali dalle parentesi che appartengono all'interno di un'etichetta. In questo esempio, le parentesi esterne creano un gruppo, mentre le parentesi all'interno dell'etichetta tra virgolette doppie rimangono testo:

(desk (\"blue (round)\" \"say \\\"hello\\\"\") () cards)

La struttura di array prevista è:

[
  [
    "desk",
    ["blue (round)", "say \"hello\""],
    [],
    "cards"
  ]
]

L'array esterno aggiuntivo è intenzionale: rappresenta l'intero documento. Ogni gruppo tra parentesi diventa un array al suo interno. Ciò rende il tipo di ritorno coerente sia che l'input contenga un gruppo, diversi gruppi o etichette senza parentesi circostanti.

Le seguenti regole definiscono il formato di input di questo articolo. Sono scelte di progettazione per questo parser, piuttosto che un formato JavaScript integrato per testo tra parentesi.

Caratteristica di input Significato nell'albero risultante
( e ) fuori dalle virgolette doppie Avviano e terminano un array nidificato
Spazi bianchi fuori dalle virgolette doppie Separano le etichette; non conservare il separatore
Un token non quotato come cards Produce una stringa
Una stringa tra virgolette doppie come \"blu piastrella\" Produce una stringa, inclusi i suoi spazi interni
() Produce un array vuoto
\"\" Produce una stringa vuota
12, true, o null fuori dalle virgolette Mantieni l'ortografia come stringa
Virgolette e virgolette singole Trattarle come caratteri ordinari in un token non quotato

Le parentesi separano anche i token, quindi a(b)c diventa ["a", ["b"], "c"]. Due etichette adiacenti necessitano di spazi bianchi: \"a\"b viene rifiutato. Se un'etichetta contiene spazi, virgolette doppie o parentesi letterali, usa la forma tra virgolette doppie.

I token tra virgolette doppie seguono la sintassi delle stringhe JSON. Il parser passa ogni token quotato completo a JSON.parse(), che accetta testo JSON e solleva un SyntaxError per sintassi non valida. Questo delega la validazione delle escape al parser documentato. Specifica ECMAScript: JSON.parse

Aprire un'area di lavoro JavaScript locale in Safari

L'Ispezionatore Web di Safari fornisce il prompt JavaScript necessario per questa trasformazione desktop. Non serve un'installazione di pacchetti o un progetto separato.

  1. In Safari, scegli Safari → Impostazioni… → Avanzate.
  2. Attiva Mostra funzionalità per sviluppatori web.
  3. Apri una scheda e inserisci about:blank nel campo indirizzo.
  4. Scegli Sviluppo → Mostra Ispezionatore Web, oppure premi Opzione–Comando–I.
  5. Seleziona la scheda Console e usa il suo prompt di input.

WebKit documenta l'impostazione per sviluppatori e la scorciatoia dell'ispezionatore in Abilitazione di Web Inspector. Il suo riferimento Interfaccia di Web Inspector spiega che il prompt della console valuta JavaScript e visualizza i risultati.

Incolla l'intero programma qui sotto in quel prompt ed eseguilo. Include il parser, un esempio autoriale, sei controlli di input validi e sette controlli di input malformati. Non sono necessarie importazioni. Il programma è stato eseguito con successo in Node.js 20.19.2; i passaggi dell'interfaccia Safari sono documentati, ma non sono stati testati indipendentemente per questo articolo.

Eseguire il parser completo per testo tra parentesi

Il parser mantiene uno stack di array aperti. Una parentesi di apertura crea un array figlio e lo pone nello stack. Una parentesi di chiusura rimuove l'array corrente dallo stack. Le etichette vengono aggiunte a qualunque array sia attualmente aperto.

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.");

Il primo risultato stampato è la struttura nidificata mostrata prima, formattata su più righe. Il messaggio finale dovrebbe essere All 13 checks passed. La funzione restituisce un array JavaScript effettivo; JSON.stringify() trasforma quel valore in testo stampabile, e il suo terzo argomento fornisce l'indentazione. Specifica ECMAScript: JSON.stringify

Per convertire un altro schema sintetico, cambia solo l'esempio assegnato a input e riesegui il programma. Mantieni i controlli invariati così continuano a verificare il comportamento previsto del parser. Copia il testo dell'array stampato nel tuo documento di lavoro quando hai bisogno di una rappresentazione testuale.

Preservare la semantica di quotatura ed escape

Ci sono due livelli da considerare: il codice sorgente JavaScript che contiene l'esempio, e il testo tra parentesi in fase di parsing. L'esempio usa un template taggato String.raw così i suoi backslash raggiungono il parser senza la decodifica ordinaria degli escape di stringa al livello del codice sorgente JavaScript. L'operazione documentata legge i segmenti raw del template. Specifica ECMAScript: String.raw

Mantieni i delimitatori backtick intorno all'esempio fornito. Per questo flusso di modifica semplice, scrivi il testo dell'esempio senza backtick o l'apritore di interpolazione del template ${, che hanno significato nella sintassi dei template JavaScript.

All'interno di un token di input tra virgolette doppie, \" rappresenta una virgoletta doppia letterale, \\ rappresenta un backslash, e \u0061 rappresenta a. Questi sono valori di stringa decodificati: l'albero preserva il significato dell'etichetta, mentre l'ortografia originale dell'escape e i segni di quotatura circostanti vengono scartati. Fuori dalle virgolette doppie, il parser personalizzato tratta i backslash come caratteri di token ordinari.

Per esempio, l'ultimo controllo di input valido conferma che i token di input "a\\b", "\u0061", e \"$&\" diventano un'etichetta contenente un backslash, l'etichetta a, e l'etichetta letterale $&. Se hai bisogno di riprodurre esattamente la spaziatura e l'ortografia dell'escape originale, un array di stringhe decodificate è insufficiente; la rappresentazione dovrebbe anche mantenere il testo sorgente o le posizioni dei token.

Perché una sostituzione globale cambia il risultato

Sostituire ogni parentesi di apertura con una parentesi quadra e ogni parentesi di chiusura con un'altra parentesi quadra non implementa questa grammatica. Cambierebbe anche le parentesi all'interno di "blue (round)", e lascerebbe irrisolte etichette non quotate e separatori di elementi mancanti. Questa conclusione deriva direttamente dall'esempio autoriale.

La sostituzione ha un altro dettaglio rilevante: un argomento di sostituzione stringa interpreta sequenze speciali come $&, che inserisce il testo corrisposto. Una funzione di sostituzione fornisce il suo testo restituito senza quel passaggio di sostituzione. Specifica ECMAScript: replaceAll e sostituzione di sostituzione

Questo esempio indipendente dimostra la differenza:

const label = "tile";
console.log(label.replaceAll("tile", "$&"));       // tile
console.log(label.replaceAll("tile", () => "$&")); // $&

Entrambi i risultati sono stati verificati durante la preparazione. Il parser ad albero evita interamente la semantica di sostituzione: aggiunge stringhe decodificate direttamente agli array. Se in seguito aggiungi un passo di rinomina delle etichette, effettua le sostituzioni sulle etichette parsate e scegli esplicitamente se il testo di sostituzione debba essere interpretato o inserito letteralmente.

Diagnosticare output inaspettato prima di riutilizzare l'albero

I controlli inclusi fungono da aiuto decisionale compatto. Coprono input vuoto, gruppi vuoti, gruppi ed etichette adiacenti, spazi quotati, valori solo stringa, escape, e sintassi malformata. Usa i seguenti sintomi per individuare una discrepanza tra il tuo schema inteso e la grammatica definita.

Sintomo Spiegazione e azione successiva
Il risultato ha un array esterno aggiuntivo La radice rappresenta il documento. Mantienilo a meno che il tuo passo successivo non richieda esplicitamente esattamente un gruppo di primo livello.
Una frase diventa diverse stringhe Lo spazio bianco separa le etichette non quotate. Metti la frase tra virgolette doppie.
Una virgola rimane in un'etichetta Le virgole sono caratteri ordinari qui. Usa spazi bianchi tra etichette separate.
Le parentesi diventano array nidificati inaspettatamente Metti le parentesi letterali dentro un'etichetta tra virgolette doppie.
Parentesi di chiusura inaspettata Una parentesi di chiusura non ha un gruppo di apertura corrispondente. Ispeziona la posizione segnalata.
Parentesi di apertura non chiusa Un gruppo rimane aperto alla fine. La posizione identifica la parentesi di apertura più interna non chiusa.
Stringa JSON non valida Controlla il token quotato per un escape non valido o un carattere di controllo senza escape.
Un errore di separatore legato alle virgolette Separa le etichette adiacenti con spazi bianchi, o racchiudi l'intera etichetta intesa in una stringa quotata.

Gli offset in questa implementazione partono da zero e contano unità di codice UTF-16, corrispondendo all'indicizzazione delle stringhe JavaScript. Non sono numeri di riga o un conteggio di caratteri visibili. Per errori di validazione di token quotati, la posizione segnalata è l'inizio del token, quindi ispeziona l'intera etichetta quotata.

L'input malformato solleva un'eccezione prima che la funzione restituisca. Anche se gli array vengono assemblati internamente mentre la scansione procede, i chiamanti non ricevono alcun albero parziale. Il parser usa uno stack esplicito invece di chiamate di funzioni ricorsive, ma l'esempio non stabilisce alcuna dimensione massima di documento o profondità di nidificazione supportata.

Per un piccolo schema desktop, la sequenza pratica è concordare la grammatica, eseguire l'esempio e i controlli invariati, poi sostituire il tuo testo autoriale. Conferma etichette quotate, gruppi vuoti, e la radice del documento prima di riutilizzare il risultato; quei tre dettagli determinano se l'albero di array mantiene la struttura che intendevi.

Fonti