מעבר לביטויים רגולריים: שיפור ניתוח ערכי ה-CSS בכלי הפיתוח ל-Chrome

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

האם שמת לב למאפייני ה-CSS בכלי הפיתוח ל-Chrome הכרטיסייה סגנונות נראית קצת יותר מלוטשת לאחרונה? העדכונים האלה, שהושקו בין הגרסאות 121 ל-128 של Chrome, הם תוצאה של שיפור משמעותי באופן שבו אנחנו מנתחים ומציגים ערכי CSS. במאמר הזה נסביר על הפרטים הטכניים של השינוי הזה – איך עוברים ממערכת התאמה של ביטויים רגולריים למנתח מתקדם יותר.

נשווה בין כלי הפיתוח הנוכחיים לגרסה הקודמת:

עליון: זה הגרסה העדכנית ביותר של Chrome, למטה: Chrome 121.

הבדל גדול, נכון? הנה פירוט של השיפורים העיקריים:

  • color-mix תצוגה מקדימה נוחה שמציגה באופן חזותי את שני הארגומנטים של הצבעים בפונקציה color-mix.
  • pink תצוגה מקדימה של צבע שניתנת ללחיצה, עבור הצבע בעל השם pink. אפשר ללחוץ עליו כדי לפתוח בוחר צבעים ולשנות אותו בקלות.
  • var(--undefined, [fallback value]) טיפול משופר במשתנים לא מוגדרים, כאשר המשתנה הלא מוגדר מופיע באפור וערך הגיבוי הפעיל (במקרה הזה, צבע HSL) מוצג עם תצוגה מקדימה של צבע שאפשר ללחוץ עליה.
  • hsl(…): תצוגה מקדימה נוספת של צבעים שאפשר ללחוץ עליה, לפונקציית הצבע hsl, שמספקת גישה מהירה לבורר הצבעים.
  • 177deg: שעון זוויתי שניתן ללחוץ עליו, שמאפשר לגרור ולשנות את ערך הזווית באופן אינטראקטיבי.
  • var(--saturation, …): קישור שניתן ללחוץ עליו להגדרת הנכס המותאם אישית, כדי שתוכלו לעבור בקלות להצהרה הרלוונטית.

ההבדל מרשים. לשם כך, נאלצנו ללמד את כלי הפיתוח להבין ערכים של נכסים ב-CSS הרבה יותר טוב מבעבר.

התצוגות המקדימות האלה כבר לא היו זמינות?

סמלי התצוגה המקדימה האלה נראים מוכרים, אבל הם לא תמיד הוצגו באופן עקבי, במיוחד בתחביר CSS מורכב כמו בדוגמה שלמעלה. גם במקרים שבהם הן פעלו, היה צורך במאמץ משמעותי כדי שהן יפעלו באופן תקין.

הסיבה לכך היא שהמערכת לניתוח הערכים גדלה באופן אורגני מאז הימים הראשונים של השימוש בכלי הפיתוח. עם זאת, השירות לא הצליח לעמוד בקצב של התכונות החדשות והמדהימות האחרונות שאנחנו מקבלים משירות CSS, והעלייה במקביל במורכבות השפה. המערכת דרשה עיצוב מחדש כדי לעמוד באבולוציה וזה בדיוק מה שעשינו!

איך מתבצע עיבוד הערכים של נכסי CSS

בכלי הפיתוח, התהליך של העיבוד והעיצוב של הצהרות נכסים בכרטיסייה סגנונות מחולק לשני שלבים נפרדים:

  1. ניתוח מבני. השלב הראשוני הזה מנתח את הצהרת הנכס כדי לזהות את הרכיבים הבסיסיים שלה ואת הקשרים ביניהם. לדוגמה, לפי ההצהרה border: 1px solid red, הוא יזהה את 1px כאורך, את solid כמחרוזת ואת red כצבע.
  2. רינדור. על בסיס הניתוח המבני, שלב העיבוד הופך רכיבים אלה לייצוג HTML. התכונה הזו משפרת את הטקסט של המאפיין שמוצג באמצעות רכיבים אינטראקטיביים ורמזים ויזואליים. לדוגמה, ערך הצבע red מוצג עם סמל צבע שניתן ללחוץ עליו, שכאשר לוחצים עליו הוא מציג בוחר צבעים שמאפשר שינוי קל.

