İç içe parantezli metni dizi ağacına dönüştürmek için soldan sağa tarayın; her açılış parantezinde bir dizi oluşturun ve her kapanış parantezinde üst dizisine dönün. Bu kılavuz, Mac’te Safari’de yerel olarak çalıştırabileceğiniz eksiksiz bir JavaScript ayrıştırıcısı sunar.

Buradaki görev; sıralarını, tırnak içine alınmış metni ve boş grupları koruyarak, elle yazılmış küçük bir etiket taslağını iç içe dizilere dönüştürmektir. Tüm örneklerde sentetik dizeler kullanılır. Ayrıştırıcı, açık dilbilgisi kuralları ve denetimleri içerir; böylece ortaya çıkan yapının amaçladığınız anlamı taşıyıp taşımadığını anlayabilirsiniz.

Parantezlerin ve tırnak içindeki etiketlerin anlamını belirleyin

Herhangi bir dönüştürme yapmadan önce yapısal parantezleri, bir etiketin içinde yer alan parantezlerden ayırın. Bu örnekte dış parantezler bir grup oluştururken çift tırnak içine alınmış etiketin içindeki parantezler metin olarak kalır:

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

Amaçlanan dizi yapısı şöyledir:

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

Ek dış dizi bilinçli olarak kullanılmıştır: tüm belgeyi temsil eder. Parantez içindeki her grup, bunun içinde bir diziye dönüşür. Böylece girdi tek grup, birkaç grup veya çevresinde parantez bulunmayan etiketler içerse de dönüş türü tutarlı olur.

Aşağıdaki kurallar bu makalenin girdi biçimini tanımlar. Bunlar bu ayrıştırıcı için belirlenmiş tasarım tercihleridir; JavaScript’in yerleşik parantezli metin biçimi değildir.

Girdi özelliği Ortaya çıkan ağaçtaki anlamı
Çift tırnakların dışındaki ( ve ) İç içe bir dizi başlatır ve bitirir
Çift tırnakların dışındaki boşluklar Etiketleri ayırır; ayırıcı korunmaz
cards gibi tırnaksız bir belirteç Bir dize üretir
"blue tile" gibi çift tırnak içindeki bir dize İçindeki boşluklar dahil tek bir dize üretir
() Boş bir dizi üretir
"" Boş bir dize üretir
Tırnakların dışındaki 12, true veya null Yazılışını dize olarak korur
Virgüller ve tek tırnaklar Tırnaksız bir belirteçte sıradan karakterler olarak değerlendirilir

Parantezler de belirteçleri ayırır; bu nedenle a(b)c, ["a", ["b"], "c"] olur. Yan yana gelen iki etiket arasında boşluk bulunmalıdır: "a"b reddedilir. Bir etikette boşluk, çift tırnak veya düz parantez varsa çift tırnaklı biçimi kullanın.

Çift tırnaklı belirteçler JSON dize söz dizimini izler. Ayrıştırıcı her tam tırnaklı belirteci JSON.parse() işlevine iletir. Bu işlev JSON metnini kabul eder ve söz dizimi geçersizse SyntaxError hatası verir. Bu sayede kaçış doğrulaması, belgelenmiş ayrıştırıcıya bırakılır. ECMAScript belirtimi: JSON.parse

Safari’de yerel bir JavaScript çalışma alanı açın

Safari’nin Web Inspector’ı, bu masaüstü dönüşümü için gereken JavaScript istemini sağlar. Bir paket yüklemeniz veya ayrı bir proje oluşturmanız gerekmez.

  1. Safari’de Safari → Settings… → Advanced seçeneklerini seçin.
  2. Show features for web developers seçeneğini etkinleştirin.
  3. Bir sekme açın ve adres alanına about:blank yazın.
  4. Develop → Show Web Inspector seçeneğini seçin veya Option–Command–I tuşlarına basın.
  5. Console sekmesini seçin ve giriş istemini kullanın.

WebKit, geliştirici ayarını ve denetleyici kısayolunu Web Inspector’ı Etkinleştirme sayfasında belgeliyor. Web Inspector Arayüzü başvuru belgesi, konsol isteminin JavaScript’i değerlendirdiğini ve sonuçları gösterdiğini açıklıyor.

