具有内置语法突出显示的字体
Font with Built-In Syntax Highlighting

原始链接: https://blog.glyphdrawing.club/font-with-built-in-syntax-highlighting/

手动编码网站语法突出显示问题:挑战在于使用 HTML、CSS 和 JavaScript 等常见技术在手动编码网站中实现语法突出显示,而不求助于改变文档对象模型的第三方工具、库或框架 (DOM)。 发现的一个特定障碍是显示代码片段时的语法突出显示,因为它通常需要使用复杂的语法突出显示库(如 Prism 或highlight.js)来扫描、剖析和使用专门的样式标签包装代码部分,以生成所需的突出显示。 由于目标是手动编码,因此避免添加此类外部资源至关重要。 作者提出了一种解决这一困境的新颖方法:利用 OpenType 功能来构建驻留在字体中的基本语法荧光笔,而不是作为附加脚本。 通过利用 OpenType Color (COLR) 表创建每个字体字符的颜色变化,并利用 Contextual Alternates 属性根据语法模式检测和交换特定字符串序列,作者成功构建了一个功能性语法荧光笔,该语法荧光笔仅通过 加载的字体。 下载字体 Monaspace Krypton-SyntaxHighlighter-Regular.woff2,并应用提供的 CSS 说明来激活语法荧光笔。 该技术值得注意的方面是其简单性(不需要 JavaScript)、快速安装、与各种平台(包括 InDesign)的兼容性、易用性(仅需要对现有字体文件进行基本更改)以及最小的依赖性。 潜在的缺点包括修改灵活性的限制(例如,调整配色方案或合并新语言)、范围有限(仅解决语法突出显示问题,无法处理更复杂的场景)、可用性受限(由于某些软件要求,特别是与字体相关的要求) 在非 Mac 设备上进行编辑),以及与专用语法荧光笔库相比的性能限制。 总的来说,虽然不能完全替代强大的语法荧光笔库,但这种基于字体的解决方案为更简单的情况提供了可行的替代方案。

本文讨论两个主要主题:创建个人博客和优化网站上语法高亮代码的渲染速度。 作者描述了他们自制的管理简单博客的解决方案,其中包括编写静态 HTML 文件、通过 PHP 脚本处理动态元素以及通过 JavaScript 添加交互性。 他们提到编辑文本区域时某些浏览器行为存在问题,例如自动大写或意外的突出显示行为。 关于语法高亮,作者指出目前有几种方法可用,包括图像、手动颜色编码、服务器端生成以及使用 JavaScript 的客户端生成。 然而,他们发现这些解决方案缓慢或困难。 然后,他们引入了一种新发明的字体的概念,该字体可以在其设计中自动处理语法突出显示。 虽然承认这种方法存在潜在的安全风险,但作者建议,与其他选项相比,这种方法可能会提供更快的渲染时间。 总体而言,作者更喜欢一次性计算数据并稍后对其进行样式化的策略。
相关文章

原文

Syntax Highlighting in Hand-Coded Websites

The problem

I have been trying to identify practical reasons why hand-coding websites with HTML and CSS is so hard (by hand-coding, I mean not relying on frameworks, generators or 3rd party scripts that modify the DOM).

Let's say, I want to make a blog. What are the actual things that prevent me from making—and maintaining—it by hand? What would it take to clear these roadblocks?

There are many, of course, but for a hand-coded programming oriented blog one of these roadblocks is syntax highlighting.

When I display snippets of code, I want to make the code easy to read and understand by highlighting it with colors. To do that, I would normally need to use a complex syntax highlighter library, like Prism or highlight.js. These scripts work by scanning and chopping up the code into small language-specific patterns, then wrapping each part in tags with special styling that creates the highlighted effect, and then injecting the resulting HTML back into the page.

But, I want to write code by hand. I don't want any external scripts to inject things I didn't write myself. Syntax highlighters also add to the overall complexity and bloat of each page, which I'm trying to avoid. I want to keep things as simple as possible.

Leveraging OpenType features to build a simple syntax highlighter inside the font

