feat(publish): auto-localize Leaflet CDN to platform assets
Memind CI / Test, build, and release guards (push) Failing after 3m57s

Ship Leaflet 1.9.4 under public/assets/leaflet and rewrite common CDN script/stylesheet URLs during publish and public HTML delivery, matching the existing Chart.js guard pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-25 16:05:50 +08:00
parent 9332bacabd
commit 1ec971515b
14 changed files with 796 additions and 9 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import { localizeGoogleFontsCss } from './mindspace-html-localize.mjs';
import { rewriteKnownCdnScriptSources } from './mindspace-published-script-localize.mjs';
import { rewriteKnownCdnAssetSources } from './mindspace-published-script-localize.mjs';
import { replacePrivateResourceReferences, scanContent } from './mindspace-content-scan.mjs';
import { pageInternals, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import { readWorkspacePublishHtml } from './mindspace-workspace-path.mjs';
@@ -403,7 +403,7 @@ async function prepareHtmlPublishContent({
h5Root = null,
}) {
let publishContent = (await localizeGoogleFontsCss(html)).html;
publishContent = rewriteKnownCdnScriptSources(publishContent).html;
publishContent = rewriteKnownCdnAssetSources(publishContent).html;
publishContent = replaceImgproxyStandardImageReferences(
publishContent,
resolvePublicBaseUrl(),
+75 -2
View File
@@ -1,4 +1,8 @@
const SCRIPT_SRC_PATTERN = /<script\b([^>]*)\bsrc\s*=\s*(['"])([^'"]+)\2([^>]*)>/gi;
const STYLESHEET_HREF_PATTERN =
/<link\b([^>]*)\brel\s*=\s*(['"])stylesheet\2([^>]*)\bhref\s*=\s*(['"])([^'"]+)\4([^>]*)>/gi;
const STYLESHEET_HREF_ALT_PATTERN =
/<link\b([^>]*)\bhref\s*=\s*(['"])([^'"]+)\2([^>]*)\brel\s*=\s*(['"])stylesheet\5([^>]*)>/gi;
export const KNOWN_PLATFORM_SCRIPT_ALIASES = [
{
@@ -10,6 +14,27 @@ export const KNOWN_PLATFORM_SCRIPT_ALIASES = [
/unpkg\.com\/chart\.js(?:@[^/]+)?\/dist\/chart\.(?:umd\.)?min\.js/i,
],
},
{
id: 'leaflet',
target: '/assets/leaflet/leaflet.js',
patterns: [
/cdn\.jsdelivr\.net\/npm\/leaflet(?:@[^/]+)?\/dist\/leaflet\.js/i,
/cdnjs\.cloudflare\.com\/ajax\/libs\/leaflet\/[^/]+\/leaflet\.js/i,
/unpkg\.com\/leaflet(?:@[^/]+)?\/dist\/leaflet\.js/i,
],
},
];
export const KNOWN_PLATFORM_STYLESHEET_ALIASES = [
{
id: 'leaflet',
target: '/assets/leaflet/leaflet.css',
patterns: [
/cdn\.jsdelivr\.net\/npm\/leaflet(?:@[^/]+)?\/dist\/leaflet\.css/i,
/cdnjs\.cloudflare\.com\/ajax\/libs\/leaflet\/[^/]+\/leaflet\.css/i,
/unpkg\.com\/leaflet(?:@[^/]+)?\/dist\/leaflet\.css/i,
],
},
];
function normalizeScriptUrl(value) {
@@ -24,11 +49,11 @@ export function isExternalScriptSrc(src) {
return false;
}
function resolveKnownPlatformScriptTarget(src) {
function resolveKnownPlatformTarget(src, aliases) {
const normalized = normalizeScriptUrl(src);
if (!isExternalScriptSrc(normalized)) return null;
const probe = normalized.startsWith('//') ? `https:${normalized}` : normalized;
for (const alias of KNOWN_PLATFORM_SCRIPT_ALIASES) {
for (const alias of aliases) {
if (alias.patterns.some((pattern) => pattern.test(probe))) {
return alias.target;
}
@@ -36,6 +61,14 @@ function resolveKnownPlatformScriptTarget(src) {
return null;
}
function resolveKnownPlatformScriptTarget(src) {
return resolveKnownPlatformTarget(src, KNOWN_PLATFORM_SCRIPT_ALIASES);
}
function resolveKnownPlatformStylesheetTarget(href) {
return resolveKnownPlatformTarget(href, KNOWN_PLATFORM_STYLESHEET_ALIASES);
}
export function findExternalScriptSources(html) {
const sources = [];
const seen = new Set();
@@ -66,3 +99,43 @@ export function rewriteKnownCdnScriptSources(html) {
rewrites,
};
}
export function rewriteKnownCdnStylesheetSources(html) {
const rewrites = [];
let next = String(html ?? '');
const replaceStylesheet = (full, before, quote, href, after) => {
const normalized = normalizeScriptUrl(href);
const target = resolveKnownPlatformStylesheetTarget(normalized);
if (!target) return full;
rewrites.push({ from: normalized, to: target });
return `<link${before}href=${quote}${target}${quote}${after}>`;
};
next = next.replace(
STYLESHEET_HREF_PATTERN,
(full, before, _relQuote, _middle, hrefQuote, href, after) =>
replaceStylesheet(full, before, hrefQuote, href, after),
);
next = next.replace(
STYLESHEET_HREF_ALT_PATTERN,
(full, before, hrefQuote, href, _middle, _relQuote, after) =>
replaceStylesheet(full, before, hrefQuote, href, after),
);
return {
html: next,
rewrittenCount: rewrites.length,
rewrites,
};
}
export function rewriteKnownCdnAssetSources(html) {
const scripts = rewriteKnownCdnScriptSources(html);
const stylesheets = rewriteKnownCdnStylesheetSources(scripts.html);
return {
html: stylesheets.html,
rewrittenCount: scripts.rewrittenCount + stylesheets.rewrittenCount,
rewrites: [...scripts.rewrites, ...stylesheets.rewrites],
};
}
@@ -3,7 +3,9 @@ import test from 'node:test';
import {
findExternalScriptSources,
isExternalScriptSrc,
rewriteKnownCdnAssetSources,
rewriteKnownCdnScriptSources,
rewriteKnownCdnStylesheetSources,
} from './mindspace-published-script-localize.mjs';
test('isExternalScriptSrc detects http and protocol-relative urls', () => {
@@ -36,3 +38,41 @@ test('findExternalScriptSources lists unresolved external scripts only', () => {
const rewritten = rewriteKnownCdnScriptSources(source).html;
assert.deepEqual(findExternalScriptSources(rewritten), ['https://cdn.example.com/app.js']);
});
test('rewriteKnownCdnStylesheetSources rewrites leaflet css cdn urls to platform asset', () => {
const source = `<!doctype html><html><head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link href="//cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" rel="stylesheet">
<link rel="stylesheet" href="/assets/local.css">
</head><body></body></html>`;
const result = rewriteKnownCdnStylesheetSources(source);
assert.equal(result.rewrittenCount, 2);
assert.match(result.html, /href="\/assets\/leaflet\/leaflet\.css"/g);
assert.doesNotMatch(result.html, /unpkg\.com\/leaflet/);
assert.doesNotMatch(result.html, /cdn\.jsdelivr\.net\/npm\/leaflet/);
assert.match(result.html, /href="\/assets\/local\.css"/);
});
test('rewriteKnownCdnScriptSources rewrites leaflet js cdn urls to platform asset', () => {
const source = `<!doctype html><html><body>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="//cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
</body></html>`;
const result = rewriteKnownCdnScriptSources(source);
assert.equal(result.rewrittenCount, 2);
assert.match(result.html, /src="\/assets\/leaflet\/leaflet\.js"/g);
assert.doesNotMatch(result.html, /unpkg\.com\/leaflet/);
});
test('rewriteKnownCdnAssetSources rewrites leaflet css and js together', () => {
const source = `<!doctype html><html><head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
</head><body>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</body></html>`;
const result = rewriteKnownCdnAssetSources(source);
assert.equal(result.rewrittenCount, 2);
assert.match(result.html, /href="\/assets\/leaflet\/leaflet\.css"/);
assert.match(result.html, /src="\/assets\/leaflet\/leaflet\.js"/);
assert.deepEqual(findExternalScriptSources(result.html), []);
});
+4
View File
@@ -9,9 +9,13 @@ const SCRIPT_SRC_PATTERN = /\bsrc\s*=\s*(['"])([^'"]+)\1/i;
const TRUSTED_SCRIPT_SRC_PATTERNS = [
/^\/assets\/page-data-client\.js(?:[?#].*)?$/i,
/^\/assets\/chart\.umd\.min\.js(?:[?#].*)?$/i,
/^\/assets\/leaflet\/leaflet\.js(?:[?#].*)?$/i,
/^https:\/\/cdn\.jsdelivr\.net\/npm\/chart\.js(?:@[^/]+)?\/dist\/chart\.(?:umd\.)?min\.js(?:[?#].*)?$/i,
/^https:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/Chart\.js\/[^/]+\/chart\.(?:umd\.)?min\.js(?:[?#].*)?$/i,
/^https:\/\/unpkg\.com\/chart\.js(?:@[^/]+)?\/dist\/chart\.(?:umd\.)?min\.js(?:[?#].*)?$/i,
/^https:\/\/cdn\.jsdelivr\.net\/npm\/leaflet(?:@[^/]+)?\/dist\/leaflet\.js(?:[?#].*)?$/i,
/^https:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/leaflet\/[^/]+\/leaflet\.js(?:[?#].*)?$/i,
/^https:\/\/unpkg\.com\/leaflet(?:@[^/]+)?\/dist\/leaflet\.js(?:[?#].*)?$/i,
];
const MAX_ZIP_COMPRESSION_RATIO = 100;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

+661
View File
@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
File diff suppressed because one or more lines are too long
@@ -27,7 +27,7 @@ import {
publishedPageCsp,
} from '../mindspace-published-page-csp.mjs';
import {
rewriteKnownCdnScriptSources,
rewriteKnownCdnAssetSources,
} from '../mindspace-published-script-localize.mjs';
import {
injectOgTags,
@@ -188,7 +188,7 @@ export function createPortalWorkspacePublicationDelivery({
}
let html =
rewriteKnownCdnScriptSources(
rewriteKnownCdnAssetSources(
delivery.html,
).html;
html = preparePublicHtmlAssetDelivery(
+5 -2
View File
@@ -211,16 +211,19 @@ await client.deleteRow('dataset_name', rowId);
禁止发明 `softDeleteRows``deleteRows` 等不存在的方法;服务端会根据 dataset 的 `soft_delete` 授权把 `deleteRow` 转换为软删除。
- **第三方 JS 库**Chart.js、ECharts 等)禁止写 CDN `https://...`;发布页 CSP 只允许同源脚本。优先用平台预置路径,或下载到 `public/assets/` 后用相对路径引用:
- **第三方 JS 库**Chart.js、Leaflet、ECharts 等)禁止写 CDN `https://...`;发布页 CSP 只允许同源脚本。优先用平台预置路径,或下载到 `public/assets/` 后用相对路径引用:
```html
<!-- 推荐:平台已预置 Chart.js -->
<script src="/assets/chart.umd.min.js"></script>
<!-- 地图页:平台已预置 Leaflet -->
<link rel="stylesheet" href="/assets/leaflet/leaflet.css">
<script src="/assets/leaflet/leaflet.js"></script>
<!-- 或页面自有副本 -->
<script src="assets/chart.umd.min.js"></script>
```
平台会在发布/访问时自动把常见 Chart.js CDN 改写为 `/assets/chart.umd.min.js`;其它未知 CDN 会在发布检查中被拦截。
平台会在发布/访问时自动把常见 Chart.js、Leaflet CDN 改写为 `/assets/chart.umd.min.js``/assets/leaflet/*`;其它未知 CDN 会在发布检查中被拦截。
### 4. 发布并绑定页面(推荐)
+1 -1
View File
@@ -24,7 +24,7 @@ description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报
7. 默认只生成 HTML;不要在没有明确需求时强制生成 Word、PDF、长图等伴生文件
8. 只有用户明确要求 **Word/PDF 等二进制下载** 时:文件单独落盘(如 `public/方案.docx`),链接用相对路径;**禁止**在 HTML 内用 `data:...;base64,...` 嵌入 docx(易截断损坏)
9. 只有用户明确要求**长图下载**时:必须先 `load_skill``long-image-download`,调用 `generate_long_image` 生成同目录 `public/<页面名>.long.png`,再返回长图预览与下载链接;禁止把 `.thumbnail.svg` 当成长图
10. **禁止**在 HTML 中用 CDN `https://...` 引用 `<script src>`MindSpace 发布页 CSP 仅允许同源脚本。常用库优先用平台路径(如 Chart.js → `/assets/chart.umd.min.js`),或下载到 `public/assets/` 后相对引用
10. **禁止**在 HTML 中用 CDN `https://...` 引用 `<script src>`MindSpace 发布页 CSP 仅允许同源脚本。常用库优先用平台路径(如 Chart.js → `/assets/chart.umd.min.js`Leaflet → `/assets/leaflet/leaflet.js` + `/assets/leaflet/leaflet.css`),或下载到 `public/assets/` 后相对引用;发布时会自动改写常见 Chart.js / Leaflet CDN
11. **纯静态页(无 Page Data 问卷/后台)禁止调用 `private_data_bind_workspace_page`**`write_file` / `edit_file` 落盘 `public/*.html` 后,直接按下方「回复格式」交付 **MindSpace 路径**`/MindSpace/<用户ID>/public/...`),无需 bind、无需口令
12. **微信分享必须使用 MindSpace 路径**:该路由会在服务端注入 `og:site_name=TKMind 智趣`、封面缩略图(`*.thumbnail.png`)与微信 JS-SDK 分享桥;禁止交付 `/u/用户名/pages/...` 作为用户可见链接
13. **只要页面接收用户输入并要求以后查看、汇总、统计、修改或删除,就不是纯静态页**。禁止使用 `localStorage` / `sessionStorage` / `IndexedDB` 保存任何数据;必须立即 `load_skill``page-data-collect`,通过 Page Data API 写入当前用户专属 PostgreSQL schema。禁止 SQLite、静态 JSON/JS 文件或内存 fallback 冒充持久化。记账、日记、打卡、清单、台账、问卷、报名及其管理页都适用。