Aşağıdaki programın tamamını bu isteme yapıştırıp çalıştırın. Program; ayrıştırıcıyı, elle yazılmış bir örneği, altı geçerli girdi denetimini ve yedi hatalı girdi denetimini içerir. İçe aktarma gerekmez. Program Node.js 20.19.2’de başarıyla çalıştırıldı; Safari arayüzü adımları belgelenmiştir ancak bu makale için ayrıca denenmemiştir.

Parantezli metin ayrıştırıcısının tamamını çalıştırın

Ayrıştırıcı, açık dizilerden oluşan bir yığın tutar. Açılış parantezi bir alt dizi oluşturur ve onu yığına ekler. Kapanış parantezi geçerli diziyi yığından çıkarır. Etiketler o anda açık olan diziye eklenir.

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

İlk yazdırılan sonuç, daha önce gösterilen ve birden çok satıra biçimlendirilmiş iç içe yapıdır. Son mesaj All 13 checks passed. olmalıdır. İşlev gerçek bir JavaScript dizisi döndürür; JSON.stringify() bu değeri yazdırılabilir metne dönüştürür ve üçüncü bağımsız değişkeni girintiyi belirler. ECMAScript belirtimi: JSON.stringify

Başka bir sentetik taslağı dönüştürmek için yalnızca input değişkenine atanan örneği değiştirip programı yeniden çalıştırın. Ayrıştırıcının amaçlanan davranışını doğrulamaya devam etmeleri için denetimleri değiştirmeyin. Metinsel bir gösterime ihtiyaç duyduğunuzda yazdırılan dizi metnini çalışma belgenize kopyalayın.

Tırnakları ve kaçış anlamlarını koruyun

Dikkate alınması gereken iki katman vardır: örneği içeren JavaScript kaynak kodu ve ayrıştırılan parantezli metin. Örnekte, ters eğik çizgilerin JavaScript kaynak katmanında olağan dize kaçış çözümlemesine uğramadan ayrıştırıcıya ulaşması için String.raw etiketli şablon kullanılır. Belgelenen işlem, şablonun ham parçalarını okur. ECMAScript belirtimi: String.raw