This lead me to think: could it be possible to build syntax highlighting directly into a font, skipping JavaScript altogether? Could I somehow leverage OpenType features, by creating colored glyphs with the COLR table, and identifying and substituting code syntax with contextual alternates?

<div class="spoilers">
  <strong>Yes, it's possible!</strong>
  <small>...to some extent =)</small>
</div>

The colors in the HTML snippet above comes from within the font itself, the code is plain text, and requires no JavaScript.

To achieve that, I modified an open source font Monaspace Krypton to include colored versions of each character, and then used OpenType contextual alternates to essentially find & replace specific strings of text based on HTML, CSS and JS syntax. The result is a simple syntax highlighter, built-in to the font itself.

If you want to try it yourself, download the font: MonaspaceKrypton-SyntaxHighlighter-Regular.woff2

And include the following bits of CSS:

@font-face {
  font-family: 'Monaspace';
  src: 
    url('/MonaspaceKrypton-SyntaxHighlighter-Regular.woff2') 
    format('woff2')
  ;
}
code {
  font-family: "Monaspace", monospace;
  font-feature-settings: "colr", "calt";
}

And that's it!

What are the Pros and Cons of this method?

This method opens up some interesting possibilities...

Pros

  1. Install is easy: Import the font and enable OpenType COLR (color) and CALT (contextual alternates) features.
  2. Works without JavaScript.
  3. Works without CSS themes.
  4. It's as fast as plain text, because it is plain text.
  5. Snippets of code can be put into <pre> and <code>, with no extra classes or <span>s.
  6. Clean HTML source code.
  7. Works everywhere that supports OpenType features, like InDesign.
  8. Doesn't require maintenance or updating.
  9. Works in <textarea> and <input>! Syntax highlighting inside <textarea> has been previously impossible, because textareas and inputs can only contain plain text. This is where the interesting possibilities lie. As a demo, I made this tiny HTML, CSS & JS sandbox, with native undo and redo, in a single, ~200 line web component.

tiny HTML & CSS sandbox =)

.container { height: 100%; width: 100%; display: grid; place-content: center; background: linear-gradient( lch(40 50 290), lch(60 50 60) 50%, lch(60 55 30) 70%, lch(20 20 290) 70.2%, lch(40 30 60) ) ; } p { font-size: clamp(16px, 2vw, 32px); color: lch(10 40 290); } document.querySelector('p').style.background = 'yellow';

Cons

There are, of course, many limitations to this method. It is not a direct replacement to the more robust syntax highligting libraries, but works well enough for simple needs.

  1. Making any modifications to the syntax highligher, like changing the color palette, adding more language supports or changing the look of the font, requires modifying the font file. This is inaccessible for most people. I used Glyphs to modify this font, but it only works on Mac, and costs ~300 euros.
  2. It only works where OpenType is supported. Fortunately, all major browsers support font-feature-settings: "colr", "calt";. However, eg. PowerPoint doesn't support OpenType (as far as I know).
  3. Finding patterns in text with OpenType contextual alternates is basic, and is no match for scripts that use regular expressions. For example, words within <p> tags that are JS keywords will be always highlighted: <p>if I throw this Object through the window, catch it, for else it’ll continue to Infinity & break</p>. It can't highlight comment blocks, or strings between quotes, etc.

How does it actually work?

Here's roughly how it works. There are two features in OpenType that make this possible: OpenType COLR table and contextual alternates.

OpenType COLR table

OpenType COLR table makes multi-colored fonts possible. There is a good guide on creating a color font using Glyphs.

I made a palette with 6 colors. I duplicated letters AZ, numbers 09 and the characters . # * - and _ four times. Each duplicated character is then suffixed with .alt1, .alt2, .alt3 or .alt4, and then assigned a color from the palette. For example, all .alt1 glyphs are this lime-yellow.

View from Glyps app. Each alternate character has a different color.

The two other colors I used for symbols &, | $ + = ~ [] () {} / ; : " and ', and are always in one color.

OpenType contextual alternates

