dropflow
Version:
A small CSS2 document renderer built from specifications
539 lines (538 loc) • 18.4 kB
JavaScript
import wasm from './wasm.js';
import { onWasmMemoryResized } from './wasm-env.js';
import { codeToName } from '../gen/script-names.js';
import * as hb from './text-harfbuzz.js';
import * as EmojiTrie from './trie-emoji.js';
import * as ScriptTrie from './trie-script.js';
const {
// SheenBidi
SBAlgorithmCreate, SBAlgorithmRelease, SBAlgorithmGetParagraphBoundary, SBAlgorithmCreateParagraph, SBParagraphRelease, SBParagraphGetLevelsPtr,
// emoji-segmenter
emoji_scan, malloc, free, memory } = wasm.instance.exports;
let heapu32 = new Uint32Array(memory.buffer);
let heapu8 = new Uint8Array(memory.buffer);
let heapu16 = new Uint16Array(memory.buffer);
onWasmMemoryResized(() => {
heapu32 = new Uint32Array(memory.buffer);
heapu8 = new Uint8Array(memory.buffer);
heapu16 = new Uint16Array(memory.buffer);
});
const seqPtr = malloc(12); // sizeof(SBCodepointSequence)
const seqPtr32 = seqPtr >>> 2; // sizeof(SBCodepointSequence)
const paraLenPtr = malloc(4 /* sizeof(SBUInteger) */);
const paraLenPtr32 = paraLenPtr >>> 2;
const paraSepPtr = malloc(4 /* sizeof(SBUInteger) */);
const paraSepPtr32 = paraSepPtr >>> 2;
// exported for testing
export function createBidiIteratorState(stringPtr, stringLength, initialLevel = 0) {
// first byte is 1 because 1 === SBStringEncodingUTF16
heapu32[seqPtr32] = 1;
heapu32[seqPtr32 + 1] = stringPtr;
heapu32[seqPtr32 + 2] = stringLength;
return {
offset: 0,
stringLength,
paragraphStart: 0,
paragraphEnd: 0,
algorithmPtr: SBAlgorithmCreate(seqPtr),
paragraphPtr: 0,
levelsPtr: 0,
initialLevel,
level: 0,
done: false
};
}
// exported for testing
export function bidiIteratorNext(state) {
if (state.done)
return;
state.level = heapu8[state.levelsPtr + state.offset - state.paragraphStart];
outer: while (state.offset < state.stringLength) {
if (state.offset === state.paragraphEnd) {
if (state.paragraphPtr)
SBParagraphRelease(state.paragraphPtr);
heapu32[paraLenPtr32] = 0;
heapu32[paraSepPtr32] = 0;
SBAlgorithmGetParagraphBoundary(state.algorithmPtr, state.offset, state.stringLength - state.offset, paraLenPtr, paraSepPtr);
const paraLen = heapu32[paraLenPtr32] + heapu32[paraSepPtr32];
state.paragraphStart = state.paragraphEnd;
state.paragraphEnd = state.offset + paraLen;
state.paragraphPtr = SBAlgorithmCreateParagraph(state.algorithmPtr, state.offset, paraLen, state.initialLevel);
state.levelsPtr = SBParagraphGetLevelsPtr(state.paragraphPtr);
if (state.offset === 0)
state.level = heapu8[state.levelsPtr];
}
while (state.offset < state.paragraphEnd) {
if (heapu8[state.levelsPtr + state.offset - state.paragraphStart] !== state.level)
break outer;
state.offset += 1;
}
}
if (state.offset === state.stringLength) {
if (state.paragraphPtr)
SBParagraphRelease(state.paragraphPtr);
state.done = true;
SBAlgorithmRelease(state.algorithmPtr);
}
}
// Some unicode char constants from Pango
const kCombiningEnclosingCircleBackslashCharacter = 0x20E0;
const kCombiningEnclosingKeycapCharacter = 0x20E3;
const kVariationSelector15Character = 0xFE0E;
const kVariationSelector16Character = 0xFE0F;
const kZeroWidthJoinerCharacter = 0x200D;
// Scanner categories
const EMOJI_TEXT_PRESENTATION = 1;
const EMOJI_EMOJI_PRESENTATION = 2;
const EMOJI_MODIFIER_BASE = 3;
const EMOJI_MODIFIER = 4;
const REGIONAL_INDICATOR = 6;
const KEYCAP_BASE = 7;
const COMBINING_ENCLOSING_KEYCAP = 8;
const COMBINING_ENCLOSING_CIRCLE_BACKSLASH = 9;
const ZWJ = 10;
const VS15 = 11;
const VS16 = 12;
const TAG_BASE = 13;
const TAG_SEQUENCE = 14;
const TAG_TERM = 15;
const kMaxEmojiScannerCategory = 1;
export function createEmojiIteratorState(stringPtr, stringLength) {
const stringPtr16 = stringPtr >>> 1;
const types = [];
const offsets = [];
for (let i = 0; i < stringLength; ++i) {
let code = heapu16[stringPtr16 + i];
const next = heapu16[stringPtr16 + i + 1];
offsets.push(i);
// If a surrogate pair
if ((0xd800 <= code && code <= 0xdbff) && (0xdc00 <= next && next <= 0xdfff)) {
i += 1;
code = ((code - 0xd800) * 0x400) + (next - 0xdc00) + 0x10000;
}
if (code === kCombiningEnclosingKeycapCharacter) {
types.push(COMBINING_ENCLOSING_KEYCAP);
}
else if (code === kCombiningEnclosingCircleBackslashCharacter) {
types.push(COMBINING_ENCLOSING_CIRCLE_BACKSLASH);
}
else if (code === kZeroWidthJoinerCharacter) {
types.push(ZWJ);
}
else if (code === kVariationSelector15Character) {
types.push(VS15);
}
else if (code === kVariationSelector16Character) {
types.push(VS16);
}
else if (code === 0x1f3f4) {
types.push(TAG_BASE);
}
else if (code >= 0xe0030 && code <= 0xe0039 || code >= 0xe0061 && code <= 0xe007a) {
types.push(TAG_SEQUENCE);
}
else if (code === 0xE007F) {
types.push(TAG_TERM);
}
else if (EmojiTrie.trie.get(code) & EmojiTrie.Emoji_Modifier_Base) {
types.push(EMOJI_MODIFIER_BASE);
}
else if (EmojiTrie.trie.get(code) & EmojiTrie.Emoji_Modifier) {
types.push(EMOJI_MODIFIER);
}
else if (code >= 0x1f1e6 && code <= 0x1f1ff) {
types.push(REGIONAL_INDICATOR);
}
else if ((code >= 48 && code <= 57) || code === 35 || code === 42) {
types.push(KEYCAP_BASE);
}
else if (EmojiTrie.trie.get(code) & EmojiTrie.Emoji_Presentation) {
types.push(EMOJI_EMOJI_PRESENTATION);
}
else if (EmojiTrie.trie.get(code) & EmojiTrie.Emoji) {
types.push(EMOJI_TEXT_PRESENTATION);
}
else {
types.push(kMaxEmojiScannerCategory);
}
}
const typesPtr = malloc(types.length);
heapu8.set(types, typesPtr);
offsets.push(stringLength);
return {
index: 0,
typesPtr,
typesLength: types.length,
offsets,
isEmoji: false,
offset: 0,
done: false
};
}
const isEmojiPtr = malloc(1);
export function emojiIteratorNext(state) {
if (state.done)
return;
const end = state.typesPtr + state.typesLength;
let p = state.typesPtr + state.index;
state.isEmoji = Boolean(heapu8[isEmojiPtr]);
state.offset = state.offsets[state.index];
while (p < end) {
p = emoji_scan(p, end, isEmojiPtr);
const isEmoji = Boolean(heapu8[isEmojiPtr]);
if (state.index === 0)
state.isEmoji = isEmoji;
state.index = p - state.typesPtr;
if (isEmoji !== state.isEmoji)
return;
state.offset = state.offsets[state.index];
}
state.offset = state.offsets.at(-1);
state.done = true;
free(state.typesPtr);
}
const pairedChars = [
0x0028, 0x0029, /* ascii paired punctuation */
0x003c, 0x003e,
0x005b, 0x005d,
0x007b, 0x007d,
0x00ab, 0x00bb, /* guillemets */
0x0f3a, 0x0f3b, /* tibetan */
0x0f3c, 0x0f3d,
0x169b, 0x169c, /* ogham */
0x2018, 0x2019, /* general punctuation */
0x201c, 0x201d,
0x2039, 0x203a,
0x2045, 0x2046,
0x207d, 0x207e,
0x208d, 0x208e,
0x27e6, 0x27e7, /* math */
0x27e8, 0x27e9,
0x27ea, 0x27eb,
0x27ec, 0x27ed,
0x27ee, 0x27ef,
0x2983, 0x2984,
0x2985, 0x2986,
0x2987, 0x2988,
0x2989, 0x298a,
0x298b, 0x298c,
0x298d, 0x298e,
0x298f, 0x2990,
0x2991, 0x2992,
0x2993, 0x2994,
0x2995, 0x2996,
0x2997, 0x2998,
0x29fc, 0x29fd,
0x2e02, 0x2e03,
0x2e04, 0x2e05,
0x2e09, 0x2e0a,
0x2e0c, 0x2e0d,
0x2e1c, 0x2e1d,
0x2e20, 0x2e21,
0x2e22, 0x2e23,
0x2e24, 0x2e25,
0x2e26, 0x2e27,
0x2e28, 0x2e29,
0x3008, 0x3009, /* chinese paired punctuation */
0x300a, 0x300b,
0x300c, 0x300d,
0x300e, 0x300f,
0x3010, 0x3011,
0x3014, 0x3015,
0x3016, 0x3017,
0x3018, 0x3019,
0x301a, 0x301b,
0xfe59, 0xfe5a,
0xfe5b, 0xfe5c,
0xfe5d, 0xfe5e,
0xff08, 0xff09,
0xff3b, 0xff3d,
0xff5b, 0xff5d,
0xff5f, 0xff60,
0xff62, 0xff63
];
function getPairIndex(ch) {
let lower = 0;
let upper = pairedChars.length - 1;
while (lower <= upper) {
const mid = Math.floor((lower + upper) / 2);
if (ch < pairedChars[mid]) {
upper = mid - 1;
}
else if (ch > pairedChars[mid]) {
lower = mid + 1;
}
else {
return mid;
}
}
return -1;
}
export function createScriptIteratorState(stringPtr, stringLength) {
return {
offset: 0,
stringPtr16: stringPtr >>> 1,
stringLength,
script: '',
parens: [],
startParen: -1,
done: false
};
}
export function scriptIteratorNext(state) {
if (state.done)
return;
state.script = 'Common';
const parens = state.parens;
while (state.offset < state.stringLength) {
const next = heapu16[state.stringPtr16 + state.offset + 1];
let code = heapu16[state.stringPtr16 + state.offset];
let jump = 1;
// If a surrogate pair
if ((0xd800 <= code && code <= 0xdbff) && (0xdc00 <= next && next <= 0xdfff)) {
jump += 1;
code = ((code - 0xd800) * 0xd400) + (next - 0xdc00) + 0x10000;
}
let script = codeToName.get(ScriptTrie.trie.get(code)) || 'Common';
const pairIndex = script !== 'Common' ? -1 : getPairIndex(code);
// Paired character handling:
// if it's an open character, push it onto the stack
// if it's a close character, find the matching open on the stack, and use
// that script code. Any non-matching open characters above it on the stack
// will be popped.
if (pairIndex >= 0) {
if ((pairIndex & 1) === 0) {
parens.push({ index: pairIndex, script: state.script });
}
else if (parens.length > 0) {
const pi = pairIndex & ~1;
while (parens.length && parens[parens.length - 1].index !== pi) {
parens.pop();
}
if (parens.length - 1 < state.startParen) {
state.startParen = parens.length - 1;
}
if (parens.length > 0) {
script = parens[parens.length - 1].script;
}
}
}
const runningIsReal = state.script !== 'Common' && state.script !== 'Inherited';
const isReal = script !== 'Common' && script !== 'Inherited';
const isSame = !runningIsReal || !isReal || script === state.script;
if (isSame) {
if (!runningIsReal && isReal) {
state.script = script;
// Now that we have a final script code, fix any open characters we
// pushed before we knew the real script code.
while (parens[state.startParen + 1])
parens[++state.startParen].script = script;
if (pairIndex >= 0 && pairIndex & 1 && parens.length > 0) {
parens.pop();
if (parens.length - 1 < state.startParen) {
state.startParen = parens.length - 1;
}
}
}
state.offset += jump;
}
else {
state.startParen = parens.length - 1;
break;
}
}
if (state.offset === state.stringLength) {
state.done = true;
}
}
export function createNewlineIteratorState(str) {
return { offset: 0, str, done: false };
}
export function newlineIteratorNext(state) {
if (state.done)
return;
const next = state.str.indexOf('\n', state.offset);
if (next < 0) {
state.offset = state.str.length;
}
else {
state.offset = next + 1;
}
if (state.offset === state.str.length)
state.done = true;
}
const END_CHILDREN = Symbol('end of children');
export function createStyleIteratorState(ifc) {
return {
parents: [ifc],
stack: ifc.children.slice().reverse(),
leader: ifc,
direction: ifc.style.direction,
style: ifc.style,
offset: 0,
lastOffset: 0,
ifc,
done: false
};
}
export function styleIteratorNext(state) {
if (state.done)
return;
state.lastOffset = state.offset;
if (state.leader !== END_CHILDREN) {
state.style = state.leader.style;
if (state.leader.isRun())
state.offset += state.leader.length;
}
while (state.stack.length) {
const item = state.stack.pop();
const parent = state.parents.at(-1);
if (item === END_CHILDREN) {
state.parents.pop();
if (state.direction === 'ltr' ? parent.hasLineRightGap() : parent.hasLineLeftGap()) {
if (state.offset !== state.lastOffset) {
state.leader = item;
break;
}
}
if (parent.style.verticalAlign !== 'baseline' ||
parent.style.position === 'relative') {
if (state.offset !== state.lastOffset) {
state.leader = item;
break;
}
}
}
else if (item.isRun()) {
if (!state.style.fontsEqual(item.style)) {
if (state.offset !== state.lastOffset) {
state.leader = item;
break;
}
state.style = item.style;
}
state.offset += item.length;
}
else if (item.isInline()) {
state.parents.push(item);
state.stack.push(END_CHILDREN);
for (let i = item.children.length - 1; i >= 0; --i) {
state.stack.push(item.children[i]);
}
if (item.style.verticalAlign !== 'baseline' ||
item.style.position === 'relative') {
if (state.offset !== state.lastOffset) {
state.leader = item;
break;
}
}
if (state.direction === 'ltr' ? item.hasLineLeftGap() : item.hasLineRightGap()) {
if (state.offset !== state.lastOffset) {
state.leader = item;
break;
}
}
}
else if (item.isBreak() || item.isReplacedBox()) {
if (state.offset !== state.lastOffset) {
state.leader = item;
break;
}
}
else if (item.isFloat()) {
// OK
}
else { // inline-block
if (state.offset !== state.lastOffset) {
state.leader = item;
break;
}
}
}
if (state.offset === state.ifc.text.length)
state.done = true;
}
export function createItemizeState(ifc) {
let newlineState;
let inlineState;
let emojiState;
let bidiState;
let scriptState;
let free;
if (ifc.hasNewlines()) {
newlineState = createNewlineIteratorState(ifc.text);
}
if (ifc.hasBreakOrInlineOrReplaced() || ifc.hasInlineBlocks()) {
inlineState = createStyleIteratorState(ifc);
}
if (ifc.hasComplexText()) {
const allocation = hb.allocateUint16Array(ifc.text.length);
const initialLevel = ifc.style.direction === 'ltr' ? 0 : 1;
const array = allocation.array;
free = allocation.destroy;
for (let i = 0; i < ifc.text.length; i++)
array[i] = ifc.text.charCodeAt(i);
emojiState = createEmojiIteratorState(array.byteOffset, array.length);
bidiState = createBidiIteratorState(array.byteOffset, array.length, initialLevel);
scriptState = createScriptIteratorState(array.byteOffset, array.length);
}
const attrs = {
isEmoji: emojiState?.isEmoji ?? false,
level: bidiState?.level ?? 0,
script: scriptState?.script ?? 'Latin',
style: inlineState?.style ?? ifc.style
};
return {
attrs,
offset: 0,
done: false,
newlineState,
inlineState,
emojiState,
bidiState,
scriptState,
simple: !newlineState && !inlineState && !emojiState && !bidiState && !scriptState,
length: ifc.text.length,
free
};
}
export function itemizeNext(state) {
if (state.done)
return;
if (state.simple) {
state.offset = state.length;
state.done = true;
return;
}
const { newlineState, inlineState, emojiState, bidiState, scriptState, offset } = state;
// Advance
if (newlineState?.offset === offset)
newlineIteratorNext(newlineState);
if (inlineState?.offset === offset)
styleIteratorNext(inlineState);
if (emojiState?.offset === offset)
emojiIteratorNext(emojiState);
if (bidiState?.offset === offset)
bidiIteratorNext(bidiState);
if (scriptState?.offset === offset)
scriptIteratorNext(scriptState);
// Map the current iterators to context
if (inlineState)
state.attrs.style = inlineState.style;
if (emojiState)
state.attrs.isEmoji = emojiState.isEmoji;
if (bidiState)
state.attrs.level = bidiState.level;
if (scriptState)
state.attrs.script = scriptState.script;
state.offset = Math.min(newlineState?.offset ?? Infinity, inlineState?.offset ?? Infinity, emojiState?.offset ?? Infinity, bidiState?.offset ?? Infinity, scriptState?.offset ?? Infinity, state.length);
if ((!newlineState || newlineState.done) &&
(!inlineState || inlineState.done) &&
(!emojiState || emojiState.done) &&
(!bidiState || bidiState.done) &&
(!scriptState || scriptState.done)) {
state.done = true;
state.free?.();
return;
}
}