لتطبيق خريطة استبدال من دون تغيير النص المُدرج حديثًا، ابحث في السلسلة الأصلية فقط وأنشئ نتيجة منفصلة. يستخدم هذا الدليل JavaScript في Safari على جهاز Mac لتحديث مقطع قصير باستبدالات حرفية يمكن توقعها.

لنفترض أن مهمة تحرير على جهاز كمبيوتر مكتبي تتطلب تغيير blue إلى green وgreen إلى gold. ينبغي أن يتحول blue الأصلي إلى green مرة واحدة. وينبغي أن يتحول green الأصلي إلى gold. يعالج النهج التالي هذا التمييز والعبارات المتداخلة والنص المقتبس وقيم الاستبدال التي تحتوي على علامات ترقيم.

لماذا تغيّر حلقة الاستبدال النص مرتين؟

إن الحلقة التي تطبق كل قاعدة على ناتج القاعدة السابقة تؤدي إلى سلسلة من الاستبدالات. مع قاعدتي اللون هاتين، تُدرج العملية الأولى green؛ ثم تعثر العملية الثانية على هذه الكلمة المُدرجة حديثًا وتغيّرها إلى gold. قد يحل تغيير ترتيب القواعد مثالًا واحدًا، لكنه لا يحدد سلوكًا موثوقًا لكل الخرائط.

تعيد replace() في JavaScript سلسلة جديدة وتترك مُدخلها من دون تغيير. ويحدث التسلسل عندما تتلقى الاستدعاءات اللاحقة السلسلة المُعادة. لذا تتطلب المهمة قاعدة واضحة: يجب أن يأتي كل تطابق من المُدخل الأصلي. راجع توثيق String.prototype.replace() على MDN.

بالنسبة إلى المُدخل المكتوب The blue cup is beside the green cup.، تكون النتيجة المقصودة The green cup is beside the gold cup. يتغير لون الكلمتين الأصليتين، بينما تبقى green المُدرجة كما هي. هذا هو التحويل الذي ينفذه التطبيق أدناه.

ماذا ينبغي أن يحدث عندما تتداخل مفاتيح الاستبدال؟

يترك منع التسلسل قرارًا آخر: أي مفتاح يفوز عندما تتطابق عدة مفاتيح مع النص المصدر نفسه؟ في مهمة التحرير هذه، استخدم السياسة الصريحة التالية. هذه خيارات تصميم لهذا المثال، وليست قواعد تلقائية توفرها JavaScript.

  • افحص المُدخل الأصلي من اليسار إلى اليمين.
  • عند كل موضع، اختر أطول مفتاح مطابق.
  • استهلك التطابق المصدري كاملًا قبل المتابعة.
  • أدرج قيمته المقابلة حرفيًا، من دون البحث داخلها.
  • انسخ النص غير المطابق كما هو.
  • طابق حالة الأحرف والمسافات بدقة.

على سبيل المثال، إذا كان كل من blue وblue vase مفتاحًا، تفوز العبارة blue vase عند موضع البداية نفسه. يكون للتطابق الذي يبدأ في موضع أسبق أولوية على تطابق أطول يبدأ في موضع لاحق. وبعد استهلاك مقطع من المصدر، لا يمكن لتطابق متداخل استخدامه مجددًا.

تناسب هذه السياسة تعديلات العبارات الحرفية ضمن مجموعة صغيرة: إذ تعطي العبارات المحددة أولوية على بادئاتها الأقصر. كما تجعل الناتج مستقلًا عن ترتيب إدخال المفاتيح المختلفة في الطول. إذا كانت مهمتك تتطلب سياسة مختلفة للتداخل، فحددها قبل تطبيق الخريطة.

كيف يمكنك تشغيل التحويل على جهاز Mac؟

يوفر Safari وحدة تحكم JavaScript، لذا لا يحتاج هذا المثال إلى تثبيت تطبيق إضافي. فعّل Web Inspector من خلال Safari → Settings… → Advanced → Show features for web developers. ثم افتح Develop → Show Web Inspector، أو اضغط Option-Command-I. تتبع هذه الخطوات وثائق WebKit لإعداد Web Inspector.

افتح صفحة فارغة بإدخال about:blank في شريط العنوان في Safari، ثم افتح المفتش واختر علامة التبويب Console. الصق البرنامج الكامل أدناه في موجّه وحدة التحكم ونفّذه. توضح وثائق WebKit أن الموجّه يقيّم JavaScript ويعرض النتائج في وحدة التحكم. راجع واجهة Web Inspector.

