Last active
January 23, 2026 15:03
-
-
Save WebReflection/291357aa6bbbd97d54a971ce5f5aae4e to your computer and use it in GitHub Desktop.
DOM Marker Interface
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //@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); | |
| } |
Author
Author
OK, this looks like a better approach:
//@ts-check
//@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;
/**
* @param {string} data
* @returns
*/
const name = data => / name=(['"])?(.+)?\1/.test(data) ? RegExp.$2 : '';
/**
* @param {Comment} node
* @param {MarkerType} type
* @returns {Marker}
*/
const promote = (node, type) => {
promoted = node;
try {
return new Marker(type, name(node.data.slice(type.length)));
}
finally {
promoted = void 0;
}
};
let promoted;
class Marker extends custom(Node) {
/** @type {string} */
#name;
/** @type {MarkerType} */
#type;
/**
* @param {MarkerType} type
* @param {string} [name]
*/
constructor(type, name = '') {
super(promoted ?? document.createComment(type));
this.#name = name;
this.#type = type;
}
/** @type {string} */
get nodeName() {
return '#marker';
}
/** @type {number} */
get nodeType() {
return MARKER_NODE;
}
/** @type {string} */
get name() {
return this.#name;
}
/** @type {MarkerType} */
get type() {
return this.#type;
}
}
//@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 = document.createTreeWalker(parent, NodeFilter.SHOW_COMMENT);
let node;
while (node = tw.nextNode()) check(/** @type {Comment} */(node));
};
const { attachShadow: $ } = Element.prototype;
const mode = { childList: true, subtree: true };
/**
*
* @param {ShadowRootInit} init
* @returns {ShadowRoot}
*/
Element.prototype.attachShadow = function attachShadow(init) {
const sr = $.call(this, init);
mo.observe(sr, mode);
return sr;
};
const { documentElement } = document;
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;
}
}
}
});
mo.observe(documentElement, mode);
walk(documentElement);
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
uhm ... I think the
namewas meant to be an attribute ... I've missed that part, if that's all a marker can have as "attribute" it could be simpler then (and attributes can be indeed removed) ... will write an even closer to specs one!