feat: Complete zCode CLI X with Telegram bot integration

- Add full Telegram bot functionality with Z.AI API integration
- Implement 4 tools: Bash, FileEdit, WebSearch, Git
- Add 3 agents: Code Reviewer, Architect, DevOps Engineer
- Add 6 skills for common coding tasks
- Add systemd service file for 24/7 operation
- Add nginx configuration for HTTPS webhook
- Add comprehensive documentation
- Implement WebSocket server for real-time updates
- Add logging system with Winston
- Add environment validation

🤖 zCode CLI X - Agentic coder with Z.AI + Telegram integration
This commit is contained in:
admin
2026-05-05 09:01:26 +00:00
Unverified
parent 4a7035dd92
commit 875c7f9b91
24688 changed files with 3224957 additions and 221 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Jeremy Dorn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,208 @@
# DOM Mutator
For those times you need to apply persistent DOM changes on top of HTML you dont control.
View demo: https://growthbook.github.io/dom-mutator/
```ts
const mutation = mutate.html('#greeting', (html) => html + ' world');
// works even if the selector doesn't exist yet
document.body.innerHTML += "<div id='greeting'>hello</div>";
// "hello world"
// re-applies if there's an external change
document.getElementById('greeting').innerHTML = 'hola';
// "hola world"
// Revert to the last externally set value
mutation.revert();
// "hola"
```
```ts
import mutate from 'dom-mutator';
mutate.html('h1', (html) => html.toUpperCase());
mutate.classes('div.greeting', (classes) => classes.add('new-class'));
mutate.attribute(
'.get-started',
'title',
(oldVal) => 'This is my new title attribute'
);
```
Features:
- No dependencies, written in Typescript, 100% test coverage
- Super fast and light-weight (1Kb gzipped)
- Persists mutations even if the underlying element is updated externally (e.g. by a React render)
- Picks up new matching elements that are added to the DOM
- Easily remove a mutation at any time
![Build Status](https://github.com/growthbook/dom-mutator/workflows/CI/badge.svg)
## Installation
Install with npm or yarn (recommended):
`yarn add dom-mutator` OR `npm install --save dom-mutator`.
```js
import mutate from "dom-mutator";
...
```
OR use with unpkg:
```html
<script type="module">
import mutate from "https://unpkg.com/dom-mutator/dist/dom-mutator.esm.js";
...
</script>
```
## Usage
There are 4 mutate methods available: `html`, `classes`, `attribute`, and `declarative`.
### html
Mutate an element's innerHTML
```ts
// Signature
mutate.html(selector: string, (oldInnerHTML: string) => string);
// Example
mutate.html("h1", x => x.toUpperCase());
```
### classes
Mutate the set of classes for an element
```ts
// Signature
mutate.classes(selector: string, (classes: Set<string>) => void);
// Example
mutate.classes("h1", (classes) => {
classes.add("green");
classes.remove("red");
});
```
### attribute
Mutate the value of an HTML element's attribute
```ts
// Signature
mutate.attribute(selector: string, attribute: string, (oldValue: string) => string);
// Example
mutate.attribute(".link", "href", (href) => href + "?foo");
```
### position
Mutate the position of an HTML element by supplying a target parent element to append it to (and optional sibling element to place it next to).
```ts
// Signature
mutate.position(selector: string, () => ({ parentSelector: string; insertBeforeSelector?: string; }));
// Example
mutate.attribute(".link", () => ({ parentSelector: '.parent', insertBeforeSelector: 'p.body' }));
```
### declarative
Mutate the html, classes, or attributes using a declarative syntax instead of callbacks.
Perfect for serialization.
```ts
// Signature
mutate.declarative({
selector: string,
action: 'set' | 'append' | 'remove',
attribute: 'html' | 'class' | string,
value: string,
});
// Examples
const mutations = [
{
selector: 'h1',
action: 'set',
attribute: 'html',
value: 'new text',
},
{
selector: '.get-started',
action: 'remove',
attribute: 'class',
value: 'green',
},
{
selector: 'a',
action: 'append',
attribute: 'href',
value: '?foo',
},
{
selector: 'a',
action: 'set',
attribute: 'position',
parentSelector: '.header',
insertBeforeSelector: '.menu-button',
},
];
mutations.forEach((m) => mutate.declarative(m));
```
## How it Works
When you create a mutation, we start watching the document for elements matching the selector to appear. We do this with a single shared MutationObserver on the body.
When a matching element is found, we attach a separate MutationObserver filtered to the exact attribute being mutated. If an external change happens (e.g. from a React render), we re-apply your mutation on top of the new baseline value.
When `revert` is called, we undo the change and go back to the last externally set value. We also disconnect the element's MutationObserver to save resources.
## Pausing / Resuming the Global MutationObserver
While the library is waiting for elements to appear, it runs `document.querySelectorAll` every time a batch of elements is added or removed from the DOM.
This is performant enough in most cases, but if you want more control, you can pause and resume the global MutationObserver on demand.
One example use case is if you are making a ton of DOM changes that you know have nothing to do with the elements you are watching. You would pause right before making the changes and resume after.
```ts
import { disconnectGlobalObserver, connectGlobalObserver } from 'dom-mutator';
// Pause
disconnectGlobalObserver();
// ... do a bunch of expensive DOM updates
// Resume
connectGlobalObserver();
```
## Developing
Built with [TSDX](https://github.com/formium/tsdx).
`npm start` or `yarn start` to rebuild on file change.
`npm run build` or `yarn build` to bundle the package to the `dist` folder.
`npm test --coverage` or `yarn test --coverage` to run the Jest test suite with coverage report.
`npm run lint --fix` or `yarn lint --fix` to lint your code and autofix problems when possible.

View File

@@ -0,0 +1,524 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var validAttributeName = /^[a-zA-Z:_][a-zA-Z0-9:_.-]*$/;
var nullController = {
revert: function revert() {}
};
var elements = /*#__PURE__*/new Map();
var mutations = /*#__PURE__*/new Set();
function getObserverInit(attr) {
return attr === 'html' ? {
childList: true,
subtree: true,
attributes: true,
characterData: true
} : {
childList: false,
subtree: false,
attributes: true,
attributeFilter: [attr]
};
}
function getElementRecord(element) {
var record = elements.get(element);
if (!record) {
record = {
element: element,
attributes: {}
};
elements.set(element, record);
}
return record;
}
function createElementPropertyRecord(el, attr, getCurrentValue, setValue, mutationRunner) {
var currentValue = getCurrentValue(el);
var record = {
isDirty: false,
originalValue: currentValue,
virtualValue: currentValue,
mutations: [],
el: el,
_positionTimeout: null,
observer: new MutationObserver(function () {
// enact a 1 second timeout that blocks subsequent firing of the
// observer until the timeout is complete. This will prevent multiple
// mutations from firing in quick succession, which can cause the
// mutation to be reverted before the DOM has a chance to update.
if (attr === 'position' && record._positionTimeout) return;else if (attr === 'position') record._positionTimeout = setTimeout(function () {
record._positionTimeout = null;
}, 1000);
var currentValue = getCurrentValue(el);
if (attr === 'position' && currentValue.parentNode === record.virtualValue.parentNode && currentValue.insertBeforeNode === record.virtualValue.insertBeforeNode) return;
if (currentValue === record.virtualValue) return;
record.originalValue = currentValue;
mutationRunner(record);
}),
mutationRunner: mutationRunner,
setValue: setValue,
getCurrentValue: getCurrentValue
};
if (attr === 'position' && el.parentNode) {
record.observer.observe(el.parentNode, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
} else {
record.observer.observe(el, getObserverInit(attr));
}
return record;
}
function queueIfNeeded(val, record) {
var currentVal = record.getCurrentValue(record.el);
record.virtualValue = val;
if (val && typeof val !== 'string') {
if (!currentVal || val.parentNode !== currentVal.parentNode || val.insertBeforeNode !== currentVal.insertBeforeNode) {
record.isDirty = true;
runDOMUpdates();
}
} else if (val !== currentVal) {
record.isDirty = true;
runDOMUpdates();
}
}
function htmlMutationRunner(record) {
var val = record.originalValue;
record.mutations.forEach(function (m) {
return val = m.mutate(val);
});
queueIfNeeded(getTransformedHTML(val), record);
}
function classMutationRunner(record) {
var val = new Set(record.originalValue.split(/\s+/).filter(Boolean));
record.mutations.forEach(function (m) {
return m.mutate(val);
});
queueIfNeeded(Array.from(val).filter(Boolean).join(' '), record);
}
function attrMutationRunner(record) {
var val = record.originalValue;
record.mutations.forEach(function (m) {
return val = m.mutate(val);
});
queueIfNeeded(val, record);
}
function _loadDOMNodes(_ref) {
var parentSelector = _ref.parentSelector,
insertBeforeSelector = _ref.insertBeforeSelector;
var parentNode = document.querySelector(parentSelector);
if (!parentNode) return null;
var insertBeforeNode = insertBeforeSelector ? document.querySelector(insertBeforeSelector) : null;
if (insertBeforeSelector && !insertBeforeNode) return null;
return {
parentNode: parentNode,
insertBeforeNode: insertBeforeNode
};
}
function positionMutationRunner(record) {
var val = record.originalValue;
record.mutations.forEach(function (m) {
var selectors = m.mutate();
var newNodes = _loadDOMNodes(selectors);
val = newNodes || val;
});
queueIfNeeded(val, record);
}
var getHTMLValue = function getHTMLValue(el) {
return el.innerHTML;
};
var setHTMLValue = function setHTMLValue(el, value) {
return el.innerHTML = value;
};
function getElementHTMLRecord(element) {
var elementRecord = getElementRecord(element);
if (!elementRecord.html) {
elementRecord.html = createElementPropertyRecord(element, 'html', getHTMLValue, setHTMLValue, htmlMutationRunner);
}
return elementRecord.html;
}
var getElementPosition = function getElementPosition(el) {
return {
parentNode: el.parentElement,
insertBeforeNode: el.nextElementSibling
};
};
var setElementPosition = function setElementPosition(el, value) {
if (value.insertBeforeNode && !value.parentNode.contains(value.insertBeforeNode)) {
// skip position mutation - insertBeforeNode not a child of parent. happens
// when mutation observer for indvidual element fires out of order
return;
}
value.parentNode.insertBefore(el, value.insertBeforeNode);
};
function getElementPositionRecord(element) {
var elementRecord = getElementRecord(element);
if (!elementRecord.position) {
elementRecord.position = createElementPropertyRecord(element, 'position', getElementPosition, setElementPosition, positionMutationRunner);
}
return elementRecord.position;
}
var setClassValue = function setClassValue(el, val) {
return val ? el.className = val : el.removeAttribute('class');
};
var getClassValue = function getClassValue(el) {
return el.className;
};
function getElementClassRecord(el) {
var elementRecord = getElementRecord(el);
if (!elementRecord.classes) {
elementRecord.classes = createElementPropertyRecord(el, 'class', getClassValue, setClassValue, classMutationRunner);
}
return elementRecord.classes;
}
var getAttrValue = function getAttrValue(attrName) {
return function (el) {
var _el$getAttribute;
return (_el$getAttribute = el.getAttribute(attrName)) != null ? _el$getAttribute : null;
};
};
var setAttrValue = function setAttrValue(attrName) {
return function (el, val) {
return val !== null ? el.setAttribute(attrName, val) : el.removeAttribute(attrName);
};
};
function getElementAttributeRecord(el, attr) {
var elementRecord = getElementRecord(el);
if (!elementRecord.attributes[attr]) {
elementRecord.attributes[attr] = createElementPropertyRecord(el, attr, getAttrValue(attr), setAttrValue(attr), attrMutationRunner);
}
return elementRecord.attributes[attr];
}
function deleteElementPropertyRecord(el, attr) {
var element = elements.get(el);
if (!element) return;
if (attr === 'html') {
var _element$html, _element$html$observe;
(_element$html = element.html) == null ? void 0 : (_element$html$observe = _element$html.observer) == null ? void 0 : _element$html$observe.disconnect();
delete element.html;
} else if (attr === 'class') {
var _element$classes, _element$classes$obse;
(_element$classes = element.classes) == null ? void 0 : (_element$classes$obse = _element$classes.observer) == null ? void 0 : _element$classes$obse.disconnect();
delete element.classes;
} else if (attr === 'position') {
var _element$position, _element$position$obs;
(_element$position = element.position) == null ? void 0 : (_element$position$obs = _element$position.observer) == null ? void 0 : _element$position$obs.disconnect();
delete element.position;
} else {
var _element$attributes, _element$attributes$a, _element$attributes$a2;
(_element$attributes = element.attributes) == null ? void 0 : (_element$attributes$a = _element$attributes[attr]) == null ? void 0 : (_element$attributes$a2 = _element$attributes$a.observer) == null ? void 0 : _element$attributes$a2.disconnect();
delete element.attributes[attr];
}
}
var transformContainer;
function getTransformedHTML(html) {
if (!transformContainer) {
transformContainer = document.createElement('div');
}
transformContainer.innerHTML = html;
return transformContainer.innerHTML;
}
function setPropertyValue(el, attr, m) {
if (!m.isDirty) return;
m.isDirty = false;
var val = m.virtualValue;
if (!m.mutations.length) {
deleteElementPropertyRecord(el, attr);
}
m.setValue(el, val);
}
function setValue(m, el) {
m.html && setPropertyValue(el, 'html', m.html);
m.classes && setPropertyValue(el, 'class', m.classes);
m.position && setPropertyValue(el, 'position', m.position);
Object.keys(m.attributes).forEach(function (attr) {
setPropertyValue(el, attr, m.attributes[attr]);
});
}
function runDOMUpdates() {
elements.forEach(setValue);
} // find or create ElementPropertyRecord, add mutation to it, then run
function startMutating(mutation, element) {
var record = null;
if (mutation.kind === 'html') {
record = getElementHTMLRecord(element);
} else if (mutation.kind === 'class') {
record = getElementClassRecord(element);
} else if (mutation.kind === 'attribute') {
record = getElementAttributeRecord(element, mutation.attribute);
} else if (mutation.kind === 'position') {
record = getElementPositionRecord(element);
}
if (!record) return;
record.mutations.push(mutation);
record.mutationRunner(record);
} // get (existing) ElementPropertyRecord, remove mutation from it, then run
function stopMutating(mutation, el) {
var record = null;
if (mutation.kind === 'html') {
record = getElementHTMLRecord(el);
} else if (mutation.kind === 'class') {
record = getElementClassRecord(el);
} else if (mutation.kind === 'attribute') {
record = getElementAttributeRecord(el, mutation.attribute);
} else if (mutation.kind === 'position') {
record = getElementPositionRecord(el);
}
if (!record) return;
var index = record.mutations.indexOf(mutation);
if (index !== -1) record.mutations.splice(index, 1);
record.mutationRunner(record);
} // maintain list of elements associated with mutation
function refreshElementsSet(mutation) {
// if a position mutation has already found an element to move, don't move
// any more elements
if (mutation.kind === 'position' && mutation.elements.size === 1) return;
var existingElements = new Set(mutation.elements);
var matchingElements = document.querySelectorAll(mutation.selector);
matchingElements.forEach(function (el) {
if (!existingElements.has(el)) {
mutation.elements.add(el);
startMutating(mutation, el);
}
});
}
function revertMutation(mutation) {
mutation.elements.forEach(function (el) {
return stopMutating(mutation, el);
});
mutation.elements.clear();
mutations["delete"](mutation);
}
function refreshAllElementSets() {
mutations.forEach(refreshElementsSet);
} // Observer for elements that don't exist in the DOM yet
var observer;
function disconnectGlobalObserver() {
observer && observer.disconnect();
}
function connectGlobalObserver() {
if (typeof document === 'undefined') return;
if (!observer) {
observer = new MutationObserver(function () {
refreshAllElementSets();
});
}
refreshAllElementSets();
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
} // run on init
connectGlobalObserver();
function newMutation(m) {
// Not in a browser
if (typeof document === 'undefined') return nullController; // add to global index of mutations
mutations.add(m); // run refresh on init to establish list of elements associated w/ mutation
refreshElementsSet(m);
return {
revert: function revert() {
revertMutation(m);
}
};
}
function html(selector, mutate) {
return newMutation({
kind: 'html',
elements: new Set(),
mutate: mutate,
selector: selector
});
}
function position(selector, mutate) {
return newMutation({
kind: 'position',
elements: new Set(),
mutate: mutate,
selector: selector
});
}
function classes(selector, mutate) {
return newMutation({
kind: 'class',
elements: new Set(),
mutate: mutate,
selector: selector
});
}
function attribute(selector, attribute, mutate) {
if (!validAttributeName.test(attribute)) return nullController;
if (attribute === 'class' || attribute === 'className') {
return classes(selector, function (classnames) {
var mutatedClassnames = mutate(Array.from(classnames).join(' '));
classnames.clear();
if (!mutatedClassnames) return;
mutatedClassnames.split(/\s+/g).filter(Boolean).forEach(function (c) {
return classnames.add(c);
});
});
}
return newMutation({
kind: 'attribute',
attribute: attribute,
elements: new Set(),
mutate: mutate,
selector: selector
});
}
function declarative(_ref2) {
var selector = _ref2.selector,
action = _ref2.action,
value = _ref2.value,
attr = _ref2.attribute,
parentSelector = _ref2.parentSelector,
insertBeforeSelector = _ref2.insertBeforeSelector;
if (attr === 'html') {
if (action === 'append') {
return html(selector, function (val) {
return val + (value != null ? value : '');
});
} else if (action === 'set') {
return html(selector, function () {
return value != null ? value : '';
});
}
} else if (attr === 'class') {
if (action === 'append') {
return classes(selector, function (val) {
if (value) val.add(value);
});
} else if (action === 'remove') {
return classes(selector, function (val) {
if (value) val["delete"](value);
});
} else if (action === 'set') {
return classes(selector, function (val) {
val.clear();
if (value) val.add(value);
});
}
} else if (attr === 'position') {
if (action === 'set' && parentSelector) {
return position(selector, function () {
return {
insertBeforeSelector: insertBeforeSelector,
parentSelector: parentSelector
};
});
}
} else {
if (action === 'append') {
return attribute(selector, attr, function (val) {
return val !== null ? val + (value != null ? value : '') : value != null ? value : '';
});
} else if (action === 'set') {
return attribute(selector, attr, function () {
return value != null ? value : '';
});
} else if (action === 'remove') {
return attribute(selector, attr, function () {
return null;
});
}
}
return nullController;
}
var index = {
html: html,
classes: classes,
attribute: attribute,
position: position,
declarative: declarative
};
exports.connectGlobalObserver = connectGlobalObserver;
exports.default = index;
exports.disconnectGlobalObserver = disconnectGlobalObserver;
exports.validAttributeName = validAttributeName;
//# sourceMappingURL=dom-mutator.cjs.development.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,518 @@
var validAttributeName = /^[a-zA-Z:_][a-zA-Z0-9:_.-]*$/;
var nullController = {
revert: function revert() {}
};
var elements = /*#__PURE__*/new Map();
var mutations = /*#__PURE__*/new Set();
function getObserverInit(attr) {
return attr === 'html' ? {
childList: true,
subtree: true,
attributes: true,
characterData: true
} : {
childList: false,
subtree: false,
attributes: true,
attributeFilter: [attr]
};
}
function getElementRecord(element) {
var record = elements.get(element);
if (!record) {
record = {
element: element,
attributes: {}
};
elements.set(element, record);
}
return record;
}
function createElementPropertyRecord(el, attr, getCurrentValue, setValue, mutationRunner) {
var currentValue = getCurrentValue(el);
var record = {
isDirty: false,
originalValue: currentValue,
virtualValue: currentValue,
mutations: [],
el: el,
_positionTimeout: null,
observer: new MutationObserver(function () {
// enact a 1 second timeout that blocks subsequent firing of the
// observer until the timeout is complete. This will prevent multiple
// mutations from firing in quick succession, which can cause the
// mutation to be reverted before the DOM has a chance to update.
if (attr === 'position' && record._positionTimeout) return;else if (attr === 'position') record._positionTimeout = setTimeout(function () {
record._positionTimeout = null;
}, 1000);
var currentValue = getCurrentValue(el);
if (attr === 'position' && currentValue.parentNode === record.virtualValue.parentNode && currentValue.insertBeforeNode === record.virtualValue.insertBeforeNode) return;
if (currentValue === record.virtualValue) return;
record.originalValue = currentValue;
mutationRunner(record);
}),
mutationRunner: mutationRunner,
setValue: setValue,
getCurrentValue: getCurrentValue
};
if (attr === 'position' && el.parentNode) {
record.observer.observe(el.parentNode, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
} else {
record.observer.observe(el, getObserverInit(attr));
}
return record;
}
function queueIfNeeded(val, record) {
var currentVal = record.getCurrentValue(record.el);
record.virtualValue = val;
if (val && typeof val !== 'string') {
if (!currentVal || val.parentNode !== currentVal.parentNode || val.insertBeforeNode !== currentVal.insertBeforeNode) {
record.isDirty = true;
runDOMUpdates();
}
} else if (val !== currentVal) {
record.isDirty = true;
runDOMUpdates();
}
}
function htmlMutationRunner(record) {
var val = record.originalValue;
record.mutations.forEach(function (m) {
return val = m.mutate(val);
});
queueIfNeeded(getTransformedHTML(val), record);
}
function classMutationRunner(record) {
var val = new Set(record.originalValue.split(/\s+/).filter(Boolean));
record.mutations.forEach(function (m) {
return m.mutate(val);
});
queueIfNeeded(Array.from(val).filter(Boolean).join(' '), record);
}
function attrMutationRunner(record) {
var val = record.originalValue;
record.mutations.forEach(function (m) {
return val = m.mutate(val);
});
queueIfNeeded(val, record);
}
function _loadDOMNodes(_ref) {
var parentSelector = _ref.parentSelector,
insertBeforeSelector = _ref.insertBeforeSelector;
var parentNode = document.querySelector(parentSelector);
if (!parentNode) return null;
var insertBeforeNode = insertBeforeSelector ? document.querySelector(insertBeforeSelector) : null;
if (insertBeforeSelector && !insertBeforeNode) return null;
return {
parentNode: parentNode,
insertBeforeNode: insertBeforeNode
};
}
function positionMutationRunner(record) {
var val = record.originalValue;
record.mutations.forEach(function (m) {
var selectors = m.mutate();
var newNodes = _loadDOMNodes(selectors);
val = newNodes || val;
});
queueIfNeeded(val, record);
}
var getHTMLValue = function getHTMLValue(el) {
return el.innerHTML;
};
var setHTMLValue = function setHTMLValue(el, value) {
return el.innerHTML = value;
};
function getElementHTMLRecord(element) {
var elementRecord = getElementRecord(element);
if (!elementRecord.html) {
elementRecord.html = createElementPropertyRecord(element, 'html', getHTMLValue, setHTMLValue, htmlMutationRunner);
}
return elementRecord.html;
}
var getElementPosition = function getElementPosition(el) {
return {
parentNode: el.parentElement,
insertBeforeNode: el.nextElementSibling
};
};
var setElementPosition = function setElementPosition(el, value) {
if (value.insertBeforeNode && !value.parentNode.contains(value.insertBeforeNode)) {
// skip position mutation - insertBeforeNode not a child of parent. happens
// when mutation observer for indvidual element fires out of order
return;
}
value.parentNode.insertBefore(el, value.insertBeforeNode);
};
function getElementPositionRecord(element) {
var elementRecord = getElementRecord(element);
if (!elementRecord.position) {
elementRecord.position = createElementPropertyRecord(element, 'position', getElementPosition, setElementPosition, positionMutationRunner);
}
return elementRecord.position;
}
var setClassValue = function setClassValue(el, val) {
return val ? el.className = val : el.removeAttribute('class');
};
var getClassValue = function getClassValue(el) {
return el.className;
};
function getElementClassRecord(el) {
var elementRecord = getElementRecord(el);
if (!elementRecord.classes) {
elementRecord.classes = createElementPropertyRecord(el, 'class', getClassValue, setClassValue, classMutationRunner);
}
return elementRecord.classes;
}
var getAttrValue = function getAttrValue(attrName) {
return function (el) {
var _el$getAttribute;
return (_el$getAttribute = el.getAttribute(attrName)) != null ? _el$getAttribute : null;
};
};
var setAttrValue = function setAttrValue(attrName) {
return function (el, val) {
return val !== null ? el.setAttribute(attrName, val) : el.removeAttribute(attrName);
};
};
function getElementAttributeRecord(el, attr) {
var elementRecord = getElementRecord(el);
if (!elementRecord.attributes[attr]) {
elementRecord.attributes[attr] = createElementPropertyRecord(el, attr, getAttrValue(attr), setAttrValue(attr), attrMutationRunner);
}
return elementRecord.attributes[attr];
}
function deleteElementPropertyRecord(el, attr) {
var element = elements.get(el);
if (!element) return;
if (attr === 'html') {
var _element$html, _element$html$observe;
(_element$html = element.html) == null ? void 0 : (_element$html$observe = _element$html.observer) == null ? void 0 : _element$html$observe.disconnect();
delete element.html;
} else if (attr === 'class') {
var _element$classes, _element$classes$obse;
(_element$classes = element.classes) == null ? void 0 : (_element$classes$obse = _element$classes.observer) == null ? void 0 : _element$classes$obse.disconnect();
delete element.classes;
} else if (attr === 'position') {
var _element$position, _element$position$obs;
(_element$position = element.position) == null ? void 0 : (_element$position$obs = _element$position.observer) == null ? void 0 : _element$position$obs.disconnect();
delete element.position;
} else {
var _element$attributes, _element$attributes$a, _element$attributes$a2;
(_element$attributes = element.attributes) == null ? void 0 : (_element$attributes$a = _element$attributes[attr]) == null ? void 0 : (_element$attributes$a2 = _element$attributes$a.observer) == null ? void 0 : _element$attributes$a2.disconnect();
delete element.attributes[attr];
}
}
var transformContainer;
function getTransformedHTML(html) {
if (!transformContainer) {
transformContainer = document.createElement('div');
}
transformContainer.innerHTML = html;
return transformContainer.innerHTML;
}
function setPropertyValue(el, attr, m) {
if (!m.isDirty) return;
m.isDirty = false;
var val = m.virtualValue;
if (!m.mutations.length) {
deleteElementPropertyRecord(el, attr);
}
m.setValue(el, val);
}
function setValue(m, el) {
m.html && setPropertyValue(el, 'html', m.html);
m.classes && setPropertyValue(el, 'class', m.classes);
m.position && setPropertyValue(el, 'position', m.position);
Object.keys(m.attributes).forEach(function (attr) {
setPropertyValue(el, attr, m.attributes[attr]);
});
}
function runDOMUpdates() {
elements.forEach(setValue);
} // find or create ElementPropertyRecord, add mutation to it, then run
function startMutating(mutation, element) {
var record = null;
if (mutation.kind === 'html') {
record = getElementHTMLRecord(element);
} else if (mutation.kind === 'class') {
record = getElementClassRecord(element);
} else if (mutation.kind === 'attribute') {
record = getElementAttributeRecord(element, mutation.attribute);
} else if (mutation.kind === 'position') {
record = getElementPositionRecord(element);
}
if (!record) return;
record.mutations.push(mutation);
record.mutationRunner(record);
} // get (existing) ElementPropertyRecord, remove mutation from it, then run
function stopMutating(mutation, el) {
var record = null;
if (mutation.kind === 'html') {
record = getElementHTMLRecord(el);
} else if (mutation.kind === 'class') {
record = getElementClassRecord(el);
} else if (mutation.kind === 'attribute') {
record = getElementAttributeRecord(el, mutation.attribute);
} else if (mutation.kind === 'position') {
record = getElementPositionRecord(el);
}
if (!record) return;
var index = record.mutations.indexOf(mutation);
if (index !== -1) record.mutations.splice(index, 1);
record.mutationRunner(record);
} // maintain list of elements associated with mutation
function refreshElementsSet(mutation) {
// if a position mutation has already found an element to move, don't move
// any more elements
if (mutation.kind === 'position' && mutation.elements.size === 1) return;
var existingElements = new Set(mutation.elements);
var matchingElements = document.querySelectorAll(mutation.selector);
matchingElements.forEach(function (el) {
if (!existingElements.has(el)) {
mutation.elements.add(el);
startMutating(mutation, el);
}
});
}
function revertMutation(mutation) {
mutation.elements.forEach(function (el) {
return stopMutating(mutation, el);
});
mutation.elements.clear();
mutations["delete"](mutation);
}
function refreshAllElementSets() {
mutations.forEach(refreshElementsSet);
} // Observer for elements that don't exist in the DOM yet
var observer;
function disconnectGlobalObserver() {
observer && observer.disconnect();
}
function connectGlobalObserver() {
if (typeof document === 'undefined') return;
if (!observer) {
observer = new MutationObserver(function () {
refreshAllElementSets();
});
}
refreshAllElementSets();
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
} // run on init
connectGlobalObserver();
function newMutation(m) {
// Not in a browser
if (typeof document === 'undefined') return nullController; // add to global index of mutations
mutations.add(m); // run refresh on init to establish list of elements associated w/ mutation
refreshElementsSet(m);
return {
revert: function revert() {
revertMutation(m);
}
};
}
function html(selector, mutate) {
return newMutation({
kind: 'html',
elements: new Set(),
mutate: mutate,
selector: selector
});
}
function position(selector, mutate) {
return newMutation({
kind: 'position',
elements: new Set(),
mutate: mutate,
selector: selector
});
}
function classes(selector, mutate) {
return newMutation({
kind: 'class',
elements: new Set(),
mutate: mutate,
selector: selector
});
}
function attribute(selector, attribute, mutate) {
if (!validAttributeName.test(attribute)) return nullController;
if (attribute === 'class' || attribute === 'className') {
return classes(selector, function (classnames) {
var mutatedClassnames = mutate(Array.from(classnames).join(' '));
classnames.clear();
if (!mutatedClassnames) return;
mutatedClassnames.split(/\s+/g).filter(Boolean).forEach(function (c) {
return classnames.add(c);
});
});
}
return newMutation({
kind: 'attribute',
attribute: attribute,
elements: new Set(),
mutate: mutate,
selector: selector
});
}
function declarative(_ref2) {
var selector = _ref2.selector,
action = _ref2.action,
value = _ref2.value,
attr = _ref2.attribute,
parentSelector = _ref2.parentSelector,
insertBeforeSelector = _ref2.insertBeforeSelector;
if (attr === 'html') {
if (action === 'append') {
return html(selector, function (val) {
return val + (value != null ? value : '');
});
} else if (action === 'set') {
return html(selector, function () {
return value != null ? value : '';
});
}
} else if (attr === 'class') {
if (action === 'append') {
return classes(selector, function (val) {
if (value) val.add(value);
});
} else if (action === 'remove') {
return classes(selector, function (val) {
if (value) val["delete"](value);
});
} else if (action === 'set') {
return classes(selector, function (val) {
val.clear();
if (value) val.add(value);
});
}
} else if (attr === 'position') {
if (action === 'set' && parentSelector) {
return position(selector, function () {
return {
insertBeforeSelector: insertBeforeSelector,
parentSelector: parentSelector
};
});
}
} else {
if (action === 'append') {
return attribute(selector, attr, function (val) {
return val !== null ? val + (value != null ? value : '') : value != null ? value : '';
});
} else if (action === 'set') {
return attribute(selector, attr, function () {
return value != null ? value : '';
});
} else if (action === 'remove') {
return attribute(selector, attr, function () {
return null;
});
}
}
return nullController;
}
var index = {
html: html,
classes: classes,
attribute: attribute,
position: position,
declarative: declarative
};
export default index;
export { connectGlobalObserver, disconnectGlobalObserver, validAttributeName };
//# sourceMappingURL=dom-mutator.esm.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
export declare const validAttributeName: RegExp;
export declare function disconnectGlobalObserver(): void;
export declare function connectGlobalObserver(): void;
declare function html(selector: HTMLMutation['selector'], mutate: HTMLMutation['mutate']): MutationController;
declare function position(selector: PositionMutation['selector'], mutate: PositionMutation['mutate']): MutationController;
declare function classes(selector: ClassnameMutation['selector'], mutate: ClassnameMutation['mutate']): MutationController;
declare function attribute(selector: AttrMutation['selector'], attribute: AttrMutation['attribute'], mutate: AttrMutation['mutate']): MutationController;
declare function declarative({ selector, action, value, attribute: attr, parentSelector, insertBeforeSelector, }: DeclarativeMutation): MutationController;
export declare type MutationController = {
revert: () => void;
};
export declare type DeclarativeMutation = {
selector: string;
attribute: string;
action: 'append' | 'set' | 'remove';
value?: string;
parentSelector?: string;
insertBeforeSelector?: string;
};
declare const _default: {
html: typeof html;
classes: typeof classes;
attribute: typeof attribute;
position: typeof position;
declarative: typeof declarative;
};
export default _default;

View File

@@ -0,0 +1,8 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dom-mutator.cjs.production.min.js')
} else {
module.exports = require('./dom-mutator.cjs.development.js')
}

View File

@@ -0,0 +1,57 @@
/// <reference types="node" />
interface BaseMutation {
selector: string;
elements: Set<Element>;
}
interface HTMLMutation extends BaseMutation {
kind: 'html';
mutate: (innerHtml: string) => string;
}
interface ClassnameMutation extends BaseMutation {
kind: 'class';
mutate: (classNames: Set<string>) => void;
}
interface AttrMutation extends BaseMutation {
kind: 'attribute';
attribute: string;
mutate: (value: string | null) => string | null;
}
interface PositionMutation extends BaseMutation {
kind: 'position';
mutate: () => ElementPosition;
}
interface ElementPosition {
parentSelector: string;
insertBeforeSelector?: null | string;
}
interface ElementPositionWithDomNode {
parentNode: HTMLElement;
insertBeforeNode: HTMLElement | null;
}
declare type Mutation = HTMLMutation | ClassnameMutation | AttrMutation | PositionMutation;
declare type MutationKind = Mutation['kind'];
interface ElementPropertyRecord<T, V> {
_positionTimeout: NodeJS.Timeout | number | null;
observer: MutationObserver;
originalValue: V;
virtualValue: V;
isDirty: boolean;
mutations: T[];
el: Element;
getCurrentValue: (el: Element) => V;
setValue: (el: Element, value: V) => void;
mutationRunner: (record: ElementPropertyRecord<T, V>) => void;
}
declare type HTMLRecord = ElementPropertyRecord<HTMLMutation, string>;
declare type ClassnameRecord = ElementPropertyRecord<ClassnameMutation, string>;
declare type AttributeRecord = ElementPropertyRecord<AttrMutation, string | null>;
declare type PositionRecord = ElementPropertyRecord<PositionMutation, ElementPositionWithDomNode>;
interface ElementRecord {
element: Element;
html?: HTMLRecord;
classes?: ClassnameRecord;
attributes: {
[key: string]: AttributeRecord;
};
position?: PositionRecord;
}

View File

@@ -0,0 +1,52 @@
{
"version": "0.6.0",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"files": ["dist", "src"],
"engines": {
"node": ">=10"
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test",
"lint": "tsdx lint",
"prepare": "tsdx build",
"size": "size-limit",
"analyze": "size-limit --why"
},
"peerDependencies": {},
"husky": {
"hooks": {
"pre-commit": "tsdx lint"
}
},
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"name": "dom-mutator",
"author": "Jeremy Dorn",
"module": "dist/dom-mutator.esm.js",
"size-limit": [
{
"path": "dist/dom-mutator.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/dom-mutator.esm.js",
"limit": "10 KB"
}
],
"devDependencies": {
"@size-limit/preset-small-lib": "^4.9.2",
"husky": "^4.3.8",
"size-limit": "^4.9.2",
"tsdx": "^0.14.1",
"tslib": "^2.1.0",
"typescript": "^4.1.3"
}
}

View File

@@ -0,0 +1,530 @@
export const validAttributeName = /^[a-zA-Z:_][a-zA-Z0-9:_.-]*$/;
const nullController: MutationController = {
revert: () => {},
};
const elements: Map<Element, ElementRecord> = new Map();
const mutations: Set<Mutation> = new Set();
function getObserverInit(attr: string): MutationObserverInit {
return attr === 'html'
? {
childList: true,
subtree: true,
attributes: true,
characterData: true,
}
: {
childList: false,
subtree: false,
attributes: true,
attributeFilter: [attr],
};
}
function getElementRecord(element: Element): ElementRecord {
let record = elements.get(element);
if (!record) {
record = { element, attributes: {} };
elements.set(element, record);
}
return record;
}
function createElementPropertyRecord(
el: Element,
attr: string,
getCurrentValue: (el: Element) => any,
setValue: (el: Element, val: any) => void,
mutationRunner: (record: ElementPropertyRecord<any, any>) => void
) {
const currentValue = getCurrentValue(el);
const record: ElementPropertyRecord<any, any> = {
isDirty: false,
originalValue: currentValue,
virtualValue: currentValue,
mutations: [],
el,
_positionTimeout: null,
observer: new MutationObserver(() => {
// enact a 1 second timeout that blocks subsequent firing of the
// observer until the timeout is complete. This will prevent multiple
// mutations from firing in quick succession, which can cause the
// mutation to be reverted before the DOM has a chance to update.
if (attr === 'position' && record._positionTimeout) return;
else if (attr === 'position')
record._positionTimeout = setTimeout(() => {
record._positionTimeout = null;
}, 1000);
const currentValue = getCurrentValue(el);
if (
attr === 'position' &&
currentValue.parentNode === record.virtualValue.parentNode &&
currentValue.insertBeforeNode === record.virtualValue.insertBeforeNode
)
return;
if (currentValue === record.virtualValue) return;
record.originalValue = currentValue;
mutationRunner(record);
}),
mutationRunner,
setValue,
getCurrentValue,
};
if (attr === 'position' && el.parentNode) {
record.observer.observe(el.parentNode, {
childList: true,
subtree: true,
attributes: false,
characterData: false,
});
} else {
record.observer.observe(el, getObserverInit(attr));
}
return record;
}
function queueIfNeeded(
val: string | null | ElementPositionWithDomNode,
record: ElementPropertyRecord<any, any>
) {
const currentVal = record.getCurrentValue(record.el);
record.virtualValue = val;
if (val && typeof val !== 'string') {
if (
!currentVal ||
val.parentNode !== currentVal.parentNode ||
val.insertBeforeNode !== currentVal.insertBeforeNode
) {
record.isDirty = true;
runDOMUpdates();
}
} else if (val !== currentVal) {
record.isDirty = true;
runDOMUpdates();
}
}
function htmlMutationRunner(record: HTMLRecord) {
let val = record.originalValue;
record.mutations.forEach(m => (val = m.mutate(val)));
queueIfNeeded(getTransformedHTML(val), record);
}
function classMutationRunner(record: ClassnameRecord) {
const val = new Set(record.originalValue.split(/\s+/).filter(Boolean));
record.mutations.forEach(m => m.mutate(val));
queueIfNeeded(
Array.from(val)
.filter(Boolean)
.join(' '),
record
);
}
function attrMutationRunner(record: AttributeRecord) {
let val: string | null = record.originalValue;
record.mutations.forEach(m => (val = m.mutate(val)));
queueIfNeeded(val, record);
}
function _loadDOMNodes({
parentSelector,
insertBeforeSelector,
}: ElementPosition): ElementPositionWithDomNode | null {
const parentNode = document.querySelector<HTMLElement>(parentSelector);
if (!parentNode) return null;
const insertBeforeNode = insertBeforeSelector
? document.querySelector<HTMLElement>(insertBeforeSelector)
: null;
if (insertBeforeSelector && !insertBeforeNode) return null;
return {
parentNode,
insertBeforeNode,
};
}
function positionMutationRunner(record: PositionRecord) {
let val = record.originalValue;
record.mutations.forEach(m => {
const selectors = m.mutate();
const newNodes = _loadDOMNodes(selectors);
val = newNodes || val;
});
queueIfNeeded(val, record);
}
const getHTMLValue = (el: Element) => el.innerHTML;
const setHTMLValue = (el: Element, value: string) => (el.innerHTML = value);
function getElementHTMLRecord(element: Element): HTMLRecord {
const elementRecord = getElementRecord(element);
if (!elementRecord.html) {
elementRecord.html = createElementPropertyRecord(
element,
'html',
getHTMLValue,
setHTMLValue,
htmlMutationRunner
);
}
return elementRecord.html;
}
const getElementPosition = (el: Element): ElementPositionWithDomNode => {
return {
parentNode: el.parentElement as HTMLElement,
insertBeforeNode: el.nextElementSibling as HTMLElement | null,
};
};
const setElementPosition = (el: Element, value: ElementPositionWithDomNode) => {
if (
value.insertBeforeNode &&
!value.parentNode.contains(value.insertBeforeNode)
) {
// skip position mutation - insertBeforeNode not a child of parent. happens
// when mutation observer for indvidual element fires out of order
return;
}
value.parentNode.insertBefore(el, value.insertBeforeNode);
};
function getElementPositionRecord(element: Element): PositionRecord {
const elementRecord = getElementRecord(element);
if (!elementRecord.position) {
elementRecord.position = createElementPropertyRecord(
element,
'position',
getElementPosition,
setElementPosition,
positionMutationRunner
);
}
return elementRecord.position;
}
const setClassValue = (el: Element, val: string) =>
val ? (el.className = val) : el.removeAttribute('class');
const getClassValue = (el: Element) => el.className;
function getElementClassRecord(el: Element): ClassnameRecord {
const elementRecord = getElementRecord(el);
if (!elementRecord.classes) {
elementRecord.classes = createElementPropertyRecord(
el,
'class',
getClassValue,
setClassValue,
classMutationRunner
);
}
return elementRecord.classes;
}
const getAttrValue = (attrName: string) => (el: Element) =>
el.getAttribute(attrName) ?? null;
const setAttrValue = (attrName: string) => (el: Element, val: string | null) =>
val !== null ? el.setAttribute(attrName, val) : el.removeAttribute(attrName);
function getElementAttributeRecord(el: Element, attr: string): AttributeRecord {
const elementRecord = getElementRecord(el);
if (!elementRecord.attributes[attr]) {
elementRecord.attributes[attr] = createElementPropertyRecord(
el,
attr,
getAttrValue(attr),
setAttrValue(attr),
attrMutationRunner
);
}
return elementRecord.attributes[attr];
}
function deleteElementPropertyRecord(el: Element, attr: string) {
const element = elements.get(el);
if (!element) return;
if (attr === 'html') {
element.html?.observer?.disconnect();
delete element.html;
} else if (attr === 'class') {
element.classes?.observer?.disconnect();
delete element.classes;
} else if (attr === 'position') {
element.position?.observer?.disconnect();
delete element.position;
} else {
element.attributes?.[attr]?.observer?.disconnect();
delete element.attributes[attr];
}
}
let transformContainer: HTMLDivElement;
function getTransformedHTML(html: string) {
if (!transformContainer) {
transformContainer = document.createElement('div');
}
transformContainer.innerHTML = html;
return transformContainer.innerHTML;
}
function setPropertyValue<T extends ElementPropertyRecord<any, any>>(
el: Element,
attr: string,
m: T
) {
if (!m.isDirty) return;
m.isDirty = false;
const val = m.virtualValue;
if (!m.mutations.length) {
deleteElementPropertyRecord(el, attr);
}
m.setValue(el, val);
}
function setValue(m: ElementRecord, el: Element) {
m.html && setPropertyValue<HTMLRecord>(el, 'html', m.html);
m.classes && setPropertyValue<ClassnameRecord>(el, 'class', m.classes);
m.position && setPropertyValue<PositionRecord>(el, 'position', m.position);
Object.keys(m.attributes).forEach(attr => {
setPropertyValue<AttributeRecord>(el, attr, m.attributes[attr]);
});
}
function runDOMUpdates() {
elements.forEach(setValue);
}
// find or create ElementPropertyRecord, add mutation to it, then run
function startMutating(mutation: Mutation, element: Element) {
let record: ElementPropertyRecord<any, any> | null = null;
if (mutation.kind === 'html') {
record = getElementHTMLRecord(element);
} else if (mutation.kind === 'class') {
record = getElementClassRecord(element);
} else if (mutation.kind === 'attribute') {
record = getElementAttributeRecord(element, mutation.attribute);
} else if (mutation.kind === 'position') {
record = getElementPositionRecord(element);
}
if (!record) return;
record.mutations.push(mutation);
record.mutationRunner(record);
}
// get (existing) ElementPropertyRecord, remove mutation from it, then run
function stopMutating(mutation: Mutation, el: Element) {
let record: ElementPropertyRecord<any, any> | null = null;
if (mutation.kind === 'html') {
record = getElementHTMLRecord(el);
} else if (mutation.kind === 'class') {
record = getElementClassRecord(el);
} else if (mutation.kind === 'attribute') {
record = getElementAttributeRecord(el, mutation.attribute);
} else if (mutation.kind === 'position') {
record = getElementPositionRecord(el);
}
if (!record) return;
const index = record.mutations.indexOf(mutation);
if (index !== -1) record.mutations.splice(index, 1);
record.mutationRunner(record);
}
// maintain list of elements associated with mutation
function refreshElementsSet(mutation: Mutation) {
// if a position mutation has already found an element to move, don't move
// any more elements
if (mutation.kind === 'position' && mutation.elements.size === 1) return;
const existingElements = new Set(mutation.elements);
const matchingElements = document.querySelectorAll(mutation.selector);
matchingElements.forEach(el => {
if (!existingElements.has(el)) {
mutation.elements.add(el);
startMutating(mutation, el);
}
});
}
function revertMutation(mutation: Mutation) {
mutation.elements.forEach(el => stopMutating(mutation, el));
mutation.elements.clear();
mutations.delete(mutation);
}
function refreshAllElementSets() {
mutations.forEach(refreshElementsSet);
}
// Observer for elements that don't exist in the DOM yet
let observer: MutationObserver;
export function disconnectGlobalObserver() {
observer && observer.disconnect();
}
export function connectGlobalObserver() {
if (typeof document === 'undefined') return;
if (!observer) {
observer = new MutationObserver(() => {
refreshAllElementSets();
});
}
refreshAllElementSets();
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: false,
characterData: false,
});
}
// run on init
connectGlobalObserver();
function newMutation(m: Mutation): MutationController {
// Not in a browser
if (typeof document === 'undefined') return nullController;
// add to global index of mutations
mutations.add(m);
// run refresh on init to establish list of elements associated w/ mutation
refreshElementsSet(m);
return {
revert: () => {
revertMutation(m);
},
};
}
function html(
selector: HTMLMutation['selector'],
mutate: HTMLMutation['mutate']
) {
return newMutation({
kind: 'html',
elements: new Set(),
mutate,
selector,
});
}
function position(
selector: PositionMutation['selector'],
mutate: PositionMutation['mutate']
) {
return newMutation({
kind: 'position',
elements: new Set(),
mutate,
selector,
});
}
function classes(
selector: ClassnameMutation['selector'],
mutate: ClassnameMutation['mutate']
) {
return newMutation({
kind: 'class',
elements: new Set(),
mutate,
selector,
});
}
function attribute(
selector: AttrMutation['selector'],
attribute: AttrMutation['attribute'],
mutate: AttrMutation['mutate']
) {
if (!validAttributeName.test(attribute)) return nullController;
if (attribute === 'class' || attribute === 'className') {
return classes(selector, classnames => {
const mutatedClassnames = mutate(Array.from(classnames).join(' '));
classnames.clear();
if (!mutatedClassnames) return;
mutatedClassnames
.split(/\s+/g)
.filter(Boolean)
.forEach(c => classnames.add(c));
});
}
return newMutation({
kind: 'attribute',
attribute,
elements: new Set(),
mutate,
selector,
});
}
function declarative({
selector,
action,
value,
attribute: attr,
parentSelector,
insertBeforeSelector,
}: DeclarativeMutation): MutationController {
if (attr === 'html') {
if (action === 'append') {
return html(selector, val => val + (value ?? ''));
} else if (action === 'set') {
return html(selector, () => value ?? '');
}
} else if (attr === 'class') {
if (action === 'append') {
return classes(selector, val => {
if (value) val.add(value);
});
} else if (action === 'remove') {
return classes(selector, val => {
if (value) val.delete(value);
});
} else if (action === 'set') {
return classes(selector, val => {
val.clear();
if (value) val.add(value);
});
}
} else if (attr === 'position') {
if (action === 'set' && parentSelector) {
return position(selector, () => ({
insertBeforeSelector,
parentSelector,
}));
}
} else {
if (action === 'append') {
return attribute(selector, attr, val =>
val !== null ? val + (value ?? '') : value ?? ''
);
} else if (action === 'set') {
return attribute(selector, attr, () => value ?? '');
} else if (action === 'remove') {
return attribute(selector, attr, () => null);
}
}
return nullController;
}
export type MutationController = {
revert: () => void;
};
export type DeclarativeMutation = {
selector: string;
attribute: string;
action: 'append' | 'set' | 'remove';
value?: string;
parentSelector?: string;
insertBeforeSelector?: string;
};
export default {
html,
classes,
attribute,
position,
declarative,
};

View File

@@ -0,0 +1,73 @@
interface BaseMutation {
selector: string;
elements: Set<Element>;
}
interface HTMLMutation extends BaseMutation {
kind: 'html';
mutate: (innerHtml: string) => string;
}
interface ClassnameMutation extends BaseMutation {
kind: 'class';
mutate: (classNames: Set<string>) => void;
}
interface AttrMutation extends BaseMutation {
kind: 'attribute';
attribute: string;
mutate: (value: string | null) => string | null;
}
interface PositionMutation extends BaseMutation {
kind: 'position';
mutate: () => ElementPosition;
}
interface ElementPosition {
parentSelector: string;
insertBeforeSelector?: null | string;
}
interface ElementPositionWithDomNode {
parentNode: HTMLElement;
insertBeforeNode: HTMLElement | null;
}
type Mutation =
| HTMLMutation
| ClassnameMutation
| AttrMutation
| PositionMutation;
type MutationKind = Mutation['kind'];
interface ElementPropertyRecord<T, V> {
_positionTimeout: NodeJS.Timeout | number | null;
observer: MutationObserver;
originalValue: V;
virtualValue: V;
isDirty: boolean;
mutations: T[];
el: Element;
getCurrentValue: (el: Element) => V;
setValue: (el: Element, value: V) => void;
mutationRunner: (record: ElementPropertyRecord<T, V>) => void;
}
type HTMLRecord = ElementPropertyRecord<HTMLMutation, string>;
type ClassnameRecord = ElementPropertyRecord<ClassnameMutation, string>;
type AttributeRecord = ElementPropertyRecord<AttrMutation, string | null>;
type PositionRecord = ElementPropertyRecord<
PositionMutation,
ElementPositionWithDomNode
>;
interface ElementRecord {
element: Element;
html?: HTMLRecord;
classes?: ClassnameRecord;
attributes: {
[key: string]: AttributeRecord;
};
position?: PositionRecord;
}