المثال برنامج متصفح كامل يستخدم نصًا اصطناعيًا ولا يتضمن أي استيرادات. تحصر دالته الخارجية التصريحات ضمن نطاقها المحلي، ما يتيح لك لصق الكتلة وتشغيلها مجددًا بعد تحرير العينة.

(() => {
  function applyReplacementMap(input, replacements) {
    if (typeof input !== "string" || !(replacements instanceof Map)) {
      throw new TypeError("Use a string and a Map.");
    }

    const entries = [...replacements];

    for (const [key, value] of entries) {
      if (
        typeof key !== "string" ||
        key.length === 0 ||
        typeof value !== "string"
      ) {
        throw new TypeError(
          "Keys must be nonempty strings; values must be strings."
        );
      }
    }

    entries.sort((a, b) => b[0].length - a[0].length);

    const output = [];
    let position = 0;

    while (position < input.length) {
      const entry = entries.find(([key]) =>
        input.startsWith(key, position)
      );

      if (entry) {
        output.push(entry[1]);
        position += entry[0].length;
      } else {
        output.push(input.slice(position, position + 1));
        position += 1;
      }
    }

    return output.join("");
  }

  const replacements = new Map([
    ["blue", "green"],
    ["green", "gold"],
    ["blue vase", "green bowl"],
    ["[tag]", "$&"],
    ["(draft)", ""]
  ]);

  const input =
    'The blue vase is beside the green cup. Label: "[tag]". Note: (draft)';

  const result = applyReplacementMap(input, replacements);
  console.log(result);
})();

يظهر النص المسجّل أدناه. تبقى مسافة واحدة في النهاية بعد Note: لأن القاعدة تزيل (draft) فقط، وتحافظ بذلك على المسافة السابقة.

The green bowl is beside the gold cup. Label: \"$&\". Note: 

لتكييف المثال، حرّر input وأزواج القيم داخل new Map(...). يحتوي كل زوج على سلسلة المصدر المطابقة تمامًا، تليها قيمة الاستبدال. استخدم مدخلًا واحدًا لكل مفتاح: تحتفظ خرائط JavaScript بمفاتيح فريدة، لذا فإن تكرار مفتاح يحدّث قيمته بدلًا من إنشاء قاعدة أخرى منافسة. راجع مرجع Map على MDN.

لماذا يبقى النص المُدرج كما هو؟

لا يتقدم المؤشر المسمى position إلا عبر input. يستخدم التطابق input.startsWith(key, position)، الذي يتحقق من وجود سلسلة عند موضع محدد ويميز بين حالة الأحرف، كما هو موضح في مرجع startsWith() على MDN. ولا تُستخدم مصفوفة الناتج هدفًا للبحث مطلقًا.

عندما لا تطابق أي قاعدة، تنسخ slice(position, position + 1) وحدة المصدر التالية. فهرس الشريحة الثاني حصري، والتقطيع لا يغيّر السلسلة الأصلية. تتيح هذه الدلالات الموثقة للدالة الحفاظ على المحتوى غير المطابق مع إنشاء نتيجتها بشكل منفصل. راجع مرجع slice() على MDN.

يُدرج المثال green bowl بدلًا من blue vase. ورغم أن العبارة المُدرجة تحتوي على مفتاح آخر، وهو green، تكون الدالة قد تجاوزت العبارة الأصلية بالفعل. فهي لا تفحص green bowl مطلقًا. أما green الأصلية التي ترد لاحقًا في المُدخل فتتغير إلى gold.

كيف تعمل القيم الحرفية والحذف؟

يلحق هذا التطبيق قيم الاستبدال مباشرة. لذلك تبقى $& كما هي، وتُعامل الأقواس المربعة أو الدائرية في المفاتيح كأحرف عادية. تحذف سلسلة استبدال فارغة مقطع المصدر المطابق. ولا يحدث في هذه الدالة أي استخدام للتعبيرات النمطية أو تفسير لقوالب الاستبدال.

تُرفض المفاتيح الفارغة لأنها لا تستهلك أي نص من المصدر؛ والسماح بها سيمنع الفرع المطابق من التقدم. كما تُرفض القيم غير النصية قبل بدء التحويل، ما يجعل القيمة الخاطئة ظاهرة بدلًا من تحويلها أو حذفها بصمت. تعيد الخريطة الفارغة المُدخل كما هو، ويعيد المُدخل الفارغ سلسلة فارغة.

