@mahe_pkm/buzl-html-editor 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -261,7 +261,7 @@
261
261
  if (!editMode) return;
262
262
  const editable = event.target.closest && event.target.closest('[data-buzl-public-edit="true"]');
263
263
  if (!editable) return;
264
- if (editable.closest('a, button, summary')) event.preventDefault();
264
+ if (event.target.closest && event.target.closest('a, button, summary')) event.preventDefault();
265
265
  event.stopPropagation();
266
266
  }, true);
267
267
 
package/dist/server.js CHANGED
@@ -26,9 +26,12 @@ const SITE_ROOT = path.resolve(process.env.BUZL_SITE_ROOT || process.cwd());
26
26
 
27
27
  const PUBLIC_LIVE_EDIT_ENABLED = new Set(['127.0.0.1', 'localhost', '::1'])
28
28
  .has(String(HOST).toLowerCase());
29
- const PUBLIC_EDIT_SELECTOR = [
30
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'li', 'a', 'button', 'summary',
31
- 'figcaption', 'blockquote', 'cite', 'label', 'small', 'strong', 'em',
29
+ const PUBLIC_BLOCK_EDIT_SELECTOR = [
30
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'figcaption', 'blockquote',
31
+ 'cite', 'label',
32
+ ].join(',');
33
+ const PUBLIC_STANDALONE_EDIT_SELECTOR = [
34
+ 'span', 'a', 'small', 'strong', 'em',
32
35
  ].join(',');
33
36
  const PUBLIC_EDIT_SESSION_TTL_MS = 2 * 60 * 60 * 1000;
34
37
  const publicEditSessions = new Map();
@@ -958,20 +961,36 @@ function removePublicEditArtifacts($) {
958
961
  }
959
962
 
960
963
  function collectPublicEditCandidates($) {
961
- const candidates = [];
962
- $(PUBLIC_EDIT_SELECTOR).each((_, element) => {
964
+ const isSafeTextTarget = (element) => {
963
965
  const target = $(element);
964
966
  const text = target.text();
965
- if (!text || !text.trim()) return;
966
- if (target.closest('svg, [aria-hidden="true"], script, style, noscript, input, textarea, select').length) {
967
- return;
967
+ if (!text || !text.trim()) return false;
968
+ if (target.closest('svg, [aria-hidden="true"], script, style, noscript, input, textarea, select, iframe, canvas').length) {
969
+ return false;
968
970
  }
971
+ return !target.find('input, textarea, select, button, summary, iframe, canvas, video, audio, [contenteditable="true"]').length;
972
+ };
969
973
 
970
- candidates.push(element);
974
+ const rawBlockCandidates = [];
975
+ $(PUBLIC_BLOCK_EDIT_SELECTOR).each((_, element) => {
976
+ if (isSafeTextTarget(element)) rawBlockCandidates.push(element);
977
+ });
978
+ // Prefer the innermost semantic block. This avoids overlapping edit targets
979
+ // for nested list items or a paragraph inside a blockquote.
980
+ const rawBlockSet = new Set(rawBlockCandidates);
981
+ const blockCandidates = rawBlockCandidates.filter((element) => !$(element).find('*').toArray()
982
+ .some((descendant) => rawBlockSet.has(descendant)));
983
+ const blockSet = new Set(blockCandidates);
984
+ const standaloneCandidates = [];
985
+ $(PUBLIC_STANDALONE_EDIT_SELECTOR).each((_, element) => {
986
+ if (!isSafeTextTarget(element)) return;
987
+ const target = $(element);
988
+ if (target.parents().toArray().some((parent) => blockSet.has(parent))) return;
989
+ standaloneCandidates.push(element);
971
990
  });
972
- const candidateSet = new Set(candidates);
973
- return candidates.filter((element) => !$(element).find('*').toArray()
974
- .some((descendant) => candidateSet.has(descendant)));
991
+ const standaloneSet = new Set(standaloneCandidates);
992
+ return [...blockCandidates, ...standaloneCandidates.filter((element) => !$(element).find('*').toArray()
993
+ .some((descendant) => standaloneSet.has(descendant)))];
975
994
  }
976
995
 
977
996
  function sanitizePublicEditHtml(value) {
@@ -984,15 +1003,22 @@ function sanitizePublicEditHtml(value) {
984
1003
  root.find('script, style, iframe, object, embed, form, input, textarea, select, option, button, link, meta')
985
1004
  .remove();
986
1005
 
987
- const allowedTags = new Set(['br', 'strong', 'b', 'em', 'i', 'u', 's', 'sub', 'sup', 'span']);
1006
+ const allowedTags = new Set(['br', 'strong', 'b', 'em', 'i', 'u', 's', 'sub', 'sup', 'span', 'a']);
988
1007
  root.find('*').toArray().reverse().forEach((element) => {
989
1008
  const tagName = String(element.tagName || element.name || '').toLowerCase();
990
1009
  if (!allowedTags.has(tagName)) {
991
1010
  fragment(element).replaceWith(fragment(element).text());
992
1011
  return;
993
1012
  }
994
- for (const attribute of Object.keys(element.attribs || {})) {
995
- fragment(element).removeAttr(attribute);
1013
+ for (const [attribute, rawValue] of Object.entries(element.attribs || {})) {
1014
+ const name = attribute.toLowerCase();
1015
+ if (tagName === 'a' && ['href', 'target', 'rel'].includes(name)) {
1016
+ if (name === 'href' && /^(?:javascript|data|vbscript):/i.test(String(rawValue).trim())) {
1017
+ fragment(element).removeAttr(attribute);
1018
+ }
1019
+ } else {
1020
+ fragment(element).removeAttr(attribute);
1021
+ }
996
1022
  }
997
1023
  });
998
1024
  return root.html() || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mahe_pkm/buzl-html-editor",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Local visual editor and static-site server for multi-page HTML websites",
5
5
  "author": "mahe_pkm",
6
6
  "type": "commonjs",