ביטויים רגולריים

בעבר הסתמכנו על ביטויים רגולריים (regex) כדי לנתח את ערכי הנכסים לניתוח מבני. הכנו רשימה של ביטויים רגולריים שמתאימים לקטעי הערכים של הנכסים שחשבנו עליהם. לדוגמה, היו ביטויים שהתאימו צבעים, אורכים, זוויות, ביטויי משנה מורכבים יותר כמו קריאות לפונקציות var וכן הלאה. סרקנו את הטקסט משמאל לימין כדי לבצע ניתוח ערכים, וממשיכים לחפש את הביטוי הראשון מהרשימה שתואם לחלק הבא בטקסט.

בדרך כלל הדבר עבד מצוין, אבל מספר המקרים שבהם הוא לא גדל. לאורך השנים קיבלנו מספר רב של דוחות על באגים, כאשר ההתאמה לא בדיוק הצליחה. אחרי שתיקנו את הבעיות – חלק מהתיקונים פשוטים וחלקם די מפורטים – נאלצנו לחשוב מחדש על הגישה שלנו כדי לשמור על החוב הטכני שלנו. בואו נבחן כמה מהבעיות!

color-mix() תואם

הביטוי הרגולרי שבו השתמשנו לפונקציה color-mix() היה כך:

/color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g

שתואם לתחביר שלו:

color-mix(<color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2})

נסו להריץ את הדוגמה הבאה כדי להציג את ההתאמות באופן חזותי.

const re = /color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g;

// it works - simpler example
const simpler = re.exec('color-mix(in srgb, pink, hsl(127deg 100% 50%))');
console.table(simpler.groups);

re.exec('');

// it doesn't work - complex example
const complex = re.exec('color-mix(in srgb, pink, var(--undefined, hsl(127deg var(--saturation, 100%) 50%)))');
console.table(complex.groups);

תוצאת התאמה עבור הפונקציה &#39;מיקס צבעים&#39;.

הדוגמה הפשוטה יותר עובדת כמו שצריך. עם זאת, בדוגמה המורכבת יותר, ההתאמה <firstColor> היא hsl(177deg var(--saturation ו-<secondColor> היא 100%) 50%)), והיא חסרת משמעות לחלוטין.

ידענו שזו בעיה. אחרי הכול, CSS כשפה רשמית הוא לא רגיל, לכן כבר הוספנו טיפול מיוחד כדי להתמודד עם ארגומנטים מורכבים יותר של פונקציות, כמו פונקציות var. עם זאת, כמו שאפשר לראות בצילום המסך הראשון, השיטה הזו עדיין לא עבדה בכל המקרים.

tan() תואם

אחד הבאגים הדיווחים הכי מצחיקים היה לגבי הפונקציה tan() הטריגונומטרית . הביטוי הרגולרי שבו השתמשנו להתאמת הצבעים כולל תת-ביטוי \b[a-zA-Z]+\b(?!-) להתאמה לצבעים, כמו מילת המפתח red. אחר כך בדקנו אם החלק התואם הוא בעצם צבע בעל שם, ונחשו מה, tan הוא גם צבע בעל שם! לכן פרשנו בטעות tan() ביטויים כצבעים.

var() תואם

נבחן דוגמה נוספת, פונקציות var() עם חלופה שמכילה הפניות אחרות של var(): var(--non-existent, var(--margin-vertical)).

