Font that can be used for validating baseline alignments. sajidanwar.com/misc/baseline-diagnostic-font/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement template generation for output files, including new index.html

+1037 -223
+2 -3
dist/LICENSE.md
··· 1 - 2 1 Copyright (c) 2026, Sajid Anwar. 3 2 4 3 This Font Software is licensed under the SIL Open Font License, Version 1.1. 5 4 This license is copied below, and is also available with a FAQ at: 6 - https\://openfontlicense.org 5 + https://openfontlicense.org 7 6   8 7 9 8 \---------------------------------------------------------------------- ··· 105 104 INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 106 105 DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 107 106 FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 108 - OTHER DEALINGS IN THE FONT SOFTWARE. 107 + OTHER DEALINGS IN THE FONT SOFTWARE.
+30 -32
dist/README.md
··· 1 - 2 1 # Baseline Diagnostic Font 3 2 4 3 ## Overview ··· 8 7 9 8 ## Baselines and Metrics 10 9 11 - | Baseline/Metric | Coordinate | BASE Value | OS/2 Value | hhea Value | 12 - |------------------------|------------|------------|----------------|------------| 13 - | ascent | 800 | | sTypoAscender | ascent | 14 - | ideographic-over | 750 | idtp | | | 15 - | hanging | 650 | hang | | | 16 - | ideographic-face-over | 650 | icft | | | 17 - | cap-height | 550 | | sCapHeight | | 18 - | math | 450 | math | | | 19 - | /central/ | 350 | | | | 20 - | /em-middle/ | 300 | | | | 21 - | x-height | 250 | | sxHeight | | 22 - | /x-middle/ | 150 | | | | 23 - | alphabetic | 50 | romn | | | 24 - | ideographic-face-under | 50 | icfb | | | 25 - | /zero/ | 0 | | | | 26 - | ideographic-under | -50 | ideo | | | 27 - | descent | -200 | | sTypoDescender | descent | 10 + | Baseline/Metric | Coordinate | BASE Value | OS/2 Value | hhea Value | 11 + |-----------------|------------|------------|------------|------------| 12 + | ascent | 800 | | sTypoAscender | ascent | 13 + | ideographic-over | 750 | idtp | | | 14 + | hanging | 650 | hang | | | 15 + | ideographic-face-over | 650 | icft | | | 16 + | cap-height | 550 | | sCapHeight | | 17 + | math | 450 | math | | | 18 + | /central/ | 350 | | | | 19 + | /em-middle/ | 300 | | | | 20 + | x-height | 250 | | sxHeight | | 21 + | /x-middle/ | 150 | | | | 22 + | alphabetic | 50 | romn | | | 23 + | ideographic-face-under | 50 | icfb | | | 24 + | /zero/ | 0 | | | | 25 + | ideographic-under | -50 | ideo | | | 26 + | descent | -200 | | sTypoDescender | descent | 28 27 29 - The `BaselineDiagnosticAlphabeticZero` variant is the same as `BaselineDiagnostic`, 30 - except the alphabetic baseline is at the common value of 0. This also 31 - results in the x-middle baseline being at 125. 28 + The `BaselineDiagnosticAlphabeticZero` variant is the same as `BaselineDiagnostic`, except `alphabetic` moves to 0; `x-middle` moves to 125. 32 29 33 30 ## Glyphs 34 31 ··· 43 40 Each baseline pair has two variants: a **layout** glyph (opaque filled rectangle 44 41 between the two baselines) and a **labeled** glyph (lines with text labels, like `X`). 45 42 46 - | Pair | Layout | Layout codepoint | Labeled | Labeled codepoint | 47 - |-------------------------------|--------|------------------|---------|-------------------| 48 - | X-height + Alphabetic | `x` | U+0078 | `χ` | U+03C7 | 49 - | Cap-height + Alphabetic | `B` | U+0042 | `β` | U+03B2 | 50 - | Ideo em-box (idtp + ideo) | `口` | U+53E3 | `日` | U+65E5 | 51 - | Ideo face (icft + icfb) | `中` | U+4E2D | `田` | U+7530 | 52 - | Hanging + Alphabetic | `अ` | U+0905 | `आ` | U+0906 | 53 - | Math + Alphabetic | `+` | U+002B | `±` | U+00B1 | 43 + | Pair | Layout | Layout codepoint | Labeled | Labeled codepoint | 44 + |------|--------|------------------|---------|-------------------| 45 + | x-height + alphabetic | `x` | U+0078 | `χ` | U+03C7 | 46 + | cap-height + alphabetic | `B` | U+0042 | `β` | U+03B2 | 47 + | ideographic-over + ideographic-under | `口` | U+53E3 | `日` | U+65E5 | 48 + | ideographic-face-over + ideographic-face-under | `中` | U+4E2D | `田` | U+7530 | 49 + | hanging + alphabetic | `अ` | U+0905 | `आ` | U+0906 | 50 + | math + alphabetic | `+` | U+002B | `±` | U+00B1 | 54 51 55 52 ### Em-box glyphs 56 53 57 54 | Variant | Glyph | Codepoint | 58 55 |---------|-------|-----------| 59 - | Filled | `█` | U+2588 | 60 - | Outline | `□` | U+25A1 | 56 + | Filled | `█` | U+2588 | 57 + | Outline | `□` | U+25A1 | 61 58 62 59 ## Source and Downloads 60 + 63 61 Both the source code and built font files can be found in the [`@sajidanwar.com/baseline-diagnostic-font`][tangled-repo] 64 62 repository on [Tangled][tangled-home] or the [`kbhomes/baseline-diagnostic-font`][github-repo] 65 63 repository on [GitHub][github-home]. ··· 78 76 [SIL Open Font License, Version 1.1][ofl-1.1], and is available at `LICENSE.txt`. 79 77 80 78 [noto-sans-mono]: https://fonts.google.com/noto/specimen/Noto+Sans+Mono/license 81 - [ofl-1.1]: https://openfontlicense.org/open-font-license-official-text/ 79 + [ofl-1.1]: https://openfontlicense.org/open-font-license-official-text/
+429
dist/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1"> 6 + <link rel="stylesheet" href="./baseline-diagnostic-font.css"> 7 + <style> 8 + body { 9 + width: 80ch; 10 + margin: auto; 11 + font: 16px/1.5 sans-serif; 12 + padding: 0; 13 + box-sizing: border-box; 14 + } 15 + 16 + code { 17 + background: #f8f8f8; 18 + border: 1px dotted #ccc; 19 + border-radius: 0.25em; 20 + padding: 0.25em; 21 + font-weight: bold; 22 + font-size: smaller; 23 + } 24 + 25 + dfn { 26 + text-decoration: 2px dotted underline; 27 + cursor: help; 28 + } 29 + 30 + #preview, #embox-preview { 31 + display: flex; 32 + justify-content: space-evenly; 33 + text-align: center; 34 + } 35 + 36 + table { 37 + text-align: center; 38 + font-family: monospace; 39 + border-collapse: collapse; 40 + margin: 1em auto; 41 + } 42 + 43 + table td, table th { 44 + padding: 0.25em 0.5em; 45 + vertical-align: middle; 46 + } 47 + 48 + th { 49 + background: #f8f8f8; 50 + } 51 + 52 + #baselines-table { 53 + text-align: left; 54 + } 55 + 56 + #baselines-table td:nth-child(2) { 57 + text-align: right; 58 + } 59 + 60 + .pair-cell { 61 + text-align: center; 62 + } 63 + 64 + .glyph-render { 65 + background: #fff; 66 + display: inline-block; 67 + margin-bottom: 10px; 68 + } 69 + 70 + .glyph-render.layout { 71 + background: repeating-conic-gradient(#eee 0 25%, #0000 0 50%) 50% / 20px 20px; 72 + } 73 + 74 + .pair-name { 75 + display: inline-flex; 76 + flex-direction: column; 77 + gap: 1px; 78 + background: black; 79 + } 80 + 81 + .pair-name span { 82 + background: white; 83 + padding: 4px; 84 + } 85 + 86 + .font-switch { 87 + display: flex; 88 + margin-bottom: 0.75em; 89 + font-family: monospace; 90 + font-size: 0.75em; 91 + justify-content: center; 92 + } 93 + 94 + .font-switch input[type="radio"] { 95 + display: none; 96 + } 97 + 98 + .font-switch label span { 99 + display: block; 100 + padding: 0.3em 0.75em; 101 + border: 1px solid #ccc; 102 + cursor: pointer; 103 + background: #f8f8f8; 104 + user-select: none; 105 + } 106 + 107 + .font-switch label:first-child span { 108 + border-radius: 4px 0 0 4px; 109 + } 110 + 111 + .font-switch label:last-child span { 112 + border-radius: 0 4px 4px 0; 113 + margin-left: -1px; 114 + } 115 + 116 + .font-switch input[type="radio"]:checked + span { 117 + font-weight: bold; 118 + border-color: #000; 119 + background: white; 120 + position: relative; 121 + } 122 + 123 + .font-switch input[type="radio"]:checked + span:after { 124 + content: ' ✔'; 125 + font-weight: 900; 126 + } 127 + 128 + #pair-section:has([value="BaselineDiagnostic"]:checked) .glyph-render { 129 + font-family: "BaselineDiagnostic"; 130 + } 131 + 132 + #pair-section:has([value="BaselineDiagnosticAlphabeticZero"]:checked) .glyph-render { 133 + font-family: "BaselineDiagnosticAlphabeticZero"; 134 + } 135 + </style> 136 + </head> 137 + <body> 138 + <h1>Baseline Diagnostic Font</h1> 139 + 140 + <h2>Overview</h2> 141 + <p> 142 + Font that can be used for validating baseline alignments. Given the embedded 143 + text in the font, this should be used with very large font sizes. 144 + </p> 145 + 146 + <h2>Baselines and Metrics</h2> 147 + <table id="baselines-table" border="1"> 148 + <thead> 149 + <tr> 150 + <th>Baseline/Metric</th> 151 + <th>Coordinate</th> 152 + <th><code>BASE</code> Value</th> 153 + <th><code>OS/2</code> Value</th> 154 + <th><code>hhea</code> Value</th> 155 + </tr> 156 + </thead> 157 + <tr> 158 + <td>ascent</td> 159 + <td>800</td> 160 + <td></td> 161 + <td>sTypoAscender</td> 162 + <td>ascent</td> 163 + </tr> 164 + <tr> 165 + <td>ideographic-over</td> 166 + <td>750</td> 167 + <td>idtp</td> 168 + <td></td> 169 + <td></td> 170 + </tr> 171 + <tr> 172 + <td>hanging</td> 173 + <td>650</td> 174 + <td>hang</td> 175 + <td></td> 176 + <td></td> 177 + </tr> 178 + <tr> 179 + <td>ideographic-face-over</td> 180 + <td>650</td> 181 + <td>icft</td> 182 + <td></td> 183 + <td></td> 184 + </tr> 185 + <tr> 186 + <td>cap-height</td> 187 + <td>550</td> 188 + <td></td> 189 + <td>sCapHeight</td> 190 + <td></td> 191 + </tr> 192 + <tr> 193 + <td>math</td> 194 + <td>450</td> 195 + <td>math</td> 196 + <td></td> 197 + <td></td> 198 + </tr> 199 + <tr> 200 + <td><dfn title="Computed at halfway between ideographic-under and ideographic-over">central</dfn></td> 201 + <td>350</td> 202 + <td></td> 203 + <td></td> 204 + <td></td> 205 + </tr> 206 + <tr> 207 + <td><dfn title="Computed at halfway between ascent and descent">em-middle</dfn></td> 208 + <td>300</td> 209 + <td></td> 210 + <td></td> 211 + <td></td> 212 + </tr> 213 + <tr> 214 + <td>x-height</td> 215 + <td>250</td> 216 + <td></td> 217 + <td>sxHeight</td> 218 + <td></td> 219 + </tr> 220 + <tr> 221 + <td><dfn title="Computed at halfway between alphabetic and x-height">x-middle</dfn></td> 222 + <td>150</td> 223 + <td></td> 224 + <td></td> 225 + <td></td> 226 + </tr> 227 + <tr> 228 + <td>alphabetic</td> 229 + <td>50</td> 230 + <td>romn</td> 231 + <td></td> 232 + <td></td> 233 + </tr> 234 + <tr> 235 + <td>ideographic-face-under</td> 236 + <td>50</td> 237 + <td>icfb</td> 238 + <td></td> 239 + <td></td> 240 + </tr> 241 + <tr> 242 + <td><dfn title="Zero coordinate">zero</dfn></td> 243 + <td>0</td> 244 + <td></td> 245 + <td></td> 246 + <td></td> 247 + </tr> 248 + <tr> 249 + <td>ideographic-under</td> 250 + <td>-50</td> 251 + <td>ideo</td> 252 + <td></td> 253 + <td></td> 254 + </tr> 255 + <tr> 256 + <td>descent</td> 257 + <td>-200</td> 258 + <td></td> 259 + <td>sTypoDescender</td> 260 + <td>descent</td> 261 + </tr> 262 + </table> 263 + <p> 264 + The <code>BaselineDiagnosticAlphabeticZero</code> variant is the same as 265 + <code>BaselineDiagnostic</code>, except 266 + <code>alphabetic</code> moves to 0; <code>x-middle</code> moves to 125. 267 + </p> 268 + 269 + <h2>Glyphs</h2> 270 + 271 + <h3>Diagnostic glyph</h3> 272 + <p> 273 + <code>X</code> (U+0058) draws all baselines with labels. Use at large font sizes. 274 + </p> 275 + <div id="preview"> 276 + <div> 277 + <p><span class="glyph-render" style="font: 250px/1 'BaselineDiagnostic'">X</span></p> 278 + <em><code>BaselineDiagnostic</code> at 250px</em> 279 + </div> 280 + <div> 281 + <p><span class="glyph-render" style="font: 250px/1 'BaselineDiagnosticAlphabeticZero'">X</span></p> 282 + <em><code>BaselineDiagnosticAlphabeticZero</code> at 250px</em> 283 + </div> 284 + </div> 285 + 286 + <h3>Baseline pair glyphs</h3> 287 + <p> 288 + Each pair has a <strong>layout</strong> variant (opaque fill between the two 289 + baselines) and a <strong>labeled</strong> variant (lines with labels, like 290 + <code>X</code>). Labeled variants also show dashed reference lines for 291 + <dfn title="Zero coordinate">zero</dfn> and 292 + <dfn title="Computed at halfway between ascent and descent">em-middle</dfn>. 293 + </p> 294 + <div id="pair-section"> 295 + <div class="font-switch"> 296 + <label><input type="radio" name="pair-font" value="BaselineDiagnostic" checked><span>BaselineDiagnostic</span></label> 297 + <label><input type="radio" name="pair-font" value="BaselineDiagnosticAlphabeticZero"><span>BaselineDiagnosticAlphabeticZero</span></label> 298 + </div> 299 + <table id="pair-table" border="1"> 300 + <thead> 301 + <tr><th>Pair</th><th>Layout</th><th>Labeled</th></tr> 302 + </thead> 303 + <tr> 304 + <td> 305 + <div class="pair-name"> 306 + <span>x-height</span><span>alphabetic</span> </div> 307 + </td> 308 + <td class="pair-cell"> 309 + <span class="glyph-render layout" style="font-size: 150px; line-height: 1">x</span> 310 + <br><code>x</code> (U+0078) 311 + </td> 312 + <td class="pair-cell"> 313 + <span class="glyph-render" style="font-size: 150px; line-height: 1">χ</span> 314 + <br><code>χ</code> (U+03C7) 315 + </td> 316 + </tr> 317 + <tr> 318 + <td> 319 + <div class="pair-name"> 320 + <span>cap-height</span><span>alphabetic</span> </div> 321 + </td> 322 + <td class="pair-cell"> 323 + <span class="glyph-render layout" style="font-size: 150px; line-height: 1">B</span> 324 + <br><code>B</code> (U+0042) 325 + </td> 326 + <td class="pair-cell"> 327 + <span class="glyph-render" style="font-size: 150px; line-height: 1">β</span> 328 + <br><code>β</code> (U+03B2) 329 + </td> 330 + </tr> 331 + <tr> 332 + <td> 333 + <div class="pair-name"> 334 + <span>ideographic-over</span><span>ideographic-under</span> </div> 335 + </td> 336 + <td class="pair-cell"> 337 + <span class="glyph-render layout" style="font-size: 150px; line-height: 1">口</span> 338 + <br><code>口</code> (U+53E3) 339 + </td> 340 + <td class="pair-cell"> 341 + <span class="glyph-render" style="font-size: 150px; line-height: 1">日</span> 342 + <br><code>日</code> (U+65E5) 343 + </td> 344 + </tr> 345 + <tr> 346 + <td> 347 + <div class="pair-name"> 348 + <span>ideographic-face-over</span><span>ideographic-face-under</span> </div> 349 + </td> 350 + <td class="pair-cell"> 351 + <span class="glyph-render layout" style="font-size: 150px; line-height: 1">中</span> 352 + <br><code>中</code> (U+4E2D) 353 + </td> 354 + <td class="pair-cell"> 355 + <span class="glyph-render" style="font-size: 150px; line-height: 1">田</span> 356 + <br><code>田</code> (U+7530) 357 + </td> 358 + </tr> 359 + <tr> 360 + <td> 361 + <div class="pair-name"> 362 + <span>hanging</span><span>alphabetic</span> </div> 363 + </td> 364 + <td class="pair-cell"> 365 + <span class="glyph-render layout" style="font-size: 150px; line-height: 1">अ</span> 366 + <br><code>अ</code> (U+0905) 367 + </td> 368 + <td class="pair-cell"> 369 + <span class="glyph-render" style="font-size: 150px; line-height: 1">आ</span> 370 + <br><code>आ</code> (U+0906) 371 + </td> 372 + </tr> 373 + <tr> 374 + <td> 375 + <div class="pair-name"> 376 + <span>math</span><span>alphabetic</span> </div> 377 + </td> 378 + <td class="pair-cell"> 379 + <span class="glyph-render layout" style="font-size: 150px; line-height: 1">+</span> 380 + <br><code>+</code> (U+002B) 381 + </td> 382 + <td class="pair-cell"> 383 + <span class="glyph-render" style="font-size: 150px; line-height: 1">±</span> 384 + <br><code>±</code> (U+00B1) 385 + </td> 386 + </tr> 387 + </table> 388 + </div> 389 + 390 + <h3>Em-box glyphs</h3> 391 + <p> 392 + Reference glyphs spanning the full em-box from ascent to descent. 393 + Use them as solid or outlined background blocks without any baseline markings. 394 + </p> 395 + <div id="embox-preview"> 396 + <div> 397 + <p><span class="glyph-render" style="font: 150px/1 'BaselineDiagnostic'">█</span></p> 398 + <em><code>█</code> (U+2588) — Filled</em> 399 + </div> 400 + <div> 401 + <p><span class="glyph-render" style="font: 150px/1 'BaselineDiagnostic'">□</span></p> 402 + <em><code>□</code> (U+25A1) — Outline</em> 403 + </div> 404 + </div> 405 + 406 + <h2>Source and Downloads</h2> 407 + <p> 408 + Both the source code and built font files can be found in the 409 + <a href="https://tangled.org/sajidanwar.com/baseline-diagnostic-font"><code>@sajidanwar.com/baseline-diagnostic-font</code></a> 410 + repository on <a href="https://tangled.org">Tangled</a> or the 411 + <a href="https://github.com/kbhomes/baseline-diagnostic-font"><code>kbhomes/baseline-diagnostic-font</code></a> 412 + repository on <a href="https://github.com/">GitHub</a>. 413 + </p> 414 + <p> 415 + This font is built using Python with the 416 + <a href="https://fonttools.readthedocs.io/en/latest/">fonttools</a> library. 417 + </p> 418 + 419 + <h2>License</h2> 420 + <p> 421 + This font contains <a href="https://fonts.google.com/noto/specimen/Noto+Sans+Mono/license">Noto Sans Mono</a> 422 + glyphs in the rendering of its baseline labels. Like that font, this font is 423 + licensed under the 424 + <a href="https://openfontlicense.org/open-font-license-official-text/">SIL Open Font License, Version 1.1</a>, 425 + and is available in the 426 + <a href="https://tangled.org/sajidanwar.com/baseline-diagnostic-font/blob/main/dist/LICENSE.md">source repository</a>. 427 + </p> 428 + </body> 429 + </html>
+75 -187
main.py
··· 1 1 import re 2 2 from font import Font, FontBaseline, FontBaselineStyle, FontGlyph, FontGlyphKind, build_baselines_font 3 + from jinja2 import Environment, FileSystemLoader 3 4 from textwrap import dedent, indent 4 5 from typing import Dict, List 6 + 7 + AUTHOR = "Sajid Anwar" 5 8 6 9 def main(): 7 10 glyphs = [ ··· 82 85 83 86 write_font_files(fonts) 84 87 write_font_stylesheet(fonts) 85 - write_font_readme() 88 + write_font_html(fonts) 89 + write_font_readme(fonts) 86 90 write_font_license() 87 91 88 92 ··· 150 154 print(f"Wrote stylesheet at {out_path}") 151 155 152 156 153 - # TODO: Generate this README and tables programmatically from the fonts. 154 - def write_font_readme(): 155 - out_path = "dist/README.md" 156 - with open(out_path, "w") as f: 157 - f.write(dedent(r''' 158 - # Baseline Diagnostic Font 157 + def prepare_template_data(fonts: List[Font]) -> dict: 158 + font = fonts[0] 159 + font_az = fonts[1] 159 160 160 - ## Overview 161 + dfn_tooltips = { 162 + 'central': 'Computed at halfway between ideographic-under and ideographic-over', 163 + 'em-middle': 'Computed at halfway between ascent and descent', 164 + 'x-middle': 'Computed at halfway between alphabetic and x-height', 165 + 'zero': 'Zero coordinate', 166 + } 161 167 162 - Font that can be used for validating baseline alignments. Given the embedded 163 - text in the font, this should be used with very large font sizes. 168 + seen: Dict[str, dict] = {} 169 + baseline_table = [] 170 + for b in font.baselines: 171 + if b.id not in seen: 172 + entry = {'id': b.id, 'position': b.position, 'base': '', 'os2': '', 'hhea': '', 'tooltip': dfn_tooltips.get(b.id)} 173 + seen[b.id] = entry 174 + baseline_table.append(entry) 175 + if b.table == 'BASE': seen[b.id]['base'] = b.name 176 + elif b.table == 'OS/2': seen[b.id]['os2'] = b.name 177 + elif b.table == 'hhea': seen[b.id]['hhea'] = b.name 164 178 165 - ## Baselines and Metrics 179 + az_positions = {} 180 + for b in font_az.baselines: 181 + if b.id not in az_positions: 182 + az_positions[b.id] = b.position 183 + az_diffs = [{'id': e['id'], 'position': az_positions[e['id']]} 184 + for e in baseline_table 185 + if e['id'] in az_positions and az_positions[e['id']] != e['position']] 186 + az_diffs.sort(key=lambda baseline: baseline['id']) 166 187 167 - | Baseline/Metric | Coordinate | BASE Value | OS/2 Value | hhea Value | 168 - |------------------------|------------|------------|----------------|------------| 169 - | ascent | 800 | | sTypoAscender | ascent | 170 - | ideographic-over | 750 | idtp | | | 171 - | hanging | 650 | hang | | | 172 - | ideographic-face-over | 650 | icft | | | 173 - | cap-height | 550 | | sCapHeight | | 174 - | math | 450 | math | | | 175 - | /central/ | 350 | | | | 176 - | /em-middle/ | 300 | | | | 177 - | x-height | 250 | | sxHeight | | 178 - | /x-middle/ | 150 | | | | 179 - | alphabetic | 50 | romn | | | 180 - | ideographic-face-under | 50 | icfb | | | 181 - | /zero/ | 0 | | | | 182 - | ideographic-under | -50 | ideo | | | 183 - | descent | -200 | | sTypoDescender | descent | 188 + pair_map: Dict[tuple, dict] = {} 189 + pair_order = [] 190 + for g in font.glyphs: 191 + if g.baseline_ids: 192 + key = tuple(g.baseline_ids) 193 + if key not in pair_map: 194 + pair_map[key] = {'ids': list(key), 'layout': None, 'labeled': None} 195 + pair_order.append(key) 196 + glyph_data = {'char': g.char, 'codepoint': f'U+{ord(g.char):04X}'} 197 + if g.kind == FontGlyphKind.PAIR_LAYOUT: pair_map[key]['layout'] = glyph_data 198 + elif g.kind == FontGlyphKind.PAIR_LABELED: pair_map[key]['labeled'] = glyph_data 184 199 185 - The `BaselineDiagnosticAlphabeticZero` variant is the same as `BaselineDiagnostic`, 186 - except the alphabetic baseline is at the common value of 0. This also 187 - results in the x-middle baseline being at 125. 200 + def embox_data(kind): 201 + g = next((g for g in font.glyphs if g.kind == kind), None) 202 + return {'char': g.char, 'codepoint': f'U+{ord(g.char):04X}'} if g else None 188 203 189 - ## Glyphs 204 + return { 205 + 'font_name': font.name, 206 + 'font_az_name': font_az.name, 207 + 'baseline_table': baseline_table, 208 + 'az_diffs': az_diffs, 209 + 'pairs': [pair_map[k] for k in pair_order], 210 + 'embox_filled': embox_data(FontGlyphKind.EMBOX_FILLED), 211 + 'embox_outline': embox_data(FontGlyphKind.EMBOX_OUTLINE), 212 + } 190 213 191 - ### Diagnostic glyph 192 214 193 - | Glyph | Codepoint | Description | 194 - |-------|-----------|-------------| 195 - | `X` | U+0058 | All baselines drawn with labels | 215 + def _jinja_env(): 216 + return Environment(loader=FileSystemLoader('templates'), trim_blocks=True, lstrip_blocks=True) 196 217 197 - ### Pair glyphs 198 218 199 - Each baseline pair has two variants: a **layout** glyph (opaque filled rectangle 200 - between the two baselines) and a **labeled** glyph (lines with text labels, like `X`). 201 - 202 - | Pair | Layout | Layout codepoint | Labeled | Labeled codepoint | 203 - |-------------------------------|--------|------------------|---------|-------------------| 204 - | X-height + Alphabetic | `x` | U+0078 | `χ` | U+03C7 | 205 - | Cap-height + Alphabetic | `B` | U+0042 | `β` | U+03B2 | 206 - | Ideo em-box (idtp + ideo) | `口` | U+53E3 | `日` | U+65E5 | 207 - | Ideo face (icft + icfb) | `中` | U+4E2D | `田` | U+7530 | 208 - | Hanging + Alphabetic | `अ` | U+0905 | `आ` | U+0906 | 209 - | Math + Alphabetic | `+` | U+002B | `±` | U+00B1 | 210 - 211 - ### Em-box glyphs 212 - 213 - | Variant | Glyph | Codepoint | 214 - |---------|-------|-----------| 215 - | Filled | `█` | U+2588 | 216 - | Outline | `□` | U+25A1 | 217 - 218 - ## Source and Downloads 219 - Both the source code and built font files can be found in the [`@sajidanwar.com/baseline-diagnostic-font`][tangled-repo] 220 - repository on [Tangled][tangled-home] or the [`kbhomes/baseline-diagnostic-font`][github-repo] 221 - repository on [GitHub][github-home]. 222 - 223 - This font is built using Python with the [fonttools](https://fonttools.readthedocs.io/en/latest/) library. 224 - 225 - [tangled-repo]: https://tangled.org/sajidanwar.com/baseline-diagnostic-font 226 - [tangled-home]: https://tangled.org/ 227 - [github-repo]: https://github.com/kbhomes/baseline-diagnostic-font 228 - [github-home]: https://github.com/ 229 - 230 - ## License 219 + def write_font_html(fonts: List[Font]): 220 + out_path = "dist/index.html" 221 + data = prepare_template_data(fonts) 222 + html = _jinja_env().get_template('index.html.jinja').render(**data) 223 + with open(out_path, 'w') as f: 224 + f.write(html) 225 + print(f"Wrote HTML at {out_path}") 231 226 232 - This font contains [Noto Sans Mono][noto-sans-mono] glyphs in the rendering 233 - of its baseline labels. Like that font, this font is licensed under the 234 - [SIL Open Font License, Version 1.1][ofl-1.1], and is available at `LICENSE.txt`. 235 227 236 - [noto-sans-mono]: https://fonts.google.com/noto/specimen/Noto+Sans+Mono/license 237 - [ofl-1.1]: https://openfontlicense.org/open-font-license-official-text/ 238 - ''')) 239 - print(f"Wrote README at {out_path}") 228 + def write_font_readme(fonts: List[Font]): 229 + out_path = "dist/README.md" 230 + data = prepare_template_data(fonts) 231 + md = _jinja_env().get_template('README.md.jinja').render(**data) 232 + with open(out_path, 'w') as f: 233 + f.write(md) 234 + print(f"Wrote README at {out_path}") 240 235 241 236 242 237 def write_font_license(): 243 238 out_path = "dist/LICENSE.md" 244 - with open(out_path, "w") as f: 245 - f.write(dedent(r''' 246 - Copyright (c) 2026, Sajid Anwar. 247 - 248 - This Font Software is licensed under the SIL Open Font License, Version 1.1. 249 - This license is copied below, and is also available with a FAQ at: 250 - https\://openfontlicense.org 251 - &nbsp; 252 - 253 - \---------------------------------------------------------------------- 254 - 255 - #### SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 256 - 257 - \---------------------------------------------------------------------- 258 - 259 - &nbsp; 260 - 261 - PREAMBLE 262 - ----------- 263 - 264 - The goals of the Open Font License (OFL) are to stimulate worldwide 265 - development of collaborative font projects, to support the font creation 266 - efforts of academic and linguistic communities, and to provide a free and 267 - open framework in which fonts may be shared and improved in partnership 268 - with others. 269 - 270 - The OFL allows the licensed fonts to be used, studied, modified and 271 - redistributed freely as long as they are not sold by themselves. The 272 - fonts, including any derivative works, can be bundled, embedded, 273 - redistributed and/or sold with any software provided that any reserved 274 - names are not used by derivative works. The fonts and derivatives, 275 - however, cannot be released under any other type of license. The 276 - requirement for fonts to remain under this license does not apply 277 - to any document created using the fonts or their derivatives. 278 - 279 - DEFINITIONS 280 - ----------- 281 - 282 - "Font Software" refers to the set of files released by the Copyright 283 - Holder(s) under this license and clearly marked as such. This may 284 - include source files, build scripts and documentation. 285 - 286 - "Reserved Font Name" refers to any names specified as such after the 287 - copyright statement(s). 288 - 289 - "Original Version" refers to the collection of Font Software components as 290 - distributed by the Copyright Holder(s). 291 - 292 - "Modified Version" refers to any derivative made by adding to, deleting, 293 - or substituting -- in part or in whole -- any of the components of the 294 - Original Version, by changing formats or by porting the Font Software to a 295 - new environment. 296 - 297 - "Author" refers to any designer, engineer, programmer, technical 298 - writer or other person who contributed to the Font Software. 299 - 300 - PERMISSION & CONDITIONS 301 - ----------- 302 - 303 - Permission is hereby granted, free of charge, to any person obtaining 304 - a copy of the Font Software, to use, study, copy, merge, embed, modify, 305 - redistribute, and sell modified and unmodified copies of the Font 306 - Software, subject to the following conditions: 307 - 308 - 1) Neither the Font Software nor any of its individual components, 309 - in Original or Modified Versions, may be sold by itself. 310 - 311 - 2) Original or Modified Versions of the Font Software may be bundled, 312 - redistributed and/or sold with any software, provided that each copy 313 - contains the above copyright notice and this license. These can be 314 - included either as stand-alone text files, human-readable headers or 315 - in the appropriate machine-readable metadata fields within text or 316 - binary files as long as those fields can be easily viewed by the user. 317 - 318 - 3) No Modified Version of the Font Software may use the Reserved Font 319 - Name(s) unless explicit written permission is granted by the corresponding 320 - Copyright Holder. This restriction only applies to the primary font name as 321 - presented to the users. 322 - 323 - 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 324 - Software shall not be used to promote, endorse or advertise any 325 - Modified Version, except to acknowledge the contribution(s) of the 326 - Copyright Holder(s) and the Author(s) or with their explicit written 327 - permission. 328 - 329 - 5) The Font Software, modified or unmodified, in part or in whole, 330 - must be distributed entirely under this license, and must not be 331 - distributed under any other license. The requirement for fonts to 332 - remain under this license does not apply to any document created 333 - using the Font Software. 334 - 335 - TERMINATION 336 - ----------- 337 - 338 - This license becomes null and void if any of the above conditions are 339 - not met. 340 - 341 - DISCLAIMER 342 - ----------- 343 - 344 - THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 345 - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 346 - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 347 - OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 348 - COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 349 - INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 350 - DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 351 - FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 352 - OTHER DEALINGS IN THE FONT SOFTWARE. 353 - ''')) 354 - print(f"Wrote OFL 1.1 license at {out_path}") 239 + md = _jinja_env().get_template('LICENSE.md.jinja').render(author=AUTHOR) 240 + with open(out_path, 'w') as f: 241 + f.write(md) 242 + print(f"Wrote OFL 1.1 license at {out_path}") 355 243 356 244 def dashing(value: str): 357 245 return re.sub(r'(?<!^)(?=[A-Z])', '-', value).lower()
+1
pyproject.toml
··· 6 6 requires-python = ">=3.14" 7 7 dependencies = [ 8 8 "fonttools>=4.61.1", 9 + "jinja2>=3.1.6", 9 10 ]
+107
templates/LICENSE.md.jinja
··· 1 + Copyright (c) 2026, {{ author }}. 2 + 3 + This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 + This license is copied below, and is also available with a FAQ at: 5 + https://openfontlicense.org 6 + &nbsp; 7 + 8 + \---------------------------------------------------------------------- 9 + 10 + #### SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 + 12 + \---------------------------------------------------------------------- 13 + 14 + &nbsp; 15 + 16 + PREAMBLE 17 + ----------- 18 + 19 + The goals of the Open Font License (OFL) are to stimulate worldwide 20 + development of collaborative font projects, to support the font creation 21 + efforts of academic and linguistic communities, and to provide a free and 22 + open framework in which fonts may be shared and improved in partnership 23 + with others. 24 + 25 + The OFL allows the licensed fonts to be used, studied, modified and 26 + redistributed freely as long as they are not sold by themselves. The 27 + fonts, including any derivative works, can be bundled, embedded, 28 + redistributed and/or sold with any software provided that any reserved 29 + names are not used by derivative works. The fonts and derivatives, 30 + however, cannot be released under any other type of license. The 31 + requirement for fonts to remain under this license does not apply 32 + to any document created using the fonts or their derivatives. 33 + 34 + DEFINITIONS 35 + ----------- 36 + 37 + "Font Software" refers to the set of files released by the Copyright 38 + Holder(s) under this license and clearly marked as such. This may 39 + include source files, build scripts and documentation. 40 + 41 + "Reserved Font Name" refers to any names specified as such after the 42 + copyright statement(s). 43 + 44 + "Original Version" refers to the collection of Font Software components as 45 + distributed by the Copyright Holder(s). 46 + 47 + "Modified Version" refers to any derivative made by adding to, deleting, 48 + or substituting -- in part or in whole -- any of the components of the 49 + Original Version, by changing formats or by porting the Font Software to a 50 + new environment. 51 + 52 + "Author" refers to any designer, engineer, programmer, technical 53 + writer or other person who contributed to the Font Software. 54 + 55 + PERMISSION & CONDITIONS 56 + ----------- 57 + 58 + Permission is hereby granted, free of charge, to any person obtaining 59 + a copy of the Font Software, to use, study, copy, merge, embed, modify, 60 + redistribute, and sell modified and unmodified copies of the Font 61 + Software, subject to the following conditions: 62 + 63 + 1) Neither the Font Software nor any of its individual components, 64 + in Original or Modified Versions, may be sold by itself. 65 + 66 + 2) Original or Modified Versions of the Font Software may be bundled, 67 + redistributed and/or sold with any software, provided that each copy 68 + contains the above copyright notice and this license. These can be 69 + included either as stand-alone text files, human-readable headers or 70 + in the appropriate machine-readable metadata fields within text or 71 + binary files as long as those fields can be easily viewed by the user. 72 + 73 + 3) No Modified Version of the Font Software may use the Reserved Font 74 + Name(s) unless explicit written permission is granted by the corresponding 75 + Copyright Holder. This restriction only applies to the primary font name as 76 + presented to the users. 77 + 78 + 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 79 + Software shall not be used to promote, endorse or advertise any 80 + Modified Version, except to acknowledge the contribution(s) of the 81 + Copyright Holder(s) and the Author(s) or with their explicit written 82 + permission. 83 + 84 + 5) The Font Software, modified or unmodified, in part or in whole, 85 + must be distributed entirely under this license, and must not be 86 + distributed under any other license. The requirement for fonts to 87 + remain under this license does not apply to any document created 88 + using the Font Software. 89 + 90 + TERMINATION 91 + ----------- 92 + 93 + This license becomes null and void if any of the above conditions are 94 + not met. 95 + 96 + DISCLAIMER 97 + ----------- 98 + 99 + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 100 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 101 + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 102 + OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 103 + COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 104 + INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 105 + DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 106 + FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 107 + OTHER DEALINGS IN THE FONT SOFTWARE.
+68
templates/README.md.jinja
··· 1 + # Baseline Diagnostic Font 2 + 3 + ## Overview 4 + 5 + Font that can be used for validating baseline alignments. Given the embedded 6 + text in the font, this should be used with very large font sizes. 7 + 8 + ## Baselines and Metrics 9 + 10 + | Baseline/Metric | Coordinate | BASE Value | OS/2 Value | hhea Value | 11 + |-----------------|------------|------------|------------|------------| 12 + {% for b in baseline_table %} 13 + | {% if b.tooltip %}/{{ b.id }}/{% else %}{{ b.id }}{% endif %} | {{ b.position }} | {{ b.base }} | {{ b.os2 }} | {{ b.hhea }} | 14 + {% endfor %} 15 + 16 + The `{{ font_az_name }}` variant is the same as `{{ font_name }}`, except {% for diff in az_diffs %}`{{ diff.id }}` moves to {{ diff.position }}{% if not loop.last %}; {% endif %}{% endfor %}. 17 + 18 + ## Glyphs 19 + 20 + ### Diagnostic glyph 21 + 22 + | Glyph | Codepoint | Description | 23 + |-------|-----------|-------------| 24 + | `X` | U+0058 | All baselines drawn with labels | 25 + 26 + ### Pair glyphs 27 + 28 + Each baseline pair has two variants: a **layout** glyph (opaque filled rectangle 29 + between the two baselines) and a **labeled** glyph (lines with text labels, like `X`). 30 + 31 + | Pair | Layout | Layout codepoint | Labeled | Labeled codepoint | 32 + |------|--------|------------------|---------|-------------------| 33 + {% for pair in pairs %} 34 + | {{ pair.ids | join(' + ') }} | {% if pair.layout %}`{{ pair.layout.char }}`{% endif %} | {% if pair.layout %}{{ pair.layout.codepoint }}{% endif %} | {% if pair.labeled %}`{{ pair.labeled.char }}`{% endif %} | {% if pair.labeled %}{{ pair.labeled.codepoint }}{% endif %} | 35 + {% endfor %} 36 + 37 + ### Em-box glyphs 38 + 39 + | Variant | Glyph | Codepoint | 40 + |---------|-------|-----------| 41 + {% if embox_filled %} 42 + | Filled | `{{ embox_filled.char }}` | {{ embox_filled.codepoint }} | 43 + {% endif %} 44 + {% if embox_outline %} 45 + | Outline | `{{ embox_outline.char }}` | {{ embox_outline.codepoint }} | 46 + {% endif %} 47 + 48 + ## Source and Downloads 49 + 50 + Both the source code and built font files can be found in the [`@sajidanwar.com/baseline-diagnostic-font`][tangled-repo] 51 + repository on [Tangled][tangled-home] or the [`kbhomes/baseline-diagnostic-font`][github-repo] 52 + repository on [GitHub][github-home]. 53 + 54 + This font is built using Python with the [fonttools](https://fonttools.readthedocs.io/en/latest/) library. 55 + 56 + [tangled-repo]: https://tangled.org/sajidanwar.com/baseline-diagnostic-font 57 + [tangled-home]: https://tangled.org/ 58 + [github-repo]: https://github.com/kbhomes/baseline-diagnostic-font 59 + [github-home]: https://github.com/ 60 + 61 + ## License 62 + 63 + This font contains [Noto Sans Mono][noto-sans-mono] glyphs in the rendering 64 + of its baseline labels. Like that font, this font is licensed under the 65 + [SIL Open Font License, Version 1.1][ofl-1.1], and is available at `LICENSE.txt`. 66 + 67 + [noto-sans-mono]: https://fonts.google.com/noto/specimen/Noto+Sans+Mono/license 68 + [ofl-1.1]: https://openfontlicense.org/open-font-license-official-text/
+278
templates/index.html.jinja
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1"> 6 + <link rel="stylesheet" href="./baseline-diagnostic-font.css"> 7 + <style> 8 + body { 9 + width: 80ch; 10 + margin: auto; 11 + font: 16px/1.5 sans-serif; 12 + padding: 0; 13 + box-sizing: border-box; 14 + } 15 + 16 + code { 17 + background: #f8f8f8; 18 + border: 1px dotted #ccc; 19 + border-radius: 0.25em; 20 + padding: 0.25em; 21 + font-weight: bold; 22 + font-size: smaller; 23 + } 24 + 25 + dfn { 26 + text-decoration: 2px dotted underline; 27 + cursor: help; 28 + } 29 + 30 + #preview, #embox-preview { 31 + display: flex; 32 + justify-content: space-evenly; 33 + text-align: center; 34 + } 35 + 36 + table { 37 + text-align: center; 38 + font-family: monospace; 39 + border-collapse: collapse; 40 + margin: 1em auto; 41 + } 42 + 43 + table td, table th { 44 + padding: 0.25em 0.5em; 45 + vertical-align: middle; 46 + } 47 + 48 + th { 49 + background: #f8f8f8; 50 + } 51 + 52 + #baselines-table { 53 + text-align: left; 54 + } 55 + 56 + #baselines-table td:nth-child(2) { 57 + text-align: right; 58 + } 59 + 60 + .pair-cell { 61 + text-align: center; 62 + } 63 + 64 + .glyph-render { 65 + background: #fff; 66 + display: inline-block; 67 + margin-bottom: 10px; 68 + } 69 + 70 + .glyph-render.layout { 71 + background: repeating-conic-gradient(#eee 0 25%, #0000 0 50%) 50% / 20px 20px; 72 + } 73 + 74 + .pair-name { 75 + display: inline-flex; 76 + flex-direction: column; 77 + gap: 1px; 78 + background: black; 79 + } 80 + 81 + .pair-name span { 82 + background: white; 83 + padding: 4px; 84 + } 85 + 86 + .font-switch { 87 + display: flex; 88 + margin-bottom: 0.75em; 89 + font-family: monospace; 90 + font-size: 0.75em; 91 + justify-content: center; 92 + } 93 + 94 + .font-switch input[type="radio"] { 95 + display: none; 96 + } 97 + 98 + .font-switch label span { 99 + display: block; 100 + padding: 0.3em 0.75em; 101 + border: 1px solid #ccc; 102 + cursor: pointer; 103 + background: #f8f8f8; 104 + user-select: none; 105 + } 106 + 107 + .font-switch label:first-child span { 108 + border-radius: 4px 0 0 4px; 109 + } 110 + 111 + .font-switch label:last-child span { 112 + border-radius: 0 4px 4px 0; 113 + margin-left: -1px; 114 + } 115 + 116 + .font-switch input[type="radio"]:checked + span { 117 + font-weight: bold; 118 + border-color: #000; 119 + background: white; 120 + position: relative; 121 + } 122 + 123 + .font-switch input[type="radio"]:checked + span:after { 124 + content: ' ✔'; 125 + font-weight: 900; 126 + } 127 + 128 + #pair-section:has([value="{{ font_name }}"]:checked) .glyph-render { 129 + font-family: "{{ font_name }}"; 130 + } 131 + 132 + #pair-section:has([value="{{ font_az_name }}"]:checked) .glyph-render { 133 + font-family: "{{ font_az_name }}"; 134 + } 135 + </style> 136 + </head> 137 + <body> 138 + <h1>Baseline Diagnostic Font</h1> 139 + 140 + <h2>Overview</h2> 141 + <p> 142 + Font that can be used for validating baseline alignments. Given the embedded 143 + text in the font, this should be used with very large font sizes. 144 + </p> 145 + 146 + <h2>Baselines and Metrics</h2> 147 + <table id="baselines-table" border="1"> 148 + <thead> 149 + <tr> 150 + <th>Baseline/Metric</th> 151 + <th>Coordinate</th> 152 + <th><code>BASE</code> Value</th> 153 + <th><code>OS/2</code> Value</th> 154 + <th><code>hhea</code> Value</th> 155 + </tr> 156 + </thead> 157 + {% for b in baseline_table %} 158 + <tr> 159 + <td>{% if b.tooltip %}<dfn title="{{ b.tooltip }}">{{ b.id }}</dfn>{% else %}{{ b.id }}{% endif %}</td> 160 + <td>{{ b.position }}</td> 161 + <td>{{ b.base }}</td> 162 + <td>{{ b.os2 }}</td> 163 + <td>{{ b.hhea }}</td> 164 + </tr> 165 + {% endfor %} 166 + </table> 167 + <p> 168 + The <code>{{ font_az_name }}</code> variant is the same as 169 + <code>{{ font_name }}</code>, except 170 + {% for diff in az_diffs %}<code>{{ diff.id }}</code> moves to {{ diff.position }}{% if not loop.last %}; {% endif %}{% endfor %}. 171 + </p> 172 + 173 + <h2>Glyphs</h2> 174 + 175 + <h3>Diagnostic glyph</h3> 176 + <p> 177 + <code>X</code> (U+0058) draws all baselines with labels. Use at large font sizes. 178 + </p> 179 + <div id="preview"> 180 + <div> 181 + <p><span class="glyph-render" style="font: 250px/1 '{{ font_name }}'">X</span></p> 182 + <em><code>{{ font_name }}</code> at 250px</em> 183 + </div> 184 + <div> 185 + <p><span class="glyph-render" style="font: 250px/1 '{{ font_az_name }}'">X</span></p> 186 + <em><code>{{ font_az_name }}</code> at 250px</em> 187 + </div> 188 + </div> 189 + 190 + <h3>Baseline pair glyphs</h3> 191 + <p> 192 + Each pair has a <strong>layout</strong> variant (opaque fill between the two 193 + baselines) and a <strong>labeled</strong> variant (lines with labels, like 194 + <code>X</code>). Labeled variants also show dashed reference lines for 195 + <dfn title="Zero coordinate">zero</dfn> and 196 + <dfn title="Computed at halfway between ascent and descent">em-middle</dfn>. 197 + </p> 198 + <div id="pair-section"> 199 + <div class="font-switch"> 200 + <label><input type="radio" name="pair-font" value="{{ font_name }}" checked><span>{{ font_name }}</span></label> 201 + <label><input type="radio" name="pair-font" value="{{ font_az_name }}"><span>{{ font_az_name }}</span></label> 202 + </div> 203 + <table id="pair-table" border="1"> 204 + <thead> 205 + <tr><th>Pair</th><th>Layout</th><th>Labeled</th></tr> 206 + </thead> 207 + {% for pair in pairs %} 208 + <tr> 209 + <td> 210 + <div class="pair-name"> 211 + {% for id in pair.ids %}<span>{{ id }}</span>{% endfor %} 212 + </div> 213 + </td> 214 + {% if pair.layout %} 215 + <td class="pair-cell"> 216 + <span class="glyph-render layout" style="font-size: 150px; line-height: 1">{{ pair.layout.char }}</span> 217 + <br><code>{{ pair.layout.char }}</code> ({{ pair.layout.codepoint }}) 218 + </td> 219 + {% else %} 220 + <td></td> 221 + {% endif %} 222 + {% if pair.labeled %} 223 + <td class="pair-cell"> 224 + <span class="glyph-render" style="font-size: 150px; line-height: 1">{{ pair.labeled.char }}</span> 225 + <br><code>{{ pair.labeled.char }}</code> ({{ pair.labeled.codepoint }}) 226 + </td> 227 + {% else %} 228 + <td></td> 229 + {% endif %} 230 + </tr> 231 + {% endfor %} 232 + </table> 233 + </div> 234 + 235 + <h3>Em-box glyphs</h3> 236 + <p> 237 + Reference glyphs spanning the full em-box from ascent to descent. 238 + Use them as solid or outlined background blocks without any baseline markings. 239 + </p> 240 + <div id="embox-preview"> 241 + {% if embox_filled %} 242 + <div> 243 + <p><span class="glyph-render" style="font: 150px/1 '{{ font_name }}'">{{ embox_filled.char }}</span></p> 244 + <em><code>{{ embox_filled.char }}</code> ({{ embox_filled.codepoint }}) — Filled</em> 245 + </div> 246 + {% endif %} 247 + {% if embox_outline %} 248 + <div> 249 + <p><span class="glyph-render" style="font: 150px/1 '{{ font_name }}'">{{ embox_outline.char }}</span></p> 250 + <em><code>{{ embox_outline.char }}</code> ({{ embox_outline.codepoint }}) — Outline</em> 251 + </div> 252 + {% endif %} 253 + </div> 254 + 255 + <h2>Source and Downloads</h2> 256 + <p> 257 + Both the source code and built font files can be found in the 258 + <a href="https://tangled.org/sajidanwar.com/baseline-diagnostic-font"><code>@sajidanwar.com/baseline-diagnostic-font</code></a> 259 + repository on <a href="https://tangled.org">Tangled</a> or the 260 + <a href="https://github.com/kbhomes/baseline-diagnostic-font"><code>kbhomes/baseline-diagnostic-font</code></a> 261 + repository on <a href="https://github.com/">GitHub</a>. 262 + </p> 263 + <p> 264 + This font is built using Python with the 265 + <a href="https://fonttools.readthedocs.io/en/latest/">fonttools</a> library. 266 + </p> 267 + 268 + <h2>License</h2> 269 + <p> 270 + This font contains <a href="https://fonts.google.com/noto/specimen/Noto+Sans+Mono/license">Noto Sans Mono</a> 271 + glyphs in the rendering of its baseline labels. Like that font, this font is 272 + licensed under the 273 + <a href="https://openfontlicense.org/open-font-license-official-text/">SIL Open Font License, Version 1.1</a>, 274 + and is available in the 275 + <a href="https://tangled.org/sajidanwar.com/baseline-diagnostic-font/blob/main/dist/LICENSE.md">source repository</a>. 276 + </p> 277 + </body> 278 + </html>
+47 -1
uv.lock
··· 8 8 source = { virtual = "." } 9 9 dependencies = [ 10 10 { name = "fonttools" }, 11 + { name = "jinja2" }, 11 12 ] 12 13 13 14 [package.metadata] 14 - requires-dist = [{ name = "fonttools", specifier = ">=4.61.1" }] 15 + requires-dist = [ 16 + { name = "fonttools", specifier = ">=4.61.1" }, 17 + { name = "jinja2", specifier = ">=3.1.6" }, 18 + ] 15 19 16 20 [[package]] 17 21 name = "fonttools" ··· 37 41 { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, 38 42 { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, 39 43 ] 44 + 45 + [[package]] 46 + name = "jinja2" 47 + version = "3.1.6" 48 + source = { registry = "https://pypi.org/simple" } 49 + dependencies = [ 50 + { name = "markupsafe" }, 51 + ] 52 + sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } 53 + wheels = [ 54 + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, 55 + ] 56 + 57 + [[package]] 58 + name = "markupsafe" 59 + version = "3.0.3" 60 + source = { registry = "https://pypi.org/simple" } 61 + sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } 62 + wheels = [ 63 + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, 64 + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, 65 + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, 66 + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, 67 + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, 68 + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, 69 + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, 70 + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, 71 + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, 72 + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, 73 + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, 74 + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, 75 + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, 76 + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, 77 + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, 78 + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, 79 + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, 80 + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, 81 + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, 82 + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, 83 + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, 84 + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, 85 + ]