كيف ينبغي أن تتحقق من النتيجة قبل نسخها؟

استخدم حالات صغيرة تميّز السلوك الذي تريده. النتائج المتوقعة التالية مستمدة من التطبيق أعلاه. وما لم يُذكر خلاف ذلك، فهي تستخدم خريطته النموذجية.

التحقق المُدخل النتيجة المتوقعة
بقاء النص المُدرج كما هو blue green green gold
فوز العبارة الأطول blue vase green bowl
بقاء علامات الترقيم في الاستبدال حرفية [tag] $&
مراعاة حالة الأحرف عند المطابقة Blue Blue
المطابقة مع أجزاء من الكلمات blueprint greenprint
بقاء علامتي الاقتباس \"blue\" \"green\"
خريطة فارغة blue blue

تم التحقق من دالة الاستبدال وهذه السلوكيات في Node.js 20.19.2، إلى جانب الحذف والمُدخل الفارغ والتطابقات المتداخلة ورفض الخرائط غير الصالحة. يثبت ذلك عمل تحويل JavaScript في بيئة التشغيل هذه؛ أما خطوات واجهة Safari فمأخوذة من وثائق WebKit.

انتبه خصوصًا إلى التحقق من المطابقة الجزئية. لا تستنتج هذه الدالة حدود الكلمات ولا تصلح القواعد النحوية. يمكن للمفتاح أن يطابق جزءًا من كلمة أطول، وقد يؤدي حذف نص إلى ترك مسافات زائدة. في مهمة تحرير العبارات، افحص الأحرف المحيطة واجعل الخريطة محددة بقدر ما يتطلبه التغيير المقصود.

ميّز أيضًا بين التطبيق مرة واحدة وتكراره. يؤدي تشغيل الدالة مرة واحدة على النص الأصلي إلى تجنب التسلسل ضمن ذلك التشغيل. أما تمرير نتيجتها إلى الدالة مجددًا فيبدأ تحويلًا جديدًا، لذا يمكن للكلمات المُدرجة سابقًا أن تتطابق عندئذ. أبقِ input الأصلية مصدرًا عند إعادة تشغيل التعديل.

هل يمكن لاستدعاء واحد للدالة replace في JavaScript أن يفعل الشيء نفسه؟

نعم. بالنسبة إلى مجموعة صغيرة وثابتة من المفاتيح البسيطة، يمكن لتعبير نمطي عام واحد ودالة ردّ عند الاستبدال أن يوفرا السلوك نفسه من دون تسلسل. تجمع مواصفة ECMAScript التطابقات من السلسلة المصدر، ثم تنشئ نتيجة الاستبدال. راجع الخوارزمية المحددة لاستبدال التعبيرات النمطية.

يتعامل هذا المثال المستقل والكامل مع مفتاحي اللون الثابتين:

(() => {
  const replacements = new Map([
    ["blue", "green"],
    ["green", "gold"]
  ]);

  const input = "The blue cup is beside the green cup.";
  const result = input.replace(
    /blue|green/g,
    (word) => replacements.get(word)
  );

  console.log(result);
  // The green cup is beside the gold cup.
})();

تجعل الراية g التعبير يستبدل كل التطابقات. كما أن إعادة القيم من دالة ردّ تتجنب أنماط سلسلة الاستبدال مثل $&، التي تعني في غير ذلك السلسلة المطابقة. توثّق MDN السلوكين صراحة في مرجع replace().

عند إنشاء تعبير نمطي لخريطة متغيرة تحتوي على علامات ترقيم، تظهر متطلبات إضافية. يجب تهريب المفاتيح لتُعامل كأنماط حرفية، كما تتطلب البدائل المتداخلة تحديد أولوية صريحة. توفر JavaScript RegExp.escape() لإنشاء أنماط حرفية، لكن MDN تشير إلى أن المتصفحات الأقدم قد لا تدعمها. راجع توثيق RegExp.escape() على MDN.

في مهمة تحرير العبارات على جهاز كمبيوتر مكتبي هنا، يُبقي الماسح الحرفي هذه القرارات واضحة: يقرأ النص الأصلي، ويختار أطول مفتاح عند كل موضع، ويلحق كل قيمة استبدال كما هي. شغّله على المقطع الأصلي، وافحص الصياغة والمسافات الناتجة، ثم انسخ النص النهائي إلى مستندك.

المصادر