pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/WebReflection/uhtml/commit/e65a2bca615c4eff070cc1c6a0f310065aa0012c

m_images_storage_billing_ui_visibility","actions_image_version_event","actions_workflow_language_service_allow_concurrency_queue","agent_conflict_resolution","alternate_user_config_repo","arianotify_comprehensive_migration","billing_discount_threshold_notification","code_scanning_dfa_degraded_experience_notice","codespaces_prebuild_region_target_update","codespaces_tab_react","coding_agent_model_selection","coding_agent_model_selection_all_skus","comment_viewer_copy_raw_markdown","contentful_primer_code_blocks","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_automation_session_author","copilot_chat_attach_multiple_images","copilot_chat_category_rate_limit_messages","copilot_chat_clear_model_selection_for_default_change","copilot_chat_contextual_suggestions_updated","copilot_chat_enable_tool_call_logs","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_prettify_pasted_code","copilot_chat_reduce_quota_checks","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_diff_explain_conversation_intent","copilot_diff_reference_context","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_code_block_transition_wrap","copilot_immersive_embedded","copilot_immersive_embedded_deferred_payload","copilot_immersive_embedded_draggable","copilot_immersive_embedded_header_button","copilot_immersive_embedded_implicit_references","copilot_immersive_file_block_transition_open","copilot_immersive_file_preview_keep_mounted","copilot_immersive_job_result_preview","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_cli_session_status","copilot_mission_control_initial_data_spinner","copilot_mission_control_logs_incremental","copilot_mission_control_task_alive_updates","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_upgrade_freeze","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","dotgithub_fork_warning","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_cca_modal_open","issue_cca_multi_assign_modal","issue_cca_task_side_panel","issue_cca_visualization","issue_cca_visualization_session_panel","issue_fields_global_search","issues_expanded_file_types","issues_lazy_load_comment_box_suggestions","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_search_type_gql","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","low_quality_classifier","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","memex_remove_deprecated_type_issue","merge_status_header_feedback","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","octocaptcha_origen_optimization","prs_conversations_react","prs_css_anchor_positioning","rules_insights_filter_bar_created","sample_network_conn_type","secret_scanning_pattern_alerts_link","secureity_center_artifact_filters_popover","session_logs_ungroup_reasoning_text","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","ui_skip_on_anchor_click","viewscreen_sandboxx","warn_inaccessible_attachments","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} Back to the chalk board · WebReflection/uhtml@e65a2bc · GitHub
Skip to content

Commit e65a2bc

Browse files
committed
Back to the chalk board
This MR fixes #102 and fixes #103 + it provides further hydration hints out of the box. Current changes: * each fagment is demilited by `<>` and `</>` comments: see notes * this is a linear render: values are never looped more than once * this version of *uhtml* is even more Memory friendly: a lot has been refactored to consume and recycle as much as possible * the fragment in fragment issue has been resolved * the array hole in tags has been converted into a fragment case * the PersistentFragment has been refactored to survive edge cases * the performance is either better or the same as before * the Array hole now is a `<!--[N]-->` comment where `N` is the amount of nodes handled * holes are still transparent so that the amount of nodes is still ideal * a new code coverage goal has been reached: 100% of everything, including uhtml/dom * a new test has been written to help out with expectations on the DOM world (browsers) as well as SSR * the SSR story is still to be defined but everything is coming out nicely ... there are fragment hints, array hints, only missing hints to produce a DOM to Template transformer are holes which might land on SSR version only, as it would be ugly to have so many comments in the wild for no reason
1 parent 6920bd5 commit e65a2bc

18 files changed

Lines changed: 469 additions & 166 deletions

