@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.
- package/dist/public/public-live-edit.js +1 -1
- package/dist/server.js +41 -15
- package/package.json +1 -1
|
@@ -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 (
|
|
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
|
|
30
|
-
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', '
|
|
31
|
-
'
|
|
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
|
|
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
|
-
|
|
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
|
|
973
|
-
return
|
|
974
|
-
.some((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.
|
|
995
|
-
|
|
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() || '';
|