Skill Tracer
Skills › gstack › design-html › pretext-patterns.md

pretext-patterns.md

145 lines · design-html/sections/pretext-patterns.md at 3f79d81

Open on GitHub
1<!-- AUTO-GENERATED from pretext-patterns.md.tmpl — do not edit directly -->
2<!-- Regenerate: bun run gen:skill-docs -->

<!-- AUTO-GENERATED from pretext-patterns.md.tmpl — do not edit directly --> <!-- Regenerate: bun run gen:skill-docs -->

Raw lines 1 to 2
3### Pretext Wiring Patterns
4
5Use these patterns based on the tier selected in Step 2. These are the correct
6Pretext API usage patterns. Follow them exactly.
7
8**Pattern 1: Basic height computation (Simple layout, Card/grid)**
9```js
10import { prepare, layout } from '@chenglou/pretext'
11// Vanilla HTML: omit the import; this code follows the inlined bundle in the same <script type="module">
13// 1. PREPARE — one-time, after fonts load
14await document.fonts.ready
15const elements = document.querySelectorAll('[data-pretext]')
16const prepared = new Map()
18for (const el of elements) {
19 const text = el.textContent
20 const font = getComputedStyle(el).font
21 prepared.set(el, prepare(text, font))
24// 2. LAYOUT — cheap, call on every resize
25function relayout() {
26 for (const [el, handle] of prepared) {
27 const { height } = layout(handle, el.clientWidth, parseFloat(getComputedStyle(el).lineHeight))
28 el.style.height = `${height}px`
29 }
32// 3. RESIZE-AWARE
33new ResizeObserver(() => relayout()).observe(document.body)
34relayout()
36// 4. CONTENT-EDITABLE — re-prepare when text changes
37for (const el of elements) {
38 if (el.contentEditable === 'true') {
39 new MutationObserver(() => {
40 const font = getComputedStyle(el).font
41 prepared.set(el, prepare(el.textContent, font))
42 relayout()
43 }).observe(el, { characterData: true, subtree: true, childList: true })
44 }
46```
48**Pattern 2: Shrinkwrap / tight-fit containers (Chat bubbles)**
49```js
50import { prepareWithSegments, walkLineRanges } from '@chenglou/pretext'
52// Tight-fit width: lay out at maxWidth, then shrink the bubble to its widest line
53function shrinkwrap(text, font, maxWidth) {
54 const segs = prepareWithSegments(text, font)
55 let widest = 0
56 const lineCount = walkLineRanges(segs, maxWidth, (line) => {
57 widest = Math.max(widest, line.width)
58 })
59 return { width: Math.ceil(widest), lineCount }
61```
63**Pattern 3: Text around obstacles (Editorial layout)**
64```js
65import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext'
67function layoutAroundObstacles(text, font, containerWidth, lineHeight, obstacles) {
68 const segs = prepareWithSegments(text, font)
69 let cursor = { segmentIndex: 0, graphemeIndex: 0 }
70 let y = 0
71 const lines = []
73 while (true) {
74 // Calculate available width at current y position, accounting for obstacles
75 let availWidth = containerWidth
76 for (const obs of obstacles) {
77 if (y >= obs.top && y < obs.top + obs.height) {
78 availWidth -= obs.width
79 }
80 }
82 const result = layoutNextLine(segs, cursor, availWidth)
83 if (!result) break
85 lines.push({ text: result.text, width: result.width, x: 0, y })
86 cursor = result.end
87 y += lineHeight
88 }
90 return { lines, totalHeight: y }
92```
94**Pattern 4: Full line-by-line rendering (Complex editorial)**
95```js
96import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
98const segs = prepareWithSegments(text, font)
99const { lines, height } = layoutWithLines(segs, containerWidth, lineHeight)
101// lines = [{ text, width, start, end }, ...] (no x/y: line i sits at y = i * lineHeight)
102// Use for Canvas/SVG rendering or custom DOM positioning
103lines.forEach((line, i) => {
104 const span = document.createElement('span')
105 span.textContent = line.text
106 span.style.position = 'absolute'
107 span.style.left = '0px'
108 span.style.top = `${i * lineHeight}px`
109 container.appendChild(span)
110})
111```

Pretext Wiring Patterns

Use these patterns based on the tier selected in Step 2. These are the correct Pretext API usage patterns. Follow them exactly.

Pattern 1: Basic height computation (Simple layout, Card/grid)

import { prepare, layout } from '@chenglou/pretext'
// Vanilla HTML: omit the import; this code follows the inlined bundle in the same <script type="module">

// 1. PREPARE — one-time, after fonts load
await document.fonts.ready
const elements = document.querySelectorAll('[data-pretext]')
const prepared = new Map()

for (const el of elements) {
  const text = el.textContent
  const font = getComputedStyle(el).font
  prepared.set(el, prepare(text, font))
}

// 2. LAYOUT — cheap, call on every resize
function relayout() {
  for (const [el, handle] of prepared) {
    const { height } = layout(handle, el.clientWidth, parseFloat(getComputedStyle(el).lineHeight))
    el.style.height = `${height}px`
  }
}

// 3. RESIZE-AWARE
new ResizeObserver(() => relayout()).observe(document.body)
relayout()

