Last active
January 28, 2026 16:38
-
-
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
uhm ... I think the name was 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!
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;
}
/** @type {Element | null} */
get previousElementSibling() {
let previous = super.previousSibling;
while (previous && previous.nodeType !== ELEMENT_NODE)
previous = previous.previousSibling;
return previous;
}
/** @type {Element | null} */
get nextElementSibling() {
let next = super.nextSibling;
while (next && next.nodeType !== ELEMENT_NODE)
next = next.nextSibling;
return next;
}
}
//@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);
}
Author
This is the ProcessingInstruction variant:
//@ts-check
//@ts-ignore
import custom from 'https://cdn.jsdelivr.net/npm/custom-function/esm/factory.js';
/** @typedef {'marker' | 'start' | 'end'} MarkerType */
if (!globalThis.Marker) {
const { COMMENT_NODE, ELEMENT_NODE, PROCESSING_INSTRUCTION_NODE } = Node;
const fix = ({ $1, $2 }) => $1 ? $2 : $2.split(/\s/)[0];
/**
* @param {string} data
* @returns
*/
const name = data => / name=(['"])?(.+)?\1/.test(data) ? fix(RegExp) : '';
/**
* @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;
globalThis.Marker = class Marker extends custom(ProcessingInstruction) {
/** @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 data() {
return super.data;
}
set data(_) {
throw new SyntaxError('Cannot set data of a Marker');
}
/** @type {string} */
get nodeName() {
return '#marker';
}
/** @type {number} */
get nodeType() {
return PROCESSING_INSTRUCTION_NODE;
}
/** @type {string} */
get name() {
return this.#name;
}
/** @type {MarkerType} */
get type() {
return this.#type;
}
}
/**
* @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
Example: