UNPKG

@evan/wasm

Version:
503 lines (396 loc) 16.6 kB
let wasm; const rewriters = new Map; const __c__ = Symbol('lhc'); const handlers = new WeakMap; const callbacks = new WeakMap; { const module = new WebAssembly.Module(await import('fs/promises').then(fs => fs.readFile(new URL('../../src/html/html.wasm', import.meta.url)))); const instance = new WebAssembly.Instance(module, { io: { drop(id) { const x = rewriters.get(id); handlers.delete(x), callbacks.delete(x), rewriters.delete(id); } }, env: { chunk(id, ptr) { callbacks.get(rewriters.get(id))(mem.u8(ptr, mem.length()).slice()); }, element(id, kind, index, ptr) { const r = rewriters.get(id); const h = handlers.get(r).on[index]; try { if (0 === kind) { const text = new Text(ptr, wasm); (h.text(text), text[__c__]()); } else if (1 === kind) { const element = new Element(ptr, wasm); (h.element(element), element[__c__]()); } else if (2 === kind) { const comment = new Comment(ptr, wasm); (h.comments(comment), comment[__c__]()); } } catch (err) { return (rewriters.get(id).error = err, 0); } return 1; }, document(id, kind, index, ptr) { const r = rewriters.get(id); const h = handlers.get(r).document[index]; try { if (0 === kind) { const end = new End(ptr, wasm); (h.end(end), end[__c__]()); } else if (2 === kind) { const text = new Text(ptr, wasm); (h.text(text), text[__c__]()); } else if (1 === kind) { const doctype = new Doctype(ptr, wasm); (h.doctype(doctype), doctype[__c__]()); } else if (3 === kind) { const comment = new Comment(ptr, wasm); (h.comments(comment), comment[__c__]()); } } catch (err) { return (rewriters.get(id).error = err, 0); } return 1; }, }, }); wasm = instance.exports; } class mem { static length() { return wasm.wlen(); } static token() { return wasm.wtoken(); } static alloc(size) { return wasm.walloc(size); } static free(ptr, size) { return wasm.wfree(ptr, size); } static u8(ptr, size) { return new Uint8Array(wasm.memory.buffer, ptr, size); } static u32(ptr, size) { return new Uint32Array(wasm.memory.buffer, ptr, size); } static gc(f) { return !('FinalizationRegistry' in globalThis) ? { delete(_) { }, add(_, __) { } } : { r: new FinalizationRegistry(f), delete(k) { this.r.unregister(k); }, add(k, v) { this.r.register(k, v, k); } }; } static copy_and_free(ptr, size) { let slice = mem.u8(ptr, size).slice(); return (wasm.wfree(ptr, size), slice); } } const gc = mem.gc(ptr => wasm.free(ptr)); const decode_utf16 = Function.prototype.apply.bind(String.fromCharCode, null); const decode_utf8 = globalThis.Deno?.core?.decode ?? TextDecoder.prototype.decode.bind(new TextDecoder); const encode_ascii = x => { const l = x.length; const u8 = new Uint8Array(l); for (let o = 0; o < l; o++) u8[o] = x.charCodeAt(o); return u8; }; const encode_utf16 = x => { const l = x.length; const u16 = new Uint16Array(l); for (let o = 0; o < l; o++) u16[o] = x.charCodeAt(o); return u16; }; const encode_utf8 = globalThis.Deno?.core?.encode ?? globalThis.Buffer?.from.bind(globalThis.Buffer) ?? TextEncoder.prototype.encode.bind(new TextEncoder); export class Rewriter { #ptr = 0; #t = null; constructor(fn) { callbacks.set(this, fn); handlers.set(this, { on: [], document: [] }); } drop() { gc.delete(this); this.#ptr = wasm.free(this.#ptr); } end() { if (0 === this.#ptr) this.#init(); else if (null == this.#ptr) return; const c = wasm.end(this.#ptr); gc.delete(this); this.#ptr = null; if (3 === c) throw this.error; if (2 === c) throw new Error('html: memory limit has been exceeded'); if (1 === c) throw new Error('html: parser has encountered a text content tag in the context where it is ambiguous whether this tag should be ignored or not'); } write(chunk) { if (0 === this.#ptr) this.#init(); else if (null == this.#ptr) throw new Error('html: rewriter is ended'); if ('string' === typeof chunk) chunk = encode_utf8(chunk); const ptr = mem.alloc(chunk.length); mem.u8(ptr, chunk.length).set(chunk); const c = wasm.write(this.#ptr, ptr, chunk.length); if (3 === c) throw (this.drop(), this.error); if (2 === c) throw (this.drop(), new Error('html: memory limit has been exceeded')); if (1 === c) throw (this.drop(), new Error('html: parser has encountered a text content tag in the context where it is ambiguous whether this tag should be ignored or not')); } on(selector, handler) { if (0 !== this.#ptr) throw new Error('html: cannot add handler after writing'); const buf = encode_utf8(selector); const ptr = mem.alloc(buf.length); mem.u8(ptr, buf.length).set(buf); const c = wasm.validate_selector(ptr, buf.length); if (0 !== c) throw new SyntaxError(`html: ${decode_utf8(mem.u8(c, mem.length()))}`); const h = handlers.get(this).on; h.push(handler); const meta = handler.meta = []; if ('text' in handler) meta.push({ kind: 0, name: selector, index: h.length - 1 }); if ('element' in handler) meta.push({ kind: 1, name: selector, index: h.length - 1 }); if ('comments' in handler) meta.push({ kind: 2, name: selector, index: h.length - 1 }); } onDocument(handler) { if (0 !== this.#ptr) throw new Error('html: cannot add handler after writing'); const h = handlers.get(this).document; h.push(handler); const meta = handler.meta = []; if ('end' in handler) meta.push({ kind: 0, index: h.length - 1 }); if ('text' in handler) meta.push({ kind: 2, index: h.length - 1 }); if ('doctype' in handler) meta.push({ kind: 1, index: h.length - 1 }); if ('comments' in handler) meta.push({ kind: 3, index: h.length - 1 }); } #init() { const h = handlers.get(this); rewriters.set(this.#t = mem.token(), this); const json = encode_utf8(JSON.stringify({ strict: true, element_handlers: h.on.map(x => x.meta).flat(), document_handlers: h.document.map(x => x.meta).flat(), })); const ptr = mem.alloc(json.length); mem.u8(ptr, json.length).set(json); gc.add(this, this.#ptr = wasm.new(this.#t, callbacks.get(this) ? 1 : 0, ptr, json.length)); } } class End { #ptr = 0; #wasm = null; constructor(ptr, wasm) { this.#ptr = ptr; this.#wasm = wasm; } [__c__]() { this.#ptr = this.#wasm = null; } #guard() { if (!this.#ptr) throw new Error('html: using html content past its handler return is forbidden'); } append(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.end_append(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } } class Doctype { #ptr = 0; #wasm = null; constructor(ptr, wasm) { this.#ptr = ptr; this.#wasm = wasm; } [__c__]() { this.#ptr = this.#wasm = null; } #guard() { if (!this.#ptr) throw new Error('html: using html content past its handler return is forbidden'); } get name() { this.#guard(); const ptr = this.#wasm.doctype_name(this.#ptr); if (!ptr) return null; const str = decode_utf8(mem.u8(ptr, mem.length())); return (mem.free(ptr, mem.length()), str); } get publicId() { this.#guard(); const ptr = this.#wasm.doctype_public_id(this.#ptr); if (!ptr) return null; const str = decode_utf8(mem.u8(ptr, mem.length())); return (mem.free(ptr, mem.length()), str); } get systemId() { this.#guard(); const ptr = this.#wasm.doctype_system_id(this.#ptr); if (!ptr) return null; const str = decode_utf8(mem.u8(ptr, mem.length())); return (mem.free(ptr, mem.length()), str); } } class Comment { #ptr = 0; #wasm = null; constructor(ptr, wasm) { this.#ptr = ptr; this.#wasm = wasm; } [__c__]() { this.#ptr = this.#wasm = null; } #guard() { if (!this.#ptr) throw new Error('html: using html content past its handler return is forbidden'); } remove() { this.#guard(); this.#wasm.comment_remove(this.#ptr); return this; } get removed() { this.#guard(); return !!this.#wasm.comment_removed(this.#ptr); } get text() { this.#guard(); const ptr = this.#wasm.comment_text_get(this.#ptr); const str = decode_utf8(mem.u8(ptr, mem.length())); return (mem.free(ptr, mem.length()), str); } set text(content) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); const c = this.#wasm.comment_text_set(this.#ptr, ptr, buffer.length); if (2 === c) throw new Error('html: comment text shouldn\'t contain comment closing sequence (`-->`)'); if (1 === c) throw new Error('html: comment text contains a character that can\'t be represented in utf8'); } after(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.comment_after(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } before(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.comment_before(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } replace(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.comment_replace(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } } class Text { #ptr = 0; #wasm = null; constructor(ptr, wasm) { this.#ptr = ptr; this.#wasm = wasm; } [__c__]() { this.#ptr = this.#wasm = null; } #guard() { if (!this.#ptr) throw new Error('html: using html content past its handler return is forbidden'); } remove() { this.#guard(); this.#wasm.text_remove(this.#ptr); return this; } get removed() { this.#guard(); return !!this.#wasm.text_removed(this.#ptr); } get lastInTextNode() { this.#guard(); return !!this.#wasm.text_last_in_text_node(this.#ptr); } get text() { this.#guard(); const ptr = this.#wasm.text_text_get(this.#ptr); return 0 === ptr ? '' : decode_utf8(mem.u8(ptr, mem.length())); } get buffer() { this.#guard(); const ptr = this.#wasm.text_text_get(this.#ptr); return 0 === ptr ? new Uint8Array(0) : mem.u8(ptr, mem.length()); } after(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.text_after(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } before(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.text_before(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } replace(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.text_replace(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } } class Element { #ptr = 0; #wasm = null; constructor(ptr, wasm) { this.#ptr = ptr; this.#wasm = wasm; } [__c__]() { this.#ptr = this.#wasm = null; } #guard() { if (!this.#ptr) throw new Error('html: using html content past its handler return is forbidden'); } remove() { this.#guard(); this.#wasm.element_remove(this.#ptr); return this; } get removed() { this.#guard(); return !!this.#wasm.element_removed(this.#ptr); } removeAndKeepContent() { this.#guard(); this.#wasm.element_remove_and_keep_content(this.#ptr); return this; } get namespaceURI() { this.#guard(); return decode_utf8(mem.u8(this.#wasm.element_namespace_uri(this.#ptr), mem.length())); } get tagName() { this.#guard(); const ptr = this.#wasm.element_tag_name(this.#ptr); const str = decode_utf8(mem.u8(ptr, mem.length())); return (mem.free(ptr, mem.length()), str); } get attributes() { this.#guard(); const ptr = this.#wasm.element_attributes(this.#ptr); const attributes = JSON.parse(decode_utf8(mem.u8(ptr, mem.length()))); return (mem.free(ptr, mem.length()), attributes.values()); } removeAttribute(name) { this.#guard(); const buffer = encode_utf8(name); const bptr = mem.alloc(buffer.length); mem.u8(bptr, buffer.length).set(buffer); this.#wasm.element_remove_attribute(this.#ptr, bptr, buffer.length); return this; } hasAttribute(name) { this.#guard(); const buffer = encode_utf8(name); const bptr = mem.alloc(buffer.length); mem.u8(bptr, buffer.length).set(buffer); return !!this.#wasm.element_has_attribute(this.#ptr, bptr, buffer.length); } getAttribute(name) { this.#guard(); const buffer = encode_utf8(name); const bptr = mem.alloc(buffer.length); mem.u8(bptr, buffer.length).set(buffer); const ptr = this.#wasm.element_get_attribute(this.#ptr, bptr, buffer.length); if (!ptr) return null; const len = mem.length(); const str = decode_utf8(mem.u8(ptr, len)); return (mem.free(ptr, len), str); } after(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.element_after(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } append(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.element_append(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } before(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.element_before(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } prepend(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.element_prepend(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } replace(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.element_replace(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } setInnerContent(content, options = {}) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); this.#wasm.element_set_inner_content(this.#ptr, ptr, buffer.length, options.html ? 1 : 0); return this; } set tagName(content) { this.#guard(); const buffer = encode_utf8(content); const ptr = mem.alloc(buffer.length); mem.u8(ptr, buffer.length).set(buffer); const c = this.#wasm.element_set_tag_name(this.#ptr, ptr, buffer.length); if (1 === c) throw new Error('html: tag name can\'t be empty'); if (3 === c) throw new Error('html: forbidden character is in the tag name'); if (2 === c) throw new Error('html: tag name contains a character that can\'t be represented in utf8'); if (4 === c) throw new Error('html: first character of the tag name should be an ASCII alphabetical character'); } setAttribute(name, value) { this.#guard(); const v = encode_utf8(value); const key = encode_utf8(name); const vptr = mem.alloc(v.length); const kptr = mem.alloc(key.length); mem.u8(vptr, v.length).set(v); mem.u8(kptr, key.length).set(key); const c = this.#wasm.element_set_attribute(this.#ptr, kptr, key.length, vptr, v.length); if (1 === c) throw new Error('html: attribute name can\'t be empty'); if (3 === c) throw new Error('html: forbidden character is in the attribute name'); if (2 === c) throw new Error('html: attribute name contains a character that can\'t be represented in utf8'); return this; } }