// 4. CONTENT-EDITABLE — re-prepare when text changes
for (const el of elements) {
  if (el.contentEditable === 'true') {
    new MutationObserver(() => {
      const font = getComputedStyle(el).font
      prepared.set(el, prepare(el.textContent, font))
      relayout()
    }).observe(el, { characterData: true, subtree: true, childList: true })
  }
}

Pattern 2: Shrinkwrap / tight-fit containers (Chat bubbles)

import { prepareWithSegments, walkLineRanges } from '@chenglou/pretext'

// Tight-fit width: lay out at maxWidth, then shrink the bubble to its widest line
function shrinkwrap(text, font, maxWidth) {
  const segs = prepareWithSegments(text, font)
  let widest = 0
  const lineCount = walkLineRanges(segs, maxWidth, (line) => {
    widest = Math.max(widest, line.width)
  })
  return { width: Math.ceil(widest), lineCount }
}

Pattern 3: Text around obstacles (Editorial layout)

import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext'

function layoutAroundObstacles(text, font, containerWidth, lineHeight, obstacles) {
  const segs = prepareWithSegments(text, font)
  let cursor = { segmentIndex: 0, graphemeIndex: 0 }
  let y = 0
  const lines = []

  while (true) {
    // Calculate available width at current y position, accounting for obstacles
    let availWidth = containerWidth
    for (const obs of obstacles) {
      if (y >= obs.top && y < obs.top + obs.height) {
        availWidth -= obs.width
      }
    }

    const result = layoutNextLine(segs, cursor, availWidth)
    if (!result) break

    lines.push({ text: result.text, width: result.width, x: 0, y })
    cursor = result.end
    y += lineHeight
  }

  return { lines, totalHeight: y }
}

Pattern 4: Full line-by-line rendering (Complex editorial)

import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'

const segs = prepareWithSegments(text, font)
const { lines, height } = layoutWithLines(segs, containerWidth, lineHeight)

// lines = [{ text, width, start, end }, ...] (no x/y: line i sits at y = i * lineHeight)
// Use for Canvas/SVG rendering or custom DOM positioning
lines.forEach((line, i) => {
  const span = document.createElement('span')
  span.textContent = line.text
  span.style.position = 'absolute'
  span.style.left = '0px'
  span.style.top = `${i * lineHeight}px`
  container.appendChild(span)
})
Raw lines 3 to 112
113### Pretext API Reference
115```
116PRETEXT API CHEATSHEET:
118prepare(text, font) → handle
119 One-time text measurement. Call after document.fonts.ready.
120 Font: CSS shorthand like '16px Inter' or 'bold 24px Georgia'.
122layout(prepared, maxWidth, lineHeight) → { height, lineCount }
123 Fast layout computation. Call on every resize. Sub-millisecond.
125prepareWithSegments(text, font) → handle
126 Like prepare() but enables line-level APIs below.
128layoutWithLines(segs, maxWidth, lineHeight) → { lineCount, height, lines: [{text, width, start, end}...] }
129 Full line-by-line breakdown (no x/y: line i is at y = i * lineHeight). For Canvas/SVG rendering.
131walkLineRanges(segs, maxWidth, onLine) → lineCount
132 Calls onLine({ width, start, end }) once per line at maxWidth (no text built).
133 The widest line's width is the tight-fit container width. For chat bubbles.
135layoutNextLine(segs, cursor, maxWidth) → { text, width, start, end } | null
136 Iterator. Different maxWidth per line = text around obstacles.
137 Start with cursor { segmentIndex: 0, graphemeIndex: 0 }; pass result.end next.
138 Returns null when text is exhausted. Takes no lineHeight: advance y yourself.
140clearCache() → void
141 Clears internal measurement caches. Use when cycling many fonts.
143setLocale(locale?) → void
144 Retargets word segmenter for future prepare() calls.
145```

Pretext API Reference

PRETEXT API CHEATSHEET:

prepare(text, font) → handle
  One-time text measurement. Call after document.fonts.ready.
  Font: CSS shorthand like '16px Inter' or 'bold 24px Georgia'.

layout(prepared, maxWidth, lineHeight) → { height, lineCount }
  Fast layout computation. Call on every resize. Sub-millisecond.

prepareWithSegments(text, font) → handle
  Like prepare() but enables line-level APIs below.

layoutWithLines(segs, maxWidth, lineHeight) → { lineCount, height, lines: [{text, width, start, end}...] }
  Full line-by-line breakdown (no x/y: line i is at y = i * lineHeight). For Canvas/SVG rendering.

walkLineRanges(segs, maxWidth, onLine) → lineCount
  Calls onLine({ width, start, end }) once per line at maxWidth (no text built).
  The widest line's width is the tight-fit container width. For chat bubbles.

layoutNextLine(segs, cursor, maxWidth) → { text, width, start, end } | null
  Iterator. Different maxWidth per line = text around obstacles.
  Start with cursor { segmentIndex: 0, graphemeIndex: 0 }; pass result.end next.
  Returns null when text is exhausted. Takes no lineHeight: advance y yourself.

clearCache() → void
  Clears internal measurement caches. Use when cycling many fonts.

setLocale(locale?) → void
  Retargets word segmenter for future prepare() calls.
Raw lines 113 to 145