The second required feature is OpenType contextual alternates. There is an indepth guide to advanced contextual alternates for Glyphs.

Contextual alternates makes characters "aware" of their adjacent characters. An example would be fonts that emulate continuous hand writing, where how a letter connects depends on which letter it connects to. There is a great article covering possible uses here.

The core feature of contextual alternates is substituting glyphs. Here is the simplified code for finding the JavaScript keyword if and substituting the letters i and f with their colored variant:

sub i' f by i.alt2;
sub i.alt2 f' by f.alt2;

In English:

  1. When i is followed by f, substitute the default i with an alternate (i.alt2).
  2. When i.alt2 is followed by f, substitute the default f with an alternate (f.alt2).
  3. As a result, every "if" in text gets substituted with if.

The substitution rules can get very long. Here's the substitution rule for the keyword localStorage:

lookup localStorageAttrCalt useExtension {
  ignore sub @AllLetters l' o c a l S t o r a g e;
  ignore sub l' o c a l S t o r a g e @AllLetters;
  sub l' o c a l S t o r a g e by l.alt;
  sub l.alt o' c a l S t o r a g e by o.alt;
  sub l.alt o.alt c' a l S t o r a g e by c.alt;
  sub l.alt o.alt c.alt a' l S t o r a g e by a.alt;
  sub l.alt o.alt c.alt a.alt l' S t o r a g e by l.alt;
  sub l.alt o.alt c.alt a.alt l.alt S' t o r a g e by S.alt;
  sub l.alt o.alt c.alt a.alt l.alt S.alt t' o r a g e by t.alt;
  sub l.alt o.alt c.alt a.alt l.alt S.alt t.alt o' r a g e by o.alt;
  sub l.alt o.alt c.alt a.alt l.alt S.alt t.alt o.alt r' a g e by r.alt;
  sub l.alt o.alt c.alt a.alt l.alt S.alt t.alt o.alt r.alt a' g e by a.alt;
  sub l.alt o.alt c.alt a.alt l.alt S.alt t.alt o.alt r.alt a.alt g' e by g.alt;
  sub l.alt o.alt c.alt a.alt l.alt S.alt t.alt o.alt r.alt a.alt g.alt e'  by e.alt;
} localStorageAttrCalt;

First two lines tell it to ignore strings like XlocalStorage or localStorages, but not if there's a period like localStorage.setItem(). The rest substitutes letters l o c a l S t o r a g e with alternates, one by one.

Identifying basic JavaScript keywords is fairly straightforward. The logic is the same for each keyword.

But for HTML and CSS... I had to get a bit more creative. There are simply too many keywords for both HTML and CSS combined. Making a separate rule for each keyword would inflate the file size.

Instead, I came up with this monstrosity. Here's how I find CSS value functions:

lookup CssParamCalt useExtension {
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' parenleft by @CssParamAlt4;
} CssParamCalt;

@CssParam is a custom OpenType glyph class I've set up. It includes the characters AZ, az, and -, which are all the possible characters used in CSS value function names. Because the longest possible CSS value function name is repeating-linear-gradient(), with 25 letters, the first line of the lookup starts with @CssParam repeated 25 times, followed by parenleft ((). This lookup will match any word up to 25 letters long, if it's immediately followed by an opening parenthesis. When a match occurs, it substitutes the matched text with its alternate color form (@CssParamAlt4).

This lookup works for both CSS and JavaScript. It will colorize standard CSS functions like rgb() as well as custom JavaScript functions like myFunction(). The result is a semi-flexible syntax highlighter that doesn't require complex parsing. I've repeated the same principle for finding HTML tags and attributes, and for CSS selectors and parameters.

End note

The full process is a little bit too convoluted to go into step-by-step, but if you're a type designer who wants to do this with their own font, don't hesitate to contact me. I'm also not an OpenType expert, so I'm sure the substitution logics could be improved upon. I'm open to sharing the modified source file to anyone interested. If you have any ideas, suggestions or feedback, let me know. You can reach me at [email protected].

联系我们 contact @ memedata.com