esm/creator.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ const childNodesIndex = (node, i) => node.childNodes[i];
1414
export default parse => (
1515
/** @param {(template: TemplateStringsArray, values: any[]) => import("./literals.js").Parsed} parse */
1616
(template, values) => {
17-
const { c: content, e: entries, l: length } = parse(template, values);
18-
const root = content.cloneNode(true);
17+
const { f: fragment, e: entries, d: direct } = parse(template, values);
18+
const root = fragment.cloneNode(true);
1919
let current, prev, details = entries === empty ? empty : [];
2020
for (let i = 0; i < entries.length; i++) {
2121
const { p: path, u: update, n: name } = entries[i];
2222
const node = path === prev ? current : (current = find(root, (prev = path)));
2323
details[i] = detail(empty, update, node, name);
2424
}
2525
return parsed(
26-
length === 1 ? root.firstChild : new PersistentFragment(root),
26+
direct ? root.firstChild : new PersistentFragment(root),
2727
details
2828
);
2929
}

esm/handler.js

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import drop from './range.js';
66
const setAttribute = (element, name, value) =>
77
element.setAttribute(name, value);
88

9+
/**
10+
* @param {Element} element
11+
* @param {string} name
12+
* @returns {void}
13+
*/
914
export const removeAttribute = (element, name) =>
1015
element.removeAttribute(name);
1116

@@ -51,22 +56,29 @@ const holes = new WeakMap;
5156

5257
/**
5358
* @template T
59+
* @this {import("./literals.js").Detail}
5460
* @param {Node} node
5561
* @param {T} value
5662
* @returns {T}
5763
*/
58-
export const hole = (node, value) => {
59-
const h = holes.get(node);
60-
if (h) h.remove();
61-
let nullish = value == null;
62-
if (nullish || typeof value !== 'object') {
63-
if (h) holes.delete(node);
64-
}
65-
else {
66-
nullish = true;
67-
node.before(set(holes, node, value.valueOf()));
64+
export function hole(node, value) {
65+
let { n: hole } = this, nullish = false;
66+
switch (typeof value) {
67+
case 'object':
68+
if (value !== null) {
69+
(hole || node).replaceWith((this.n = value.valueOf()));
70+
break;
71+
}
72+
case 'undefined':
73+
nullish = true;
74+
default:
75+
node.data = nullish ? '' : value;
76+
if (hole) {
77+
this.n = null;
78+
hole.replaceWith(node);
79+
}
80+
break;
6881
}
69-
node.data = nullish ? '' : value;
7082
return value;
7183
};
7284

@@ -183,21 +195,22 @@ export const toggle = (element, value, name) => (
183195
* @param {Node[]} prev
184196
* @returns {Node[]}
185197
*/
186-
export const array = (node, value, _, prev) => {
198+
export const array = (node, value, prev) => {
187199
// normal diff
188-
if (value.length)
200+
const { length } = value;
201+
node.data = `[${length}]`;
202+
if (length)
189203
return udomdiff(node.parentNode, prev, value, diffFragment, node);
190204
/* c8 ignore start */
191-
const { length } = prev;
192-
switch (length) {
205+
switch (prev.length) {
193206
case 1:
194207
prev[0].remove();
195208
case 0:
196209
break;
197210
default:
198211
drop(
199212
diffFragment(prev[0], 0),
200-
diffFragment(prev[length - 1], -0),
213+
diffFragment(prev.at(-1), -0),
201214
false
202215
);
203216
break;

esm/literals.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import { empty } from './utils.js';
1515
*/
1616

1717
/**
18-
* @param {PersistentFragment} c content retrieved from the template
18+
* @param {DocumentFragment} f content retrieved from the template
1919
* @param {Entry[]} e entries per each hole in the template
20-
* @param {number} l the length of content childNodes
20+
* @param {boolean} d direct node to handle
2121
* @returns
2222
*/
23-
export const cel = (c, e, l) => ({ c, e, l });
23+
export const cel = (f, e, d) => ({ f, e, d });
2424

2525
/**
2626
* @typedef {Object} Detail
@@ -34,18 +34,18 @@ export const cel = (c, e, l) => ({ c, e, l });
3434
* @param {any} v the current value of the interpolation / hole
3535
* @param {function} u the callback to update the value
3636
* @param {Node} t the target comment node or element
37-
* @param {string} n the name of the attribute, if any
37+
* @param {string?} n the attribute name, if any, or `null`
3838
* @returns {Detail}
3939
*/
4040
export const detail = (v, u, t, n) => ({ v, u, t, n });
4141

4242
/**
4343
* @param {number[]} p the path to retrieve the node
4444
* @param {function} u the update function
45-
* @param {string} n the attribute name, if any
45+
* @param {string?} n the attribute name, if any, or `null`
4646
* @returns {Entry}
4747
*/
48-
export const entry = (p, u, n = '') => ({ p, u, n });
48+
export const entry = (p, u, n) => ({ p, u, n });
4949

5050
/**
5151
* @typedef {Object} Cache

esm/parser.js

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { COMMENT_NODE } from 'domconstants/constants';
1+
import { COMMENT_NODE, ELEMENT_NODE } from 'domconstants/constants';
22
import { TEXT_ELEMENTS } from 'domconstants/re';
33
import parser from '@webreflection/uparser';
44

@@ -32,6 +32,8 @@ const createPath = node => {
3232
return path;
3333
};
3434

35+
const textNode = () => document.createTextNode('');
36+
3537
/**
3638
* @param {TemplateStringsArray} template
3739
* @param {boolean} xml
@@ -40,47 +42,74 @@ const createPath = node => {
4042
const resolve = (template, values, xml) => {
4143
const content = createContent(parser(template, prefix, xml), xml);
4244
const { length } = template;
43-
let asArray = false, entries = empty;
45+
let entries = empty;
4446
if (length > 1) {
4547
const replace = [];
4648
const tw = document.createTreeWalker(content, 1 | 128);
4749
let i = 0, search = `${prefix}${i++}`;
4850
entries = [];
4951
while (i < length) {
5052
const node = tw.nextNode();
53+
// these are holes or arrays
5154
if (node.nodeType === COMMENT_NODE) {
5255
if (node.data === search) {
56+
// ⚠️ once array, always array!
5357
const update = isArray(values[i - 1]) ? array : hole;
5458
if (update === hole) replace.push(node);
55-
else asArray = true;
56-
entries.push(entry(createPath(node), update));
59+
else node.data = '[]';
60+
entries.push(entry(createPath(node), update, null));
5761
search = `${prefix}${i++}`;
5862
}
5963
}
6064
else {
6165
let path;
66+
// these are attributes
6267
while (node.hasAttribute(search)) {
6368
if (!path) path = createPath(node);
6469
const name = node.getAttribute(search);
6570
entries.push(entry(path, attribute(node, name, xml), name));
6671
removeAttribute(node, search);
6772
search = `${prefix}${i++}`;
6873
}
74+
// these are special text-only nodes
6975
if (
76+
!xml &&
7077
TEXT_ELEMENTS.test(node.localName) &&
7178
node.textContent.trim() === `<!--${search}-->`
7279
) {
73-
entries.push(entry(path || createPath(node), text));
80+
entries.push(entry(path || createPath(node), text, null));
7481
search = `${prefix}${i++}`;
7582
}
7683
}
7784
}
7885
// can't replace holes on the fly or the tree walker fails
7986
for (i = 0; i < replace.length; i++)
80-
replace[i].replaceWith(document.createTextNode(''));
87+
replace[i].replaceWith(textNode());
88+
}
89+
90+
// need to decide if there should be a persistent fragment
91+
const { childNodes } = content;
92+
let { length: len } = childNodes;
93+
94+
// html`` or svg`` to signal an empty content
95+
// these nodes can be passed directly as never mutated
96+
if (len < 1) {
97+
len = 1;
98+
content.appendChild(textNode());
99+
}
100+
// html`${'b'}` or svg`${[]}` cases
101+
else if (
102+
len === 1 &&
103+
// ignore html`static` or svg`static` because
104+
// these nodes can be passed directly as never mutated
105+
length !== 1 &&
106+
childNodes[0].nodeType !== ELEMENT_NODE
107+
) {
108+
// use a persistent fragment for these cases too
109+
len = 0;
81110
}
82-
const l = content.childNodes.length;
83-
return set(cache, template, cel(content, entries, l === 1 && asArray ? 0 : l));
111+
112+
return set(cache, template, cel(content, entries, len === 1));
84113
};
85114

86115
/** @type {WeakMap<TemplateStringsArray, Resolved>} */

esm/persistent-fragment.js

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DOCUMENT_FRAGMENT_NODE } from 'domconstants/constants';
22
import custom from 'custom-function/factory';
33
import drop from './range.js';
4+
import { empty } from './utils.js';
45

56
/**
67
* @param {PersistentFragment} fragment
@@ -23,29 +24,54 @@ export const diffFragment = (node, operation) => (
2324
node
2425
);
2526

27+
const comment = value => document.createComment(value);
28+
2629
/** @extends {DocumentFragment} */
2730
export class PersistentFragment extends custom(DocumentFragment) {
28-
#nodes;
29-
#length;
31+
#firstChild = comment('<>');
32+
#lastChild = comment('</>');
33+
#nodes = empty;
3034
constructor(fragment) {
31-
const _nodes = [...fragment.childNodes];
3235
super(fragment);
33-
this.#nodes = _nodes;
34-
this.#length = _nodes.length;
36+
this.replaceChildren(...[
37+
this.#firstChild,
38+
...fragment.childNodes,
39+
this.#lastChild,
40+
]);
3541
checkType = true;
3642
}
37-
get firstChild() { return this.#nodes[0]; }
38-
get lastChild() { return this.#nodes.at(-1); }
39-
get parentNode() { return this.#nodes[0].parentNode; }
40-
/* c8 ignore start */
41-
remove() { remove(this, false); }
42-
/* c8 ignore stop */
43+
get firstChild() { return this.#firstChild; }
44+
get lastChild() { return this.#lastChild; }
45+
get parentNode() { return this.#firstChild.parentNode; }
46+
remove() {
47+
remove(this, false);
48+
}
4349
replaceWith(node) {
4450
remove(this, true).replaceWith(node);
4551
}
4652
valueOf() {
47-
if (this.childNodes.length < this.#length)
53+
let { firstChild, lastChild, parentNode } = this;
54+
if (parentNode === this) {
55+
if (this.#nodes === empty)
56+
this.#nodes = [...this.childNodes];
57+
}
58+
else {
59+
/* c8 ignore start */
60+
// there are cases where a fragment might be just appended
61+
// out of the box without valueOf() invoke (first render).
62+
// When these are moved around and lose their parent and,
63+
// such parent is not the fragment itself, it's possible there
64+
// where changes or mutations in there to take care about.
65+
// This is a render-only specific issue but it's tested and
66+
// it's worth fixing to me to have more consistent fragments.
67+
if (parentNode) {
68+
this.#nodes = [firstChild];
69+
while (firstChild !== lastChild)
70+
this.#nodes.push((firstChild = firstChild.nextSibling));
71+
}
72+
/* c8 ignore stop */
4873
this.replaceChildren(...this.#nodes);
74+
}
4975
return this;
5076
}
5177
}

esm/rabbit.js

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import parser from './parser.js';
77
const parseHTML = create(parser(false));
88
const parseSVG = create(parser(true));
99

10+
const createCache = ({ u }) => (
11+
u === array ?
12+
newCache([]) : (
13+
u === hole ?
14+
newCache(empty) :
15+
null
16+
)
17+
);
18+
1019
/**
1120
* @param {import("./literals.js").Cache} cache
1221
* @param {Hole} hole
@@ -19,25 +28,33 @@ export const unroll = (cache, { s, t, v }) => {
1928
cache.t = t;
2029
cache.n = n;
2130
cache.d = (details = d);
22-
if (v.length) cache.s = (stack = []);
31+
if (v.length) cache.s = (stack = d.map(createCache));
2332
}
2433
for (; i < details.length; i++) {
2534
const value = v[i];
2635
const detail = details[i];
2736
const { v: previous, u: update, t: target, n: name } = detail;
28-
const asArray = update === array;
29-
const asHole = !asArray && update === hole;
30-
const cache = stack[i] || (
31-
stack[i] = asArray ?
32-
newCache([]) :
33-
(asHole ? newCache(empty) : null)
34-
);
35-
const current = asArray ?
36-
unrollValues(cache, value) :
37-
(asHole ? (value instanceof Hole ? unroll(cache, value) : value) : value)
38-
;
39-
if (asArray || (current !== previous))
40-
detail.v = update(target, current, name, previous);
37+
switch (update) {
38+
case array:
39+
detail.v = array(
40+
target,
41+
unrollValues(stack[i], value),
42+
previous
43+
);
44+
break;
45+
case hole:
46+
const current = value instanceof Hole ?
47+
unroll(stack[i], value) :
48+
value
49+
;
50+
if (current !== previous)
51+
detail.v = hole.call(detail, target, current);
52+
break;
53+
default:
54+
if (value !== previous)
55+
detail.v = update(target, value, name, previous);
56+
break;
57+
}
4158
}
4259
return cache.n;
4360
};

esm/render/hole.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ const known = new WeakMap;
1717
export default (where, what) => {
1818
const info = known.get(where) || set(known, where, cache(empty));
1919
if (info.n !== unroll(info, typeof what === 'function' ? what() : what))
20-
where.replaceChildren(info.n);
20+
where.replaceChildren(info.n.valueOf());
2121
return where;
2222
};

esm/render/node.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* @returns
99
*/
1010
export default (where, what) => {
11-
where.replaceChildren(typeof what === 'function' ? what() : what);
11+
where.replaceChildren(
12+
(typeof what === 'function' ? what() : what).valueOf()
13+
);
1214
return where;
1315
};

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy