From 8056c4fab550ef45d0ddb382c1d13db63fb9772e Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 27 Nov 2024 11:47:05 -0500 Subject: [PATCH] make tree logging work in the browser whoops forgot this earlier --- examples/bidi-1.ts | 2 +- examples/fallbacks-1.ts | 2 +- examples/images-1.ts | 2 +- examples/inlines-1.ts | 2 +- examples/rtl-1.ts | 2 +- examples/svg-1.ts | 2 +- site/index.js | 5 +- src/layout-box.ts | 47 ++++++++++------- src/layout-flow.ts | 37 ++++++-------- src/layout-text.ts | 95 +++------------------------------- src/util.ts | 110 ++++++++++++++++++++++++++++++++++++++++ 11 files changed, 171 insertions(+), 135 deletions(-) diff --git a/examples/bidi-1.ts b/examples/bidi-1.ts index 5557cdf..059d1d9 100644 --- a/examples/bidi-1.ts +++ b/examples/bidi-1.ts @@ -20,7 +20,7 @@ const rootElement = flow.parse(` const blockContainer = flow.generate(rootElement); -console.log(blockContainer.repr()); +blockContainer.log(); const canvas = createCanvas(200, 250); flow.layout(blockContainer, canvas.width, canvas.height); diff --git a/examples/fallbacks-1.ts b/examples/fallbacks-1.ts index 82abba8..fc5b230 100644 --- a/examples/fallbacks-1.ts +++ b/examples/fallbacks-1.ts @@ -18,7 +18,7 @@ const rootElement = flow.parse(` const blockContainer = flow.generate(rootElement); -console.log(blockContainer.repr()); +blockContainer.log(); const canvas = createCanvas(400, 150); flow.layout(blockContainer, canvas.width, canvas.height); diff --git a/examples/images-1.ts b/examples/images-1.ts index b1deb53..39abc14 100644 --- a/examples/images-1.ts +++ b/examples/images-1.ts @@ -56,7 +56,7 @@ const rootElement = flow.dom( // Normal layout, logging const blockContainer = flow.generate(rootElement); -console.log(blockContainer.repr()); +blockContainer.log(); flow.layout(blockContainer, 600, 400); const canvas = createCanvas(600, 400); const ctx = canvas.getContext('2d'); diff --git a/examples/inlines-1.ts b/examples/inlines-1.ts index a566ee1..38b0712 100644 --- a/examples/inlines-1.ts +++ b/examples/inlines-1.ts @@ -25,7 +25,7 @@ const rootElement = flow.parse(` `); const blockContainer = flow.generate(rootElement); -console.log(blockContainer.repr(0, {css: 'zoom'})); +blockContainer.log({css: 'zoom'}); const canvas = createCanvas(600, 400); flow.layout(blockContainer, canvas.width, canvas.height); diff --git a/examples/rtl-1.ts b/examples/rtl-1.ts index 21c2d60..442473b 100644 --- a/examples/rtl-1.ts +++ b/examples/rtl-1.ts @@ -25,7 +25,7 @@ const rootElement = flow.parse(` const blockContainer = flow.generate(rootElement); -console.log(blockContainer.repr()); +blockContainer.log(); const canvas = createCanvas(200, 600); flow.layout(blockContainer, canvas.width, canvas.height); diff --git a/examples/svg-1.ts b/examples/svg-1.ts index b6d4eba..0a31405 100644 --- a/examples/svg-1.ts +++ b/examples/svg-1.ts @@ -22,7 +22,7 @@ const doc = flow.parse(` await flow.loadNotoFonts(doc, {paint: false}); const block = flow.generate(doc); -console.log(block.repr()); +block.log(); flow.layout(block, 600, 100); const svg = flow.paintToSvg(block); diff --git a/site/index.js b/site/index.js index f750c6a..8c35326 100644 --- a/site/index.js +++ b/site/index.js @@ -29,7 +29,10 @@ async function render(html) { ctx.save(); ctx.clearRect(0, 0, canvas.width, canvas.height); - flow.renderToCanvas(documentElement, canvas); + const box = flow.generate(documentElement); + box.log(); + flow.layout(box, canvas.width, canvas.height); + flow.paintToCanvas(box, canvas.getContext('2d')); ctx.restore(); window.documentElement = documentElement; diff --git a/src/layout-box.ts b/src/layout-box.ts index 57a037f..7ea2571 100644 --- a/src/layout-box.ts +++ b/src/layout-box.ts @@ -1,4 +1,4 @@ -import {id} from './util.js'; +import {id, Logger} from './util.js'; import {Style} from './style.js'; import {Run} from './layout-text.js'; import {Break, Inline, IfcInline, BlockContainer} from './layout-flow.js'; @@ -10,7 +10,7 @@ export interface LogicalArea { inlineSize: number | undefined; } -export interface ReprOptions { +export interface RenderItemLogOptions { containingBlocks?: boolean; css?: keyof Style paragraphText?: string; @@ -110,38 +110,49 @@ export abstract class RenderItem { return false; } - abstract desc(options?: ReprOptions): string; + abstract logName(log: Logger, options?: RenderItemLogOptions): void; - abstract sym(): string; + abstract getLogSymbol(): string; - repr(indent = 0, options?: ReprOptions): string { - let c = ''; + log(options?: RenderItemLogOptions, log?: Logger) { + const flush = !log; + + log ||= new Logger(); if (this.isIfcInline()) { options = {...options}; options.paragraphText = this.text; } - if (this.isBox() && this.children.length) { - c = '\n' + this.children.map(c => c.repr(indent + 1, options)).join('\n'); - } - - let extra = ''; + log.text(`${this.getLogSymbol()} `); + this.logName(log, options); if (options?.containingBlocks && this.isBlockContainer()) { - extra += ` (cb = ${this.containingBlock ? this.containingBlock.box.id : '(null)'})`; + log.text(` (cb: ${this.containingBlock?.box.id ?? '(null)'})`); } if (options?.css) { const css = this.style[options.css]; - extra += ` (${options.css}: ${css && JSON.stringify(css)})`; + log.text(` (${options.css}: ${css && JSON.stringify(css)})`); } if (options?.bits && this.isBox()) { - extra += ` (bf: ${this.stringifyBitfield()})`; + log.text(` (bf: ${this.stringifyBitfield()})`); + } + + log.text('\n'); + + if (this.isBox() && this.children.length) { + log.pushIndent(); + + for (let i = 0; i < this.children.length; i++) { + this.children[i].log(options, log); + } + + log.popIndent(); } - return ' '.repeat(indent) + this.sym() + ' ' + this.desc(options) + extra + c; + if (flush) log.flush(); } preprocess() { @@ -266,11 +277,11 @@ export class Box extends RenderItem { } } - desc(options?: ReprOptions) { - return 'Box'; + logName(log: Logger, options?: RenderItemLogOptions) { + log.text('Box'); } - sym() { + getLogSymbol() { return '◼︎'; } diff --git a/src/layout-flow.ts b/src/layout-flow.ts index ffa51d3..799753e 100644 --- a/src/layout-flow.ts +++ b/src/layout-flow.ts @@ -1,4 +1,4 @@ -import {binarySearch} from './util.js'; +import {binarySearch, Logger} from './util.js'; import {HTMLElement, TextNode} from './dom.js'; import {createStyle, Style, EMPTY_STYLE} from './style.js'; import { @@ -43,10 +43,6 @@ function isWsPreserved(whiteSpace: WhiteSpace) { return whiteSpace === 'pre' || whiteSpace === 'pre-wrap'; } -const reset = '\x1b[0m'; -const dim = '\x1b[2m'; -const underline = '\x1b[4m'; - export interface LayoutContext { lastBlockContainerArea: BoxArea, lastPositionedArea: BoxArea, @@ -780,7 +776,7 @@ export class BlockContainer extends Box { } } - sym() { + getLogSymbol() { if (this.isFloat()) { return '○︎'; } else if (this.isInlineLevel()) { @@ -790,11 +786,11 @@ export class BlockContainer extends Box { } } - desc() { - return (this.isAnonymous() ? dim : '') - + (this.isBfcRoot() ? underline : '') - + 'Block ' + this.id - + reset; + logName(log: Logger) { + if (this.isAnonymous()) log.dim(); + if (this.isBfcRoot()) log.underline(); + log.text(`Block ${this.id}`); + log.reset(); } get writingModeAsParticipant() { @@ -1293,12 +1289,12 @@ export class Break extends RenderItem { return true; } - sym() { + getLogSymbol() { return '⏎'; } - desc() { - return 'BR'; + logName(log: Logger) { + log.text('BR'); } } @@ -1355,16 +1351,15 @@ export class Inline extends Box { return true; } - sym() { + getLogSymbol() { return '▭'; } - desc(): string /* TS 4.9 throws TS7023 - almost certainly a bug */ { - return (this.isAnonymous() ? dim : '') - + (this.isIfcInline() ? underline : '') - + 'Inline' - + ' ' + this.id - + reset; + logName(log: Logger) { + if (this.isAnonymous()) log.dim(); + if (this.isIfcInline()) log.underline(); + log.text(`Inline ${this.id}`); + log.reset(); } assignContainingBlocks(ctx: LayoutContext) { diff --git a/src/layout-text.ts b/src/layout-text.ts index 6cf48a0..6261a46 100644 --- a/src/layout-text.ts +++ b/src/layout-text.ts @@ -1,5 +1,5 @@ -import {binarySearchTuple, basename, loggableText} from './util.js'; -import {RenderItem, ReprOptions} from './layout-box.js'; +import {binarySearchTuple, basename, loggableText, Logger} from './util.js'; +import {RenderItem, RenderItemLogOptions} from './layout-box.js'; import {Style, Color, TextAlign, WhiteSpace} from './style.js'; import { BlockContainer, @@ -81,7 +81,7 @@ export class Run extends RenderItem { return this.end - this.start; } - sym() { + getLogSymbol() { return 'Ͳ'; } @@ -104,12 +104,11 @@ export class Run extends RenderItem { return true; } - desc(options?: ReprOptions) { - let ret = `${this.start},${this.end}`; + logName(log: Logger, options?: RenderItemLogOptions) { + log.text(`${this.start},${this.end}`); if (options?.paragraphText) { - ret += ` "${loggableText(options.paragraphText.slice(this.start, this.end))}"`; + log.text(` "${loggableText(options.paragraphText.slice(this.start, this.end))}"`); } - return ret; } } @@ -436,88 +435,6 @@ function nextGlyph(state: GlyphIteratorState) { } } -export class Logger { - string: string; - formats: string[]; // only for browsers - indent: string[]; - lineIsEmpty: boolean; - - constructor() { - this.string = ''; - this.formats = []; - this.indent = []; - this.lineIsEmpty = false; - } - - bold() { - if (typeof process === 'object') { - this.string += '\x1b[1m'; - } else { - this.string += '%c'; - this.formats.push('font-weight: bold'); - } - } - - reset() { - if (typeof process === 'object') { - this.string += '\x1b[0m'; - } else { - this.string += '%c'; - this.formats.push('font-weight: normal'); - } - } - - flush() { - console.log(this.string, ...this.formats); - this.string = ''; - this.formats = []; - } - - text(str: string | number) { - const lines = String(str).split('\n'); - - const append = (s: string) => { - if (s) { - if (this.lineIsEmpty) this.string += this.indent.join(''); - this.string += s; - this.lineIsEmpty = false; - } - }; - - for (let i = 0; i < lines.length; i++) { - if (i === 0) { - append(lines[i]); - } else { - this.string += '\n'; - this.lineIsEmpty = true; - append(lines[i]); - } - } - } - - glyphs(glyphs: Int32Array) { - for (let i = 0; i < glyphs.length; i += G_SZ) { - const cl = glyphs[i + G_CL]; - const isp = i - G_SZ >= 0 && glyphs[i - G_SZ + G_CL] === cl; - const isn = i + G_SZ < glyphs.length && glyphs[i + G_SZ + G_CL] === cl; - if (isp || isn) this.bold(); - if (isn && !isp) this.text('('); - this.text(glyphs[i + G_ID]); - if (!isn && isp) this.text(')'); - this.text(' '); - if (isp || isn) this.reset(); - } - } - - pushIndent(indent = ' ') { - this.indent.push(indent); - } - - popIndent() { - this.indent.pop(); - } -} - function shiftGlyphs(glyphs: Int32Array, offset: number, dir: 'ltr' | 'rtl') { if (dir === 'ltr') { for (let i = 0; i < glyphs.length; i += G_SZ) { diff --git a/src/util.ts b/src/util.ts index 077a527..5d74f91 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,6 @@ +import {G_ID, G_CL, G_SZ} from './layout-text.js'; +import type {Style} from './style.js'; + /** * Binary search that returns the position `x` should be in */ @@ -83,3 +86,110 @@ export function loggableText(text: string): string { export function basename(p: string) { return p.match(/([^.\/]+)\.[A-z]+$/)?.[1] || p; } + +export interface TreeLogOptions { + containingBlocks?: boolean; + css?: keyof Style + paragraphText?: string; + bits?: boolean; +} + +export class Logger { + string: string; + formats: string[]; // only for browsers + indent: string[]; + lineIsEmpty: boolean; + + constructor() { + this.string = ''; + this.formats = []; + this.indent = []; + this.lineIsEmpty = false; + } + + bold() { + if (typeof process === 'object') { + this.string += '\x1b[1m'; + } else { + this.string += '%c'; + this.formats.push('font-weight: bold'); + } + } + + underline() { + if (typeof process === 'object') { + this.string += '\x1b[4m'; + } else { + this.string += '%c'; + this.formats.push('text-decoration: underline'); + } + } + + dim() { + if (typeof process === 'object') { + this.string += '\x1b[2m'; + } else { + this.string += '%c'; + this.formats.push('color: gray'); + } + } + + reset() { + if (typeof process === 'object') { + this.string += '\x1b[0m'; + } else { + this.string += '%c'; + this.formats.push('font-weight: normal'); + } + } + + flush() { + console.log(this.string, ...this.formats); + this.string = ''; + this.formats = []; + } + + text(str: string | number) { + const lines = String(str).split('\n'); + + const append = (s: string) => { + if (s) { + if (this.lineIsEmpty) this.string += this.indent.join(''); + this.string += s; + this.lineIsEmpty = false; + } + }; + + for (let i = 0; i < lines.length; i++) { + if (i === 0) { + append(lines[i]); + } else { + this.string += '\n'; + this.lineIsEmpty = true; + append(lines[i]); + } + } + } + + glyphs(glyphs: Int32Array) { + for (let i = 0; i < glyphs.length; i += G_SZ) { + const cl = glyphs[i + G_CL]; + const isp = i - G_SZ >= 0 && glyphs[i - G_SZ + G_CL] === cl; + const isn = i + G_SZ < glyphs.length && glyphs[i + G_SZ + G_CL] === cl; + if (isp || isn) this.bold(); + if (isn && !isp) this.text('('); + this.text(glyphs[i + G_ID]); + if (!isn && isp) this.text(')'); + this.text(' '); + if (isp || isn) this.reset(); + } + } + + pushIndent(indent = ' ') { + this.indent.push(indent); + } + + popIndent() { + this.indent.pop(); + } +}