feat(publish): auto-localize Leaflet CDN to platform assets
Memind CI / Test, build, and release guards (push) Failing after 3m57s
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:
@@ -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(),
|
||||
|
||||
@@ -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), []);
|
||||
});
|
||||
|
||||
@@ -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 |
@@ -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(
|
||||
|
||||
@@ -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. 发布并绑定页面(推荐)
|
||||
|
||||
|
||||
@@ -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 冒充持久化。记账、日记、打卡、清单、台账、问卷、报名及其管理页都适用。
|
||||
|
||||
Reference in New Issue
Block a user