נשמח לקבל התאמה לביטוי הרגולרי שלנו עבור var(). מלבד זאת, היא תפסיק את ההתאמה בסוגריים הסוגרים הראשונים. אז הטקסט שלמעלה מותאם כ-var(--non-existent, var(--margin-vertical). זוהי מגבלה של ספר לימוד בנושא התאמה של ביטויים רגולריים. שפות שדורשות התאמה בסוגריים הן לא רגילות.

מעבר למנתח CSS

כשניתוח טקסט באמצעות ביטויים רגולריים מפסיק לעבוד (כי השפה שמנותחת אינה רגולרית), יש שלב קנוני הבא: שימוש במנתח לבדיקת דקדוק מסוג גבוה יותר. ב-CSS, המשמעות היא כלי לניתוח שפות ללא הקשר. למעשה, מערכת ניתוח כזו כבר הייתה קיימת ב-codebase של DevTools: Lezer של CodeMirror, שהוא הבסיס להדגשת תחביר ב-CodeMirror, של העורך שמופיע בחלונית מקורות. כלי ה-CSS של Lezer אפשר לנו ליצור עצי תחביר (לא מופשטים) בשביל כללי CSS ומוכן לשימוש. ניצחון.

עץ תחביר לערך הנכס `hsl(177deg var(--saturation, 100%) 50%)&#39;. זוהי גרסה פשוטה יותר של התוצאה שהופקה על ידי מנתח Lezer, ללא צמתים תחביריים לחלוטין עבור פסיקים וסוגריים.

מלבד זאת, לא מצאנו כל אפשרות לעבור מהתאמה שמבוססת על ביטויים רגולריים (regex) להתאמה שמבוססת על מנתח נתונים באופן ישיר: שתי הגישות פועלות מכיוונים מנוגדים. כשמתאימים בין קטעי ערכים לביטויים רגולריים, כלי הפיתוח סורקים את הקלט משמאל לימין, ומנסים שוב ושוב למצוא את ההתאמה המוקדמת ביותר מתוך רשימה ממוינת של תבניות. בעץ תחביר, ההתאמה תתחיל מלמטה למעלה, לדוגמה, ניתוח הארגומנטים של קריאה ראשונה לפני הניסיון להתאים את הקריאה לפונקציה. חשוב על הביטוי כהערכה של ביטוי אריתמטי, שבו קודם תחשבו ביטויים בסוגריים, לאחר מכן אופרטורים של הכפלה ואז אופרטורים של חיבור. במסגרת הזו, ההתאמה שמבוססת על ביטויים רגולריים (regex) מקבילה להערכת הביטוי האריתמטי משמאל לימין. לא באמת רצינו לשכתב את כל מערכת ההתאמה מההתחלה: היו 15 זוגות של התאמות וכלים של רינדור, עם אלפי שורות קוד, ולכן לא סביר שנוכל לשלוח את המוצר באותה אבן דרך.

לכן חשבנו על פתרון שמאפשר לנו לבצע שינויים מצטברים, שנתאר אותם בפירוט בהמשך. בקצרה, שמרנו על הגישה הדו-שלבית, אבל בשלב הראשון אנחנו מנסים להתאים ביטויי משנה מלמטה למעלה (וכך המערכת מקוטעת את הרצף של הביטויים הרגולריים), ובשלב השני אנחנו מעבדים את התוכן מלמעלה למטה. בשני השלבים, יכולנו להשתמש בהתאמות ובעיבודים הקיימים שמבוססים על ביטויים רגולריים (regex), כמעט ללא שינוי, ולכן הצלחנו להעביר אותם אחד אחרי השני.

שלב 1: התאמה מלמטה למעלה

השלב הראשון הוא, פחות או יותר, בדיוק מה שמופיע בעטיפה של הסרטון. אנחנו חוצים את העץ לפי הסדר מלמטה עד למעלה ומנסים להתאים ביטויי משנה בכל צומת עץ תחביר שבו אנחנו מבקרים. כדי לבצע התאמה לתת-ביטוי ספציפי, כלי ההתאמה יכול להשתמש בביטוי רגולרי (regex) בדיוק כמו במערכת הקיימת. החל מגרסה 128, אנחנו עדיין עושים זאת במספר מקרים, לדוגמה, להתאמות באורכים. לחלופין, כלי תואם יכול לנתח את המבנה של עץ המשנה שעבר תהליך רוט (Root) בצומת הנוכחי. כך הוא יכול לקלוט שגיאות תחביר ולתעד מידע מבני בו-זמנית.

עיינו בדוגמה לעץ התחביר שלמעלה:

שלב 1: התאמה מלמטה למעלה בעץ התחביר.

בעץ הזה, ההתאמות שלנו יחולו בסדר הבא:

  1. hsl(177degvar(--saturation, 100%) 50%): תחילה אנחנו מגלים את הארגומנט הראשון של הפעלת הפונקציה hsl, זווית הגוון. אנחנו מתאימים אותה באמצעות התאמת זווית, כדי שנוכל לקשט את ערך הזווית בסמל הזווית.
  2. hsl(177degvar(--saturation, 100%)50%): שנית, אנחנו מגלים את הבקשה להפעלת הפונקציה var באמצעות פעולת כוונון משתנים. בשיחות כאלה, אנחנו רוצים בעיקר שני דברים:
    • אפשר לחפש את ההצהרה של המשתנה ולחשב את הערך שלו, ולהוסיף קישור וחלון קופץ לשם המשתנה כדי להתחבר אליו, בהתאמה.
    • אם הערך שחושב הוא צבע, אפשר לקשט את השיחה בסמל של צבע. למעשה יש דבר שלישי, אבל נדבר על זה מאוחר יותר.
  3. hsl(177deg var(--saturation, 100%) 50%): לבסוף, אנחנו מתאימים את ביטוי הקריאה לפונקציה hsl כדי שנוכל לקשט אותו עם סמל הצבע.

בנוסף לחיפוש ביטויי משנה שאנחנו רוצים לקשט, יש למעשה תכונה נוספת שאנחנו מפעילים כחלק מתהליך ההתאמה. שימו לב שבשלב 2, אמרנו שאנחנו מחפשים את הערך המחושב עבור שם משתנה. למעשה, אנחנו לוקחים את התהליך צעד אחד קדימה ומפיצים את התוצאות לעץ. לא רק עבור המשתנה, אלא גם עבור הערך החלופי! מובטח שבזמן ביקור בצומת של פונקציה var, הצאצאים שלו כבר ביקרו מראש, כך שאנחנו כבר יודעים מה התוצאות של כל פונקציות var שעשויות להופיע בערך החלופי. לכן אנחנו יכולים להחליף בקלות ובזול פונקציות של var בתוצאות בזמן אמת, וכך לענות באופן טריוויאלי על שאלות כמו "האם התוצאה של var נקראת צבע?", כמו שעשינו בשלב 2.

שלב 2: רינדור מלמעלה למטה

בשלב השני, אנחנו הופכים את הכיוון. על סמך תוצאות ההתאמה משלב 1, אנחנו מעבדים את העץ ל-HTML על ידי מעבר שלו לפי הסדר, מלמעלה למטה. עבור כל צומת שבו ביקרתם, אנחנו בודקים אם הוא תואם, ואם כן, קוראים למעבד התואם של ההתאמה. אין צורך בטיפול מיוחד בצמתים שמכילים רק טקסט (כגון NumberLiteral "50%"), על-ידי הכללת ברירת מחדל של התאמות ורינדור עבור צמתים של טקסט. כלי הרינדור פשוט יוצרים פלט של צומתי HTML, שבשילוב שלהם נוצר ייצוג של ערך המאפיין, כולל העיצובים שלו.

שלב 2: עיבוד מלמעלה למטה בעץ התחביר.

בעץ לדוגמה, זהו הסדר שבו מוצג ערך המאפיין:

  1. כניסה לפונקציה hsl. התוצאה תאמה, אז צריך לקרוא לרינדור של פונקציית הצבע. היא עושה שתי פעולות:
    • מחשבת את ערך הצבע בפועל באמצעות מנגנון החלפת המצב של כל ארגומנט var, ואז משרטטת סמל של צבע.
    • מתבצע עיבוד רקורסיבי של הצאצאים של CallExpression. התכונה הזו מטפלת באופן אוטומטי ברינדור של שם הפונקציה, הסוגריים והפסיקים, שהם רק טקסט.
  2. נכנסים לארגומנט הראשון של הקריאה hsl. הוא התאים, ולכן קראתם למעבד הזווית, שצייר את סמל הזווית ואת הטקסט של הזווית.
  3. עוברים לארגומנט השני, שהוא הקריאה var. הוא התאים, לכן קראו לכלי הרינדור שמפיק את הפלט הבא:
    • הטקסט var( בהתחלה.
    • שם המשתנה מקושט בו בקישור להגדרת המשתנה או בטקסט אפור כדי לציין שהוא לא מוגדר. בנוסף, הוא מוסיף למשתנה חלון קופץ כדי להציג מידע על הערך שלו.
    • הפסיק ולאחר מכן מעבד באופן רקורסיבי את הערך החלופי.
    • סוגר סוגר.
  4. כניסה לארגומנט האחרון של הקריאה hsl. הוא לא תאם, לכן פשוט פלט את תוכן הטקסט שלו.

שמתם לב שבאלגוריתם הזה, רינדור קובע באופן מלא את אופן העיבוד של הצאצאים של צומת תואם? עיבוד רקורסיבי של הילדים הוא יזום. הטריק הזה הוא מה שאפשרה העברה חכמה מרינדור שמבוסס על ביטויים רגולריים (regex) לעיבוד מבוסס-עצים בתחביר. לצמתים שתואמים להתאמה לביטוי רגולרי מדור קודם, ניתן להשתמש בכלי לרינדור המתאים בצורתו המקורית. במונחים של עץ תחביר, האחריות לעיבוד עץ המשנה כולו והתוצאה שלו (צומת HTML) תחובר בצורה חלקה לתהליך העיבוד שמסביב. זה אפשר לנו לאפשר ניוד של התאמות ועיבודים בזוגות, ולהחליף אותם אחד אחרי השני.

תכונה מגניבה נוספת של רינדור השולטים ברינדור של צאצאי הצומת שנמצאה להם התאמה היא, שהיא מספקת לנו את היכולת להסביר את יחסי התלות בין הסמלים שאנחנו מוסיפים. בדוגמה שלמעלה, הצבע שנוצר על ידי הפונקציה hsl תלוי כמובן בערך הגוון שלה. המשמעות היא שהצבע המוצג על ידי סמל הצבע תלוי בזווית שמוצגת על ידי סמל הזווית. אם המשתמש פותח את הכלי לעריכת זווית דרך הסמל הזה ומשנה את הזווית, עכשיו אנחנו יכולים לעדכן את הצבע של הסמל בזמן אמת בזמן אמת:

כמו שאפשר לראות בדוגמה שלמעלה, אנחנו משתמשים במנגנון הזה גם לצמדי סמלים אחרים, כמו למשל לפונקציות color-mix() ולשני ערוצי הצבעים שלו, או לפונקציות var שמחזירות צבע למצב הראשוני שלו.

השפעה על הביצועים

כשניסינו לפתור את הבעיה לשיפור האמינות ותיקון הבעיות הקיימות, ציפינו לרגרסיה מסוימת של הביצועים כי התחלנו להפעיל מנתח מתקדם. כדי לבדוק את זה, יצרנו נקודת השוואה שמציגה כ-3,500 הצהרות נכסים, ויצרנו פרופיל גם לגרסה שמבוססת על ביטוי רגולרי וגם לגרסה שמבוססת על ניתוח, עם ויסות נתונים (throttle) פי 6 במכונה M1.

כפי שציפינו, הגישה שמבוססת על הניתוח הייתה איטית ב-27% מהגישה שמבוססת על ביטויים רגולריים במקרה הזה. במסגרת הגישה שמבוססת על ביטוי רגולרי (regex) נמשכה 11 שניות כדי לעבד את המידע, ולגישה המבוססת על מנתח נתונים נמשכה 15 שניות.

בהתחשב בניצחונות שאנחנו מקבלים מהגישה החדשה, החלטנו להמשיך בתהליך.

אישורים

תודה רבה לסופיה אמליאנובה וג'סלין יין על העזרה החשובה שלהן בעריכת הפוסט הזה.

הורדת הערוצים של התצוגה המקדימה

כדאי להשתמש ב-Chrome Canary, Dev או בטא כדפדפן הפיתוח שמוגדר כברירת מחדל. הערוצים לתצוגה מקדימה אלה מעניקים לך גישה לתכונות החדשות של כלי הפיתוח, בודקים ממשקי API מתקדמים של פלטפורמות אינטרנט ומוצאים בעיות באתר שלך לפני שהמשתמשים עושים זאת.

יצירת קשר עם הצוות של כלי הפיתוח ל-Chrome

אפשר להשתמש באפשרויות הבאות כדי לדון בתכונות ובשינויים החדשים בפוסט, או בכל נושא אחר שקשור לכלי פיתוח.

  • אפשר לשלוח לנו הצעה או משוב דרך crbug.com.
  • כדי לדווח על בעיה בכלי הפיתוח, לוחצים על אפשרויות נוספות   עוד > עזרה > דיווח על בעיות בכלי הפיתוח ב'כלי פיתוח'.
  • שליחת ציוץ אל @ChromeDevTools.
  • נשמח לשמוע מה חדש בסרטונים ב-YouTube של כלי הפיתוח או בסרטונים ב-YouTube שקשורים לכלי פיתוח.