Verilen örneğin çevresindeki ters tırnakları koruyun. Bu basit düzenleme akışında, JavaScript şablon söz diziminde anlamı olan ters tırnakları veya şablon enterpolasyon başlatıcısı ${ öğesini içermeyen örnek metinler yazın.

Çift tırnaklı bir girdi belirtecinde \" düz çift tırnağı, \\ tek bir ters eğik çizgiyi ve \u0061 a öğesini temsil eder. Bunlar çözümlenmiş dize değerleridir: ağaç etiketin anlamını korur, ancak özgün kaçış yazımını ve çevresindeki tırnak işaretlerini atar. Çift tırnakların dışında özel ayrıştırıcı, ters eğik çizgileri sıradan belirteç karakterleri olarak değerlendirir.

Örneğin, geçerli girdi denetimlerinin sonuncusu "a\\b", "\u0061" ve "$&" girdi belirteçlerinin; bir ters eğik çizgi içeren bir etikete, a etiketine ve $& düz etiketine dönüştüğünü doğrular. Özgün boşlukları ve kaçış yazımını aynen yeniden üretmeniz gerekiyorsa çözümlenmiş dizelerden oluşan bir dizi yeterli olmaz; gösterimin kaynak metni veya belirteç konumlarını da saklaması gerekir.

Tüm metinde değiştirme işlemi neden sonucu değiştirir

Her açılış parantezini bir köşeli açma paranteziyle ve her kapanış parantezini başka bir köşeli parantezle değiştirmek bu dilbilgisini uygulamaz. Bu işlem "blue (round)" içindeki parantezleri de değiştirir; ayrıca tırnaksız etiketleri ve eksik öğe ayırıcılarını çözümsüz bırakır. Bu sonuç, doğrudan elle yazılmış örnekten çıkar.

Değiştirme işleminin bir başka ilgili ayrıntısı da şudur: dize değiştirme bağımsız değişkeni, eşleşen metni ekleyen $& gibi özel dizileri yorumlar. Değiştirme işlevi, döndürdüğü metni bu ikame adımına uğratmadan verir. ECMAScript belirtimi: replaceAll ve değiştirme ikamesi

Bu bağımsız örnek farkı gösterir:

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

Hazırlık sırasında her iki sonuç da doğrulandı. Ağaç ayrıştırıcısı değiştirme işleminin anlamlarını bütünüyle atlar: çözümlenmiş dizeleri doğrudan dizilere ekler. Daha sonra bir etiket yeniden adlandırma adımı eklerseniz, değiştirme işlemlerini ayrıştırılmış etiketlerde yapın ve değiştirme metninin yorumlanıp yorumlanmayacağını açıkça seçin.

Ağacı yeniden kullanmadan önce beklenmeyen çıktıyı tanılayın

Dahil edilen denetimler kısa bir karar desteği işlevi görür. Boş girdiyi, boş grupları, bitişik grupları ve etiketleri, tırnak içindeki boşlukları, yalnızca dizelerden oluşan değerleri, kaçışları ve hatalı söz dizimini kapsar. Taslağınızla tanımlanan dilbilgisi arasındaki uyuşmazlığı bulmak için aşağıdaki belirtileri kullanın.

Belirti Açıklama ve sonraki adım
Sonuçta fazladan bir dış dizi var Kök, belgeyi temsil eder. Sonraki adımınız özellikle tek bir üst düzey grup gerektirmedikçe bunu koruyun.
Bir ifade birkaç dizeye dönüşüyor Boşluk, tırnaksız etiketleri ayırır. İfadeyi çift tırnak içine alın.
Virgül bir etikette kalıyor Burada virgüller sıradan karakterlerdir. Ayrı etiketler arasında boşluk kullanın.
Parantezler beklenmedik biçimde iç içe dizilere dönüşüyor Düz parantezleri çift tırnak içine alınmış bir etiketin içinde kullanın.
Unexpected closing parenthesis Bir kapanış parantezinin eşleşen açılış grubu yoktur. Bildirilen konumu inceleyin.
Unclosed opening parenthesis Girdinin sonunda bir grup açık kalmıştır. Konum, kapanmamış en içteki açılış parantezini belirtir.
Invalid JSON string Tırnaklı belirteci geçersiz bir kaçış veya kaçışsız bir denetim karakteri açısından inceleyin.
Tırnakla ilgili ayırıcı hatası Yan yana gelen etiketleri boşlukla ayırın veya amaçlanan etiketin tamamını tek bir tırnaklı dizenin içine alın.

Bu uygulamadaki ofsetler sıfırdan başlar ve JavaScript dize indekslemesiyle aynı şekilde UTF-16 kod birimlerini sayar. Bunlar satır numarası veya görünen karakterlerin sayısı değildir. Tırnaklı belirteç doğrulama hatalarında bildirilen konum belirtecin başlangıcını gösterir; bu nedenle tırnaklı etiketin tamamını inceleyin.

Hatalı girdi, işlev dönmeden önce bir istisna fırlatır. Tarama sırasında diziler dahili olarak oluşturulsa da çağıranlar kısmi bir ağaç almaz. Ayrıştırıcı, özyinelemeli işlev çağrıları yerine açık bir yığın kullanır; ancak örnek, desteklenen belge boyutu veya iç içe geçme derinliği için herhangi bir üst sınır belirlemez.

Küçük bir masaüstü taslağı için izlenecek pratik sıra şöyledir: dilbilgisi üzerinde anlaşın, değiştirilmemiş örneği ve denetimleri çalıştırın, ardından kendi yazdığınız metni yerleştirin. Sonucu yeniden kullanmadan önce tırnaklı etiketleri, boş grupları ve belge kökünü doğrulayın; bu üç ayrıntı, dizi ağacının amaçladığınız yapıyı koruyup korumadığını belirler.

Kaynaklar