Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active June 25, 2026 11:42
Show Gist options
  • Select an option

  • Save WebReflection/291357aa6bbbd97d54a971ce5f5aae4e to your computer and use it in GitHub Desktop.

Select an option

Save WebReflection/291357aa6bbbd97d54a971ce5f5aae4e to your computer and use it in GitHub Desktop.
DOM Marker Interface
//@ts-check
// ⚠️ UPDATED https://gist.github.com/WebReflection/291357aa6bbbd97d54a971ce5f5aae4e?permalink_comment_id=5952616#gistcomment-5952616
//@ts-ignore
import custom from 'https://cdn.jsdelivr.net/npm/custom-function/esm/factory.js';
/** @typedef {'marker' | 'start' | 'end'} MarkerType */
if (!('MARKER_NODE' in Node)) {
const { COMMENT_NODE, ELEMENT_NODE } = Node;
const MARKER_NODE = 13;
const re = /(\w+)([\s>]|=(['"])?(.*)?\3)?/g;
/**
* @param {string} data
* @returns
*/
const parse = data => {
const attributes = {};
let match;
while (match = re.exec(data)) {
const [, name, _, __, value] = match;
attributes[name] = value ?? true;
}
return attributes;
};
/**
* @param {Comment} node
* @param {MarkerType} type
* @returns {Marker}
*/
const promote = (node, type) => {
promoted = node;
try {
return new Marker(type, '', parse(node.data.slice(type.length + 1)));
}
finally {
promoted = void 0;
}
};
let promoted;
class Marker extends custom(Node) {
#attributes;
/** @type {string} */
#name;
/** @type {MarkerType} */
#type;
/**
* @param {MarkerType} type
* @param {string} [name]
*/
constructor(type, name = '', attributes = {}) {
super(promoted ?? document.createComment(type));
this.#attributes = attributes;
this.#name = name;
this.#type = type;
}
get attributes() {
return this.#attributes;
}
/** @returns {number} */
get nodeType() {
return MARKER_NODE;
}
/** @returns {string} */
get name() {
return this.#name;
}
/** @returns {MarkerType} */
get type() {
return this.#type;
}
/**
* @param {string} name
* @returns {boolean}
*/
hasAttribute(name) {
return !!this.#attributes.hasOwnProperty(name);
}
/**
* @param {string} name
* @returns {string | null}
*/
getAttribute(name) {
return this.#attributes[name] ?? null;
}
/**
* @param {string} name
* @param {string | boolean} value
*/
setAttribute(name, value) {
this.#attributes[name] = value;
}
removeAttribute(name) {
delete this.#attributes[name];
}
}
//@ts-ignore
Node.MARKER_NODE = MARKER_NODE;
/**
* @param {Comment} node
*/
const check = node => {
if (/^(start|end|marker)(?:$|\s+)/.test(node.data))
promote(node, /** @type {MarkerType} */ (RegExp.$1));
};
/**
* @param {Element} parent
*/
const walk = parent => {
const tw = parent.ownerDocument.createTreeWalker(parent, NodeFilter.SHOW_COMMENT);
let node;
while (node = tw.nextNode()) check(/** @type {Comment} */ (node));
};
const mo = new MutationObserver(records => {
for (const record of records) {
for (const node of record.addedNodes) {
switch (node.nodeType) {
case COMMENT_NODE:
check(/** @type {Comment} */ (node));
break;
case ELEMENT_NODE:
walk(/** @type {Element} */ (node));
break;
}
}
}
});
const { documentElement } = document;
mo.observe(documentElement, { childList: true, subtree: true });
walk(documentElement);
}
@WebReflection

Copy link
Copy Markdown
Author

apparently we're resurrecting ProcessingInstruction instead and here we go with a polyfill https://github.com/ungap/processing-instruction

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment