quikdown 1.1.0 → 1.1.1
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/README.md +11 -3
- package/dist/quikdown.cjs +2 -2
- package/dist/quikdown.dark.css +1 -1
- package/dist/quikdown.esm.js +2 -2
- package/dist/quikdown.esm.min.js +2 -2
- package/dist/quikdown.light.css +1 -1
- package/dist/quikdown.umd.js +2 -2
- package/dist/quikdown.umd.min.js +2 -2
- package/dist/quikdown_bd.cjs +2 -2
- package/dist/quikdown_bd.esm.js +2 -2
- package/dist/quikdown_bd.esm.min.js +2 -2
- package/dist/quikdown_bd.umd.js +2 -2
- package/dist/quikdown_bd.umd.min.js +2 -2
- package/dist/quikdown_edit.cjs +1860 -75
- package/dist/quikdown_edit.esm.js +1860 -75
- package/dist/quikdown_edit.esm.min.js +3 -4
- package/dist/quikdown_edit.esm.min.js.map +1 -1
- package/dist/quikdown_edit.umd.js +1860 -75
- package/dist/quikdown_edit.umd.min.js +3 -4
- package/dist/quikdown_edit.umd.min.js.map +1 -1
- package/package.json +5 -5
package/dist/quikdown_edit.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quikdown Editor - Drop-in Markdown Parser
|
|
3
|
-
* @version 1.1.
|
|
3
|
+
* @version 1.1.1
|
|
4
4
|
* @license BSD-2-Clause
|
|
5
5
|
* @copyright DeftIO 2025
|
|
6
6
|
*/
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
// Version will be injected at build time
|
|
23
|
-
const quikdownVersion = '1.1.
|
|
23
|
+
const quikdownVersion = '1.1.1';
|
|
24
24
|
|
|
25
25
|
// Constants for reuse
|
|
26
26
|
const CLASS_PREFIX = 'quikdown-';
|
|
@@ -1066,6 +1066,1472 @@ if (typeof window !== 'undefined') {
|
|
|
1066
1066
|
window.quikdown_bd = quikdown_bd;
|
|
1067
1067
|
}
|
|
1068
1068
|
|
|
1069
|
+
/**
|
|
1070
|
+
* Rich copy functionality for QuikdownEditor
|
|
1071
|
+
* Handles copying rendered content with proper formatting for pasting into rich text editors
|
|
1072
|
+
*/
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Get platform information
|
|
1076
|
+
* @returns {string} The detected platform: 'macos', 'windows', 'linux', or 'unknown'
|
|
1077
|
+
*/
|
|
1078
|
+
function getPlatform() {
|
|
1079
|
+
const platform = navigator.platform?.toLowerCase() || '';
|
|
1080
|
+
const userAgent = navigator.userAgent?.toLowerCase() || '';
|
|
1081
|
+
|
|
1082
|
+
if (platform.includes('mac') || userAgent.includes('mac')) {
|
|
1083
|
+
return 'macos';
|
|
1084
|
+
} else if (userAgent.includes('windows')) {
|
|
1085
|
+
return 'windows';
|
|
1086
|
+
} else if (userAgent.includes('linux')) {
|
|
1087
|
+
return 'linux';
|
|
1088
|
+
}
|
|
1089
|
+
return 'unknown';
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Copy to clipboard using HTML selection fallback (for Safari)
|
|
1094
|
+
* Uses div with selection to preserve HTML formatting
|
|
1095
|
+
* @param {string} html - HTML content to copy
|
|
1096
|
+
* @returns {boolean} Success status
|
|
1097
|
+
*/
|
|
1098
|
+
function copyToClipboard(html) {
|
|
1099
|
+
let tempDiv;
|
|
1100
|
+
let result;
|
|
1101
|
+
|
|
1102
|
+
try {
|
|
1103
|
+
// Use a div instead of textarea to preserve HTML formatting
|
|
1104
|
+
tempDiv = document.createElement('div');
|
|
1105
|
+
tempDiv.style.position = 'fixed';
|
|
1106
|
+
tempDiv.style.left = '-9999px';
|
|
1107
|
+
tempDiv.style.top = '0';
|
|
1108
|
+
tempDiv.style.width = '1px';
|
|
1109
|
+
tempDiv.style.height = '1px';
|
|
1110
|
+
tempDiv.style.overflow = 'hidden';
|
|
1111
|
+
tempDiv.innerHTML = html;
|
|
1112
|
+
|
|
1113
|
+
document.body.appendChild(tempDiv);
|
|
1114
|
+
|
|
1115
|
+
// Select the HTML content
|
|
1116
|
+
const range = document.createRange();
|
|
1117
|
+
range.selectNodeContents(tempDiv);
|
|
1118
|
+
const selection = window.getSelection();
|
|
1119
|
+
selection.removeAllRanges();
|
|
1120
|
+
selection.addRange(range);
|
|
1121
|
+
|
|
1122
|
+
// Try to copy
|
|
1123
|
+
result = document.execCommand('copy');
|
|
1124
|
+
|
|
1125
|
+
// Clear selection
|
|
1126
|
+
selection.removeAllRanges();
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
console.error('Fallback copy failed:', err);
|
|
1129
|
+
result = false;
|
|
1130
|
+
} finally {
|
|
1131
|
+
if (tempDiv && tempDiv.parentNode) {
|
|
1132
|
+
document.body.removeChild(tempDiv);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Convert SVG to PNG blob (based on squibview's implementation)
|
|
1141
|
+
* @param {SVGElement} svgElement - The SVG element to convert
|
|
1142
|
+
* @returns {Promise<Blob>} A promise that resolves with the PNG blob
|
|
1143
|
+
*/
|
|
1144
|
+
async function svgToPng(svgElement, needsWhiteBackground = false) {
|
|
1145
|
+
return new Promise((resolve, reject) => {
|
|
1146
|
+
const svgString = new XMLSerializer().serializeToString(svgElement);
|
|
1147
|
+
const canvas = document.createElement('canvas');
|
|
1148
|
+
const ctx = canvas.getContext('2d');
|
|
1149
|
+
const img = new Image();
|
|
1150
|
+
|
|
1151
|
+
const scale = 2;
|
|
1152
|
+
|
|
1153
|
+
// Check if this is a Mermaid-generated SVG (they don't have explicit width/height attributes)
|
|
1154
|
+
const isMermaidSvg = svgElement.closest('.mermaid') || svgElement.classList.contains('mermaid');
|
|
1155
|
+
const hasExplicitDimensions = svgElement.getAttribute('width') && svgElement.getAttribute('height');
|
|
1156
|
+
|
|
1157
|
+
let svgWidth, svgHeight;
|
|
1158
|
+
|
|
1159
|
+
if (isMermaidSvg || !hasExplicitDimensions) {
|
|
1160
|
+
// For Mermaid or other generated SVGs, prioritize computed dimensions
|
|
1161
|
+
svgWidth = svgElement.clientWidth ||
|
|
1162
|
+
(svgElement.viewBox && svgElement.viewBox.baseVal.width) ||
|
|
1163
|
+
parseFloat(svgElement.getAttribute('width')) || 400;
|
|
1164
|
+
svgHeight = svgElement.clientHeight ||
|
|
1165
|
+
(svgElement.viewBox && svgElement.viewBox.baseVal.height) ||
|
|
1166
|
+
parseFloat(svgElement.getAttribute('height')) || 300;
|
|
1167
|
+
} else {
|
|
1168
|
+
// For explicit SVGs (like fenced SVG blocks), prioritize explicit attributes
|
|
1169
|
+
svgWidth = parseFloat(svgElement.getAttribute('width')) ||
|
|
1170
|
+
(svgElement.viewBox && svgElement.viewBox.baseVal.width) ||
|
|
1171
|
+
svgElement.clientWidth || 400;
|
|
1172
|
+
svgHeight = parseFloat(svgElement.getAttribute('height')) ||
|
|
1173
|
+
(svgElement.viewBox && svgElement.viewBox.baseVal.height) ||
|
|
1174
|
+
svgElement.clientHeight || 300;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Ensure the SVG string has explicit dimensions by modifying it if necessary
|
|
1178
|
+
let modifiedSvgString = svgString;
|
|
1179
|
+
if (svgWidth && svgHeight) {
|
|
1180
|
+
// Create a temporary SVG element to modify the serialized string
|
|
1181
|
+
const tempDiv = document.createElement('div');
|
|
1182
|
+
tempDiv.innerHTML = svgString;
|
|
1183
|
+
const tempSvg = tempDiv.querySelector('svg');
|
|
1184
|
+
if (tempSvg) {
|
|
1185
|
+
tempSvg.setAttribute('width', svgWidth.toString());
|
|
1186
|
+
tempSvg.setAttribute('height', svgHeight.toString());
|
|
1187
|
+
modifiedSvgString = new XMLSerializer().serializeToString(tempSvg);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
canvas.width = svgWidth * scale;
|
|
1192
|
+
canvas.height = svgHeight * scale;
|
|
1193
|
+
ctx.scale(scale, scale);
|
|
1194
|
+
|
|
1195
|
+
img.onload = () => {
|
|
1196
|
+
try {
|
|
1197
|
+
// Add white background for math equations (they often have transparent backgrounds)
|
|
1198
|
+
if (needsWhiteBackground) {
|
|
1199
|
+
ctx.fillStyle = 'white';
|
|
1200
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
ctx.drawImage(img, 0, 0, svgWidth, svgHeight);
|
|
1204
|
+
canvas.toBlob(blob => {
|
|
1205
|
+
resolve(blob);
|
|
1206
|
+
}, 'image/png', 1.0);
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
reject(err);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
img.onerror = reject;
|
|
1213
|
+
// Use data URI instead of blob URL to avoid tainting the canvas
|
|
1214
|
+
const svgDataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(modifiedSvgString)}`;
|
|
1215
|
+
img.src = svgDataUrl;
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Rasterize a GeoJSON Leaflet map to PNG data URL (following Gem's guide)
|
|
1221
|
+
* @param {HTMLElement} liveContainer - The live map container element
|
|
1222
|
+
* @returns {Promise<string|null>} PNG data URL or null if failed
|
|
1223
|
+
*/
|
|
1224
|
+
async function rasterizeGeoJSONMap(liveContainer) {
|
|
1225
|
+
try {
|
|
1226
|
+
const map = liveContainer._map;
|
|
1227
|
+
if (!map) {
|
|
1228
|
+
console.warn('No map found on container');
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Get container dimensions
|
|
1233
|
+
const mapRect = liveContainer.getBoundingClientRect();
|
|
1234
|
+
const width = Math.round(mapRect.width);
|
|
1235
|
+
const height = Math.round(mapRect.height);
|
|
1236
|
+
|
|
1237
|
+
if (width === 0 || height === 0) {
|
|
1238
|
+
console.warn('Map container has zero dimensions');
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Create canvas sized to the map container
|
|
1243
|
+
const canvas = document.createElement('canvas');
|
|
1244
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1245
|
+
|
|
1246
|
+
// Set canvas size with DPR for sharpness
|
|
1247
|
+
canvas.width = width * dpr;
|
|
1248
|
+
canvas.height = height * dpr;
|
|
1249
|
+
canvas.style.width = width + 'px';
|
|
1250
|
+
canvas.style.height = height + 'px';
|
|
1251
|
+
|
|
1252
|
+
const ctx = canvas.getContext('2d');
|
|
1253
|
+
ctx.scale(dpr, dpr);
|
|
1254
|
+
|
|
1255
|
+
// White background
|
|
1256
|
+
ctx.fillStyle = '#FFFFFF';
|
|
1257
|
+
ctx.fillRect(0, 0, width, height);
|
|
1258
|
+
|
|
1259
|
+
// 1. Draw tiles from THIS container only
|
|
1260
|
+
const tiles = liveContainer.querySelectorAll('.leaflet-tile');
|
|
1261
|
+
|
|
1262
|
+
const tilePromises = [];
|
|
1263
|
+
for (const tile of tiles) {
|
|
1264
|
+
tilePromises.push(new Promise((resolve) => {
|
|
1265
|
+
const img = new Image();
|
|
1266
|
+
img.crossOrigin = 'anonymous';
|
|
1267
|
+
|
|
1268
|
+
img.onload = () => {
|
|
1269
|
+
try {
|
|
1270
|
+
// Calculate tile position relative to container
|
|
1271
|
+
const tileRect = tile.getBoundingClientRect();
|
|
1272
|
+
const offsetX = tileRect.left - mapRect.left;
|
|
1273
|
+
const offsetY = tileRect.top - mapRect.top;
|
|
1274
|
+
|
|
1275
|
+
// Draw tile at correct position
|
|
1276
|
+
ctx.drawImage(img, offsetX, offsetY, tileRect.width, tileRect.height);
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
console.warn('Failed to draw tile:', err);
|
|
1279
|
+
}
|
|
1280
|
+
resolve();
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
img.onerror = () => {
|
|
1284
|
+
console.warn('Failed to load tile:', tile.src);
|
|
1285
|
+
resolve();
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
img.src = tile.src;
|
|
1289
|
+
}));
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Wait for all tiles to load
|
|
1293
|
+
await Promise.all(tilePromises);
|
|
1294
|
+
|
|
1295
|
+
// 2. Draw vector overlays (SVG paths for GeoJSON features)
|
|
1296
|
+
const svgOverlays = liveContainer.querySelectorAll('svg:not(.leaflet-attribution-flag)');
|
|
1297
|
+
|
|
1298
|
+
for (const svg of svgOverlays) {
|
|
1299
|
+
// Skip attribution/control overlays
|
|
1300
|
+
if (svg.closest('.leaflet-control')) continue;
|
|
1301
|
+
|
|
1302
|
+
try {
|
|
1303
|
+
const svgRect = svg.getBoundingClientRect();
|
|
1304
|
+
const offsetX = svgRect.left - mapRect.left;
|
|
1305
|
+
const offsetY = svgRect.top - mapRect.top;
|
|
1306
|
+
|
|
1307
|
+
// Serialize SVG
|
|
1308
|
+
const serializer = new XMLSerializer();
|
|
1309
|
+
const svgStr = serializer.serializeToString(svg);
|
|
1310
|
+
const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
|
|
1311
|
+
const url = URL.createObjectURL(svgBlob);
|
|
1312
|
+
|
|
1313
|
+
// Draw SVG overlay
|
|
1314
|
+
await new Promise((resolve, reject) => {
|
|
1315
|
+
const img = new Image();
|
|
1316
|
+
img.onload = () => {
|
|
1317
|
+
ctx.drawImage(img, offsetX, offsetY, svgRect.width, svgRect.height);
|
|
1318
|
+
URL.revokeObjectURL(url);
|
|
1319
|
+
resolve();
|
|
1320
|
+
};
|
|
1321
|
+
img.onerror = () => {
|
|
1322
|
+
URL.revokeObjectURL(url);
|
|
1323
|
+
reject(new Error('Failed to load SVG overlay'));
|
|
1324
|
+
};
|
|
1325
|
+
img.src = url;
|
|
1326
|
+
});
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
console.warn('Failed to draw SVG overlay:', err);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// 3. Draw marker icons if any
|
|
1333
|
+
const markerIcons = liveContainer.querySelectorAll('.leaflet-marker-icon');
|
|
1334
|
+
|
|
1335
|
+
for (const marker of markerIcons) {
|
|
1336
|
+
try {
|
|
1337
|
+
const img = new Image();
|
|
1338
|
+
img.crossOrigin = 'anonymous';
|
|
1339
|
+
|
|
1340
|
+
await new Promise((resolve) => {
|
|
1341
|
+
img.onload = () => {
|
|
1342
|
+
const markerRect = marker.getBoundingClientRect();
|
|
1343
|
+
const offsetX = markerRect.left - mapRect.left;
|
|
1344
|
+
const offsetY = markerRect.top - mapRect.top;
|
|
1345
|
+
ctx.drawImage(img, offsetX, offsetY, markerRect.width, markerRect.height);
|
|
1346
|
+
resolve();
|
|
1347
|
+
};
|
|
1348
|
+
img.onerror = resolve;
|
|
1349
|
+
img.src = marker.src;
|
|
1350
|
+
});
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
console.warn('Failed to draw marker icon:', err);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Return PNG data URL
|
|
1357
|
+
return canvas.toDataURL('image/png', 1.0);
|
|
1358
|
+
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
console.error('Failed to rasterize GeoJSON map:', error);
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Get rendered content as rich HTML suitable for clipboard
|
|
1367
|
+
* @param {HTMLElement} previewPanel - The preview panel element to copy from
|
|
1368
|
+
* @returns {Promise<{success: boolean, html?: string, text?: string}>}
|
|
1369
|
+
*/
|
|
1370
|
+
async function getRenderedContent(previewPanel) {
|
|
1371
|
+
if (!previewPanel) {
|
|
1372
|
+
throw new Error('No preview panel available');
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Check if MathJax needs to render (only if not already rendered)
|
|
1376
|
+
const mathBlocks = previewPanel.querySelectorAll('.math-display');
|
|
1377
|
+
if (mathBlocks.length > 0) {
|
|
1378
|
+
// Check if already rendered (has mjx-container inside)
|
|
1379
|
+
const needsRendering = Array.from(mathBlocks).some(block => !block.querySelector('mjx-container'));
|
|
1380
|
+
|
|
1381
|
+
if (needsRendering && window.MathJax && window.MathJax.typesetPromise) {
|
|
1382
|
+
try {
|
|
1383
|
+
await window.MathJax.typesetPromise(Array.from(mathBlocks));
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
console.warn('MathJax typesetting failed:', err);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Clone the preview panel to avoid modifying the actual DOM
|
|
1391
|
+
const clone = previewPanel.cloneNode(true);
|
|
1392
|
+
|
|
1393
|
+
// Process different fence types for rich copy
|
|
1394
|
+
try {
|
|
1395
|
+
// Phase 1: Process basic markdown elements with inline styles
|
|
1396
|
+
|
|
1397
|
+
// 1.1 Text formatting - add inline styles
|
|
1398
|
+
clone.querySelectorAll('strong, b').forEach(el => {
|
|
1399
|
+
el.style.fontWeight = 'bold';
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
clone.querySelectorAll('em, i').forEach(el => {
|
|
1403
|
+
el.style.fontStyle = 'italic';
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
clone.querySelectorAll('del, s, strike').forEach(el => {
|
|
1407
|
+
el.style.textDecoration = 'line-through';
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
clone.querySelectorAll('u').forEach(el => {
|
|
1411
|
+
el.style.textDecoration = 'underline';
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
clone.querySelectorAll('code:not(pre code)').forEach(el => {
|
|
1415
|
+
el.style.backgroundColor = '#f4f4f4';
|
|
1416
|
+
el.style.padding = '2px 4px';
|
|
1417
|
+
el.style.borderRadius = '3px';
|
|
1418
|
+
el.style.fontFamily = 'monospace';
|
|
1419
|
+
el.style.fontSize = '0.9em';
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
// 1.2 Block elements - add inline styles
|
|
1423
|
+
clone.querySelectorAll('h1').forEach(el => {
|
|
1424
|
+
el.style.fontSize = '2em';
|
|
1425
|
+
el.style.fontWeight = 'bold';
|
|
1426
|
+
el.style.marginTop = '0.67em';
|
|
1427
|
+
el.style.marginBottom = '0.67em';
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
clone.querySelectorAll('h2').forEach(el => {
|
|
1431
|
+
el.style.fontSize = '1.5em';
|
|
1432
|
+
el.style.fontWeight = 'bold';
|
|
1433
|
+
el.style.marginTop = '0.83em';
|
|
1434
|
+
el.style.marginBottom = '0.83em';
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
clone.querySelectorAll('h3').forEach(el => {
|
|
1438
|
+
el.style.fontSize = '1.17em';
|
|
1439
|
+
el.style.fontWeight = 'bold';
|
|
1440
|
+
el.style.marginTop = '1em';
|
|
1441
|
+
el.style.marginBottom = '1em';
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
clone.querySelectorAll('h4').forEach(el => {
|
|
1445
|
+
el.style.fontSize = '1em';
|
|
1446
|
+
el.style.fontWeight = 'bold';
|
|
1447
|
+
el.style.marginTop = '1.33em';
|
|
1448
|
+
el.style.marginBottom = '1.33em';
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
clone.querySelectorAll('h5').forEach(el => {
|
|
1452
|
+
el.style.fontSize = '0.83em';
|
|
1453
|
+
el.style.fontWeight = 'bold';
|
|
1454
|
+
el.style.marginTop = '1.67em';
|
|
1455
|
+
el.style.marginBottom = '1.67em';
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
clone.querySelectorAll('h6').forEach(el => {
|
|
1459
|
+
el.style.fontSize = '0.67em';
|
|
1460
|
+
el.style.fontWeight = 'bold';
|
|
1461
|
+
el.style.marginTop = '2.33em';
|
|
1462
|
+
el.style.marginBottom = '2.33em';
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
clone.querySelectorAll('blockquote').forEach(el => {
|
|
1466
|
+
el.style.borderLeft = '4px solid #ddd';
|
|
1467
|
+
el.style.marginLeft = '0';
|
|
1468
|
+
el.style.paddingLeft = '1em';
|
|
1469
|
+
el.style.color = '#666';
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
clone.querySelectorAll('hr').forEach(el => {
|
|
1473
|
+
el.style.border = 'none';
|
|
1474
|
+
el.style.borderTop = '1px solid #ccc';
|
|
1475
|
+
el.style.margin = '1em 0';
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
// 1.3 Tables - add inline styles
|
|
1479
|
+
clone.querySelectorAll('table').forEach(table => {
|
|
1480
|
+
table.style.borderCollapse = 'collapse';
|
|
1481
|
+
table.style.width = '100%';
|
|
1482
|
+
table.style.marginBottom = '1em';
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
clone.querySelectorAll('th').forEach(th => {
|
|
1486
|
+
th.style.border = '1px solid #ccc';
|
|
1487
|
+
th.style.padding = '8px';
|
|
1488
|
+
th.style.textAlign = 'left';
|
|
1489
|
+
th.style.backgroundColor = '#f0f0f0';
|
|
1490
|
+
th.style.fontWeight = 'bold';
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
clone.querySelectorAll('td').forEach(td => {
|
|
1494
|
+
td.style.border = '1px solid #ccc';
|
|
1495
|
+
td.style.padding = '8px';
|
|
1496
|
+
td.style.textAlign = 'left';
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
// 1.4 Links - add inline styles
|
|
1500
|
+
clone.querySelectorAll('a').forEach(a => {
|
|
1501
|
+
a.style.color = '#0066cc';
|
|
1502
|
+
a.style.textDecoration = 'underline';
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
// Process code blocks - wrap in table and add syntax highlighting colors
|
|
1506
|
+
clone.querySelectorAll('pre code').forEach(block => {
|
|
1507
|
+
const pre = block.parentElement;
|
|
1508
|
+
|
|
1509
|
+
// Add inline styles for syntax highlighting (GitHub theme colors)
|
|
1510
|
+
if (block.classList.contains('hljs')) {
|
|
1511
|
+
// Apply inline styles to all highlight.js elements
|
|
1512
|
+
block.querySelectorAll('.hljs-keyword').forEach(el => {
|
|
1513
|
+
el.style.color = '#d73a49';
|
|
1514
|
+
el.style.fontWeight = 'bold';
|
|
1515
|
+
});
|
|
1516
|
+
block.querySelectorAll('.hljs-string').forEach(el => {
|
|
1517
|
+
el.style.color = '#032f62';
|
|
1518
|
+
});
|
|
1519
|
+
block.querySelectorAll('.hljs-number').forEach(el => {
|
|
1520
|
+
el.style.color = '#005cc5';
|
|
1521
|
+
});
|
|
1522
|
+
block.querySelectorAll('.hljs-comment').forEach(el => {
|
|
1523
|
+
el.style.color = '#6a737d';
|
|
1524
|
+
el.style.fontStyle = 'italic';
|
|
1525
|
+
});
|
|
1526
|
+
block.querySelectorAll('.hljs-function').forEach(el => {
|
|
1527
|
+
el.style.color = '#6f42c1';
|
|
1528
|
+
});
|
|
1529
|
+
block.querySelectorAll('.hljs-class').forEach(el => {
|
|
1530
|
+
el.style.color = '#6f42c1';
|
|
1531
|
+
});
|
|
1532
|
+
block.querySelectorAll('.hljs-title').forEach(el => {
|
|
1533
|
+
el.style.color = '#6f42c1';
|
|
1534
|
+
});
|
|
1535
|
+
block.querySelectorAll('.hljs-built_in').forEach(el => {
|
|
1536
|
+
el.style.color = '#005cc5';
|
|
1537
|
+
});
|
|
1538
|
+
block.querySelectorAll('.hljs-literal').forEach(el => {
|
|
1539
|
+
el.style.color = '#005cc5';
|
|
1540
|
+
});
|
|
1541
|
+
block.querySelectorAll('.hljs-meta').forEach(el => {
|
|
1542
|
+
el.style.color = '#005cc5';
|
|
1543
|
+
});
|
|
1544
|
+
block.querySelectorAll('.hljs-attr').forEach(el => {
|
|
1545
|
+
el.style.color = '#22863a';
|
|
1546
|
+
});
|
|
1547
|
+
block.querySelectorAll('.hljs-variable').forEach(el => {
|
|
1548
|
+
el.style.color = '#e36209';
|
|
1549
|
+
});
|
|
1550
|
+
block.querySelectorAll('.hljs-regexp').forEach(el => {
|
|
1551
|
+
el.style.color = '#032f62';
|
|
1552
|
+
});
|
|
1553
|
+
block.querySelectorAll('.hljs-selector-class').forEach(el => {
|
|
1554
|
+
el.style.color = '#22863a';
|
|
1555
|
+
});
|
|
1556
|
+
block.querySelectorAll('.hljs-selector-id').forEach(el => {
|
|
1557
|
+
el.style.color = '#6f42c1';
|
|
1558
|
+
});
|
|
1559
|
+
block.querySelectorAll('.hljs-selector-tag').forEach(el => {
|
|
1560
|
+
el.style.color = '#22863a';
|
|
1561
|
+
});
|
|
1562
|
+
block.querySelectorAll('.hljs-tag').forEach(el => {
|
|
1563
|
+
el.style.color = '#22863a';
|
|
1564
|
+
});
|
|
1565
|
+
block.querySelectorAll('.hljs-name').forEach(el => {
|
|
1566
|
+
el.style.color = '#22863a';
|
|
1567
|
+
});
|
|
1568
|
+
block.querySelectorAll('.hljs-attribute').forEach(el => {
|
|
1569
|
+
el.style.color = '#6f42c1';
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const table = document.createElement('table');
|
|
1574
|
+
table.style.width = '100%';
|
|
1575
|
+
table.style.borderCollapse = 'collapse';
|
|
1576
|
+
table.style.border = 'none';
|
|
1577
|
+
table.style.marginBottom = '1em';
|
|
1578
|
+
|
|
1579
|
+
const tr = document.createElement('tr');
|
|
1580
|
+
const td = document.createElement('td');
|
|
1581
|
+
td.style.backgroundColor = '#f7f7f7';
|
|
1582
|
+
td.style.padding = '12px';
|
|
1583
|
+
td.style.fontFamily = 'Consolas, Monaco, "Courier New", monospace';
|
|
1584
|
+
td.style.fontSize = '14px';
|
|
1585
|
+
td.style.lineHeight = '1.4';
|
|
1586
|
+
td.style.whiteSpace = 'pre';
|
|
1587
|
+
td.style.overflowX = 'auto';
|
|
1588
|
+
td.style.border = '1px solid #ddd';
|
|
1589
|
+
td.style.borderRadius = '4px';
|
|
1590
|
+
|
|
1591
|
+
// Move the formatted code content with inline styles
|
|
1592
|
+
td.innerHTML = block.innerHTML;
|
|
1593
|
+
|
|
1594
|
+
tr.appendChild(td);
|
|
1595
|
+
table.appendChild(tr);
|
|
1596
|
+
|
|
1597
|
+
// Replace the pre element with the table
|
|
1598
|
+
pre.parentNode.replaceChild(table, pre);
|
|
1599
|
+
});
|
|
1600
|
+
|
|
1601
|
+
// Process images - convert to data URLs and ensure proper dimensions
|
|
1602
|
+
const images = clone.querySelectorAll('img');
|
|
1603
|
+
for (const img of images) {
|
|
1604
|
+
// Ensure image has dimensions for Google Docs compatibility
|
|
1605
|
+
if (!img.width && img.naturalWidth) {
|
|
1606
|
+
img.width = img.naturalWidth;
|
|
1607
|
+
}
|
|
1608
|
+
if (!img.height && img.naturalHeight) {
|
|
1609
|
+
img.height = img.naturalHeight;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// Set max dimensions to prevent huge images
|
|
1613
|
+
const maxWidth = 800;
|
|
1614
|
+
const maxHeight = 600;
|
|
1615
|
+
if (img.width > maxWidth || img.height > maxHeight) {
|
|
1616
|
+
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
|
|
1617
|
+
img.width = Math.round(img.width * scale);
|
|
1618
|
+
img.height = Math.round(img.height * scale);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Ensure width and height attributes are set
|
|
1622
|
+
if (img.width) {
|
|
1623
|
+
img.setAttribute('width', img.width.toString());
|
|
1624
|
+
img.style.width = img.width + 'px';
|
|
1625
|
+
}
|
|
1626
|
+
if (img.height) {
|
|
1627
|
+
img.setAttribute('height', img.height.toString());
|
|
1628
|
+
img.style.height = img.height + 'px';
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Add v:shapes for Word compatibility
|
|
1632
|
+
if (!img.getAttribute('v:shapes')) {
|
|
1633
|
+
img.setAttribute('v:shapes', 'image' + Math.random().toString(36).substr(2, 9));
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// Skip if already a data URL
|
|
1637
|
+
if (img.src && !img.src.startsWith('data:')) {
|
|
1638
|
+
try {
|
|
1639
|
+
// Try to convert to data URL
|
|
1640
|
+
const response = await fetch(img.src);
|
|
1641
|
+
const blob = await response.blob();
|
|
1642
|
+
|
|
1643
|
+
// Check if image is too large (Google Docs has limits)
|
|
1644
|
+
const maxSize = 2 * 1024 * 1024; // 2MB limit for inline images
|
|
1645
|
+
if (blob.size > maxSize) {
|
|
1646
|
+
console.warn('Image too large for inline data URL:', img.src, 'Size:', blob.size);
|
|
1647
|
+
// For large images, we might want to resize or keep the URL
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const dataUrl = await new Promise(resolve => {
|
|
1652
|
+
const reader = new FileReader();
|
|
1653
|
+
reader.onloadend = () => resolve(reader.result);
|
|
1654
|
+
reader.readAsDataURL(blob);
|
|
1655
|
+
});
|
|
1656
|
+
img.src = dataUrl;
|
|
1657
|
+
} catch (err) {
|
|
1658
|
+
console.warn('Failed to convert image to data URL:', img.src, err);
|
|
1659
|
+
// Keep original src if conversion fails
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Phase 2: Process fence block types
|
|
1665
|
+
// 1. Process STL 3D models - convert canvas to image or placeholder
|
|
1666
|
+
const stlContainers = clone.querySelectorAll('.qde-stl-container');
|
|
1667
|
+
for (const container of stlContainers) {
|
|
1668
|
+
try {
|
|
1669
|
+
// Find the corresponding original container to get the canvas
|
|
1670
|
+
const containerId = container.dataset.stlId;
|
|
1671
|
+
const originalContainer = previewPanel.querySelector(`.qde-stl-container[data-stl-id="${containerId}"]`);
|
|
1672
|
+
|
|
1673
|
+
if (originalContainer) {
|
|
1674
|
+
// Look for canvas element in the original container (Three.js WebGL canvas)
|
|
1675
|
+
const canvas = originalContainer.querySelector('canvas');
|
|
1676
|
+
if (canvas && canvas.width > 0 && canvas.height > 0) {
|
|
1677
|
+
try {
|
|
1678
|
+
// Get Three.js references stored on the container (like squibview)
|
|
1679
|
+
const renderer = originalContainer._threeRenderer;
|
|
1680
|
+
const scene = originalContainer._threeScene;
|
|
1681
|
+
const camera = originalContainer._threeCamera;
|
|
1682
|
+
|
|
1683
|
+
// If we have access to the Three.js objects, force render the scene
|
|
1684
|
+
if (renderer && scene && camera) {
|
|
1685
|
+
renderer.render(scene, camera);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Try to capture the canvas as an image
|
|
1689
|
+
const dataUrl = canvas.toDataURL('image/png', 1.0);
|
|
1690
|
+
const img = document.createElement('img');
|
|
1691
|
+
img.src = dataUrl;
|
|
1692
|
+
|
|
1693
|
+
// Use canvas dimensions for the image
|
|
1694
|
+
const imgWidth = canvas.width / 2; // Divide by scale factor (2x for retina)
|
|
1695
|
+
const imgHeight = canvas.height / 2;
|
|
1696
|
+
|
|
1697
|
+
// Set both HTML attributes and CSS properties for maximum compatibility
|
|
1698
|
+
img.width = imgWidth;
|
|
1699
|
+
img.height = imgHeight;
|
|
1700
|
+
img.setAttribute('width', imgWidth.toString());
|
|
1701
|
+
img.setAttribute('height', imgHeight.toString());
|
|
1702
|
+
img.style.width = imgWidth + 'px';
|
|
1703
|
+
img.style.height = imgHeight + 'px';
|
|
1704
|
+
img.style.maxWidth = 'none';
|
|
1705
|
+
img.style.maxHeight = 'none';
|
|
1706
|
+
img.style.border = '1px solid #ddd';
|
|
1707
|
+
img.style.borderRadius = '4px';
|
|
1708
|
+
img.style.margin = '0.5em 0';
|
|
1709
|
+
img.setAttribute('v:shapes', 'image' + Math.random().toString(36).substr(2, 9));
|
|
1710
|
+
img.alt = 'STL 3D Model';
|
|
1711
|
+
|
|
1712
|
+
container.parentNode.replaceChild(img, container);
|
|
1713
|
+
continue;
|
|
1714
|
+
} catch (canvasErr) {
|
|
1715
|
+
console.warn('Failed to convert STL canvas to image (likely WebGL context issue):', canvasErr);
|
|
1716
|
+
}
|
|
1717
|
+
} else {
|
|
1718
|
+
console.warn('No valid canvas found in STL container');
|
|
1719
|
+
}
|
|
1720
|
+
} else {
|
|
1721
|
+
console.warn('Could not find original STL container');
|
|
1722
|
+
}
|
|
1723
|
+
} catch (err) {
|
|
1724
|
+
console.error('Error processing STL container for copy:', err);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// Fallback to placeholder if canvas conversion fails
|
|
1728
|
+
const placeholder = document.createElement('div');
|
|
1729
|
+
placeholder.style.cssText = 'padding: 12px; background-color: #f0f0f0; border: 1px solid #ccc; text-align: center; margin: 0.5em 0; border-radius: 4px;';
|
|
1730
|
+
placeholder.textContent = '[STL 3D Model - Interactive content not available in copy]';
|
|
1731
|
+
container.parentNode.replaceChild(placeholder, container);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// 2. Process Mermaid diagrams - convert SVG to PNG
|
|
1735
|
+
const mermaidContainers = clone.querySelectorAll('.mermaid');
|
|
1736
|
+
for (const container of mermaidContainers) {
|
|
1737
|
+
const svg = container.querySelector('svg');
|
|
1738
|
+
if (svg) {
|
|
1739
|
+
try {
|
|
1740
|
+
const pngBlob = await svgToPng(svg);
|
|
1741
|
+
const dataUrl = await new Promise(resolve => {
|
|
1742
|
+
const reader = new FileReader();
|
|
1743
|
+
reader.onloadend = () => resolve(reader.result);
|
|
1744
|
+
reader.readAsDataURL(pngBlob);
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
const img = document.createElement('img');
|
|
1748
|
+
img.src = dataUrl;
|
|
1749
|
+
|
|
1750
|
+
// Use the exact same dimension calculation logic as svgToPng (like squibview)
|
|
1751
|
+
const isMermaidSvg = svg.closest('.mermaid') || svg.classList.contains('mermaid');
|
|
1752
|
+
const hasExplicitDimensions = svg.getAttribute('width') && svg.getAttribute('height');
|
|
1753
|
+
|
|
1754
|
+
let imgWidth, imgHeight;
|
|
1755
|
+
|
|
1756
|
+
if (isMermaidSvg || !hasExplicitDimensions) {
|
|
1757
|
+
// For Mermaid or other generated SVGs, prioritize computed dimensions
|
|
1758
|
+
imgWidth = svg.clientWidth ||
|
|
1759
|
+
(svg.viewBox && svg.viewBox.baseVal.width) ||
|
|
1760
|
+
parseFloat(svg.getAttribute('width')) || 400;
|
|
1761
|
+
imgHeight = svg.clientHeight ||
|
|
1762
|
+
(svg.viewBox && svg.viewBox.baseVal.height) ||
|
|
1763
|
+
parseFloat(svg.getAttribute('height')) || 300;
|
|
1764
|
+
} else {
|
|
1765
|
+
// For explicit SVGs (like fenced SVG blocks), prioritize explicit attributes
|
|
1766
|
+
imgWidth = parseFloat(svg.getAttribute('width')) ||
|
|
1767
|
+
(svg.viewBox && svg.viewBox.baseVal.width) ||
|
|
1768
|
+
svg.clientWidth || 400;
|
|
1769
|
+
imgHeight = parseFloat(svg.getAttribute('height')) ||
|
|
1770
|
+
(svg.viewBox && svg.viewBox.baseVal.height) ||
|
|
1771
|
+
svg.clientHeight || 300;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Set both HTML attributes and CSS properties for maximum compatibility (like squibview)
|
|
1775
|
+
img.width = imgWidth;
|
|
1776
|
+
img.height = imgHeight;
|
|
1777
|
+
img.setAttribute('width', imgWidth.toString());
|
|
1778
|
+
img.setAttribute('height', imgHeight.toString());
|
|
1779
|
+
img.style.width = imgWidth + 'px';
|
|
1780
|
+
img.style.height = imgHeight + 'px';
|
|
1781
|
+
img.style.maxWidth = 'none'; // Prevent CSS from constraining the image
|
|
1782
|
+
img.style.maxHeight = 'none';
|
|
1783
|
+
img.setAttribute('v:shapes', 'image' + Math.random().toString(36).substr(2, 9));
|
|
1784
|
+
img.alt = 'Mermaid Diagram';
|
|
1785
|
+
|
|
1786
|
+
container.parentNode.replaceChild(img, container);
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
console.warn('Failed to convert Mermaid diagram:', err);
|
|
1789
|
+
// Fallback: leave as SVG
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// 3. Process Chart.js charts - convert canvas to image
|
|
1795
|
+
const chartContainers = clone.querySelectorAll('.qde-chart-container');
|
|
1796
|
+
for (const container of chartContainers) {
|
|
1797
|
+
try {
|
|
1798
|
+
const containerId = container.dataset.chartId;
|
|
1799
|
+
const originalContainer = previewPanel.querySelector(`.qde-chart-container[data-chart-id="${containerId}"]`);
|
|
1800
|
+
|
|
1801
|
+
if (originalContainer) {
|
|
1802
|
+
const canvas = originalContainer.querySelector('canvas');
|
|
1803
|
+
if (canvas && canvas.width > 0 && canvas.height > 0) {
|
|
1804
|
+
try {
|
|
1805
|
+
const dataUrl = canvas.toDataURL('image/png', 1.0);
|
|
1806
|
+
const img = document.createElement('img');
|
|
1807
|
+
img.src = dataUrl;
|
|
1808
|
+
|
|
1809
|
+
// Use canvas dimensions for the image
|
|
1810
|
+
const imgWidth = canvas.width;
|
|
1811
|
+
const imgHeight = canvas.height;
|
|
1812
|
+
|
|
1813
|
+
// Set both HTML attributes and CSS properties for maximum compatibility
|
|
1814
|
+
img.width = imgWidth;
|
|
1815
|
+
img.height = imgHeight;
|
|
1816
|
+
img.setAttribute('width', imgWidth.toString());
|
|
1817
|
+
img.setAttribute('height', imgHeight.toString());
|
|
1818
|
+
img.style.width = imgWidth + 'px';
|
|
1819
|
+
img.style.height = imgHeight + 'px';
|
|
1820
|
+
img.style.maxWidth = 'none';
|
|
1821
|
+
img.style.maxHeight = 'none';
|
|
1822
|
+
img.style.margin = '0.5em 0';
|
|
1823
|
+
img.setAttribute('v:shapes', 'image' + Math.random().toString(36).substr(2, 9));
|
|
1824
|
+
img.alt = 'Chart';
|
|
1825
|
+
|
|
1826
|
+
container.parentNode.replaceChild(img, container);
|
|
1827
|
+
continue;
|
|
1828
|
+
} catch (canvasErr) {
|
|
1829
|
+
console.warn('Failed to convert chart canvas to image:', canvasErr);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
console.warn('Error processing chart container:', err);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// Fallback to placeholder
|
|
1838
|
+
const placeholder = document.createElement('div');
|
|
1839
|
+
placeholder.style.cssText = 'padding: 12px; background-color: #f0f0f0; border: 1px solid #ccc; text-align: center; margin: 0.5em 0; border-radius: 4px;';
|
|
1840
|
+
placeholder.textContent = '[Chart - Interactive content not available in copy]';
|
|
1841
|
+
container.parentNode.replaceChild(placeholder, container);
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// 4. Process SVG fenced blocks - convert to PNG
|
|
1845
|
+
const svgContainers = clone.querySelectorAll('.qde-svg-container svg');
|
|
1846
|
+
for (const svg of svgContainers) {
|
|
1847
|
+
try {
|
|
1848
|
+
const pngBlob = await svgToPng(svg);
|
|
1849
|
+
const dataUrl = await new Promise(resolve => {
|
|
1850
|
+
const reader = new FileReader();
|
|
1851
|
+
reader.onloadend = () => resolve(reader.result);
|
|
1852
|
+
reader.readAsDataURL(pngBlob);
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
const img = document.createElement('img');
|
|
1856
|
+
img.src = dataUrl;
|
|
1857
|
+
|
|
1858
|
+
// Calculate dimensions for the SVG
|
|
1859
|
+
const hasExplicitDimensions = svg.getAttribute('width') && svg.getAttribute('height');
|
|
1860
|
+
|
|
1861
|
+
let imgWidth, imgHeight;
|
|
1862
|
+
|
|
1863
|
+
if (hasExplicitDimensions) {
|
|
1864
|
+
// For explicit SVGs (like fenced SVG blocks), prioritize explicit attributes
|
|
1865
|
+
imgWidth = parseFloat(svg.getAttribute('width')) ||
|
|
1866
|
+
(svg.viewBox && svg.viewBox.baseVal.width) ||
|
|
1867
|
+
svg.clientWidth || 400;
|
|
1868
|
+
imgHeight = parseFloat(svg.getAttribute('height')) ||
|
|
1869
|
+
(svg.viewBox && svg.viewBox.baseVal.height) ||
|
|
1870
|
+
svg.clientHeight || 300;
|
|
1871
|
+
} else {
|
|
1872
|
+
// For generated SVGs, prioritize computed dimensions
|
|
1873
|
+
imgWidth = svg.clientWidth ||
|
|
1874
|
+
(svg.viewBox && svg.viewBox.baseVal.width) ||
|
|
1875
|
+
parseFloat(svg.getAttribute('width')) || 400;
|
|
1876
|
+
imgHeight = svg.clientHeight ||
|
|
1877
|
+
(svg.viewBox && svg.viewBox.baseVal.height) ||
|
|
1878
|
+
parseFloat(svg.getAttribute('height')) || 300;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// Set both HTML attributes and CSS properties for maximum compatibility
|
|
1882
|
+
img.width = imgWidth;
|
|
1883
|
+
img.height = imgHeight;
|
|
1884
|
+
img.setAttribute('width', imgWidth.toString());
|
|
1885
|
+
img.setAttribute('height', imgHeight.toString());
|
|
1886
|
+
img.style.width = imgWidth + 'px';
|
|
1887
|
+
img.style.height = imgHeight + 'px';
|
|
1888
|
+
img.style.maxWidth = 'none'; // Prevent CSS from constraining the image
|
|
1889
|
+
img.style.maxHeight = 'none';
|
|
1890
|
+
img.setAttribute('v:shapes', 'image' + Math.random().toString(36).substr(2, 9));
|
|
1891
|
+
img.alt = 'SVG Image';
|
|
1892
|
+
|
|
1893
|
+
svg.parentNode.replaceChild(img, svg);
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
console.warn('Failed to convert SVG to image:', err);
|
|
1896
|
+
// Leave as SVG if conversion fails
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// 5. Process Math equations - convert to PNG images (exactly like SquibView)
|
|
1901
|
+
const mathElements = Array.from(clone.querySelectorAll('.math-display'));
|
|
1902
|
+
|
|
1903
|
+
if (mathElements.length > 0) {
|
|
1904
|
+
for (const mathEl of mathElements) {
|
|
1905
|
+
try {
|
|
1906
|
+
// Find SVG inside the math element (MathJax creates it)
|
|
1907
|
+
const svg = mathEl.querySelector('svg');
|
|
1908
|
+
if (!svg) {
|
|
1909
|
+
console.warn('No SVG found in math element, skipping');
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Convert SVG to PNG data URL (exactly like SquibView)
|
|
1914
|
+
const serializer = new XMLSerializer();
|
|
1915
|
+
const svgStr = serializer.serializeToString(svg);
|
|
1916
|
+
const svgBlob = new Blob([svgStr], { type: "image/svg+xml;charset=utf-8" });
|
|
1917
|
+
const url = URL.createObjectURL(svgBlob);
|
|
1918
|
+
|
|
1919
|
+
const img = new Image();
|
|
1920
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
1921
|
+
img.onload = function () {
|
|
1922
|
+
const canvas = document.createElement('canvas');
|
|
1923
|
+
|
|
1924
|
+
// Determine SVG dimensions robustly (exactly like SquibView)
|
|
1925
|
+
let width, height;
|
|
1926
|
+
try {
|
|
1927
|
+
// First try baseVal.value (works for absolute units)
|
|
1928
|
+
width = svg.width.baseVal.value;
|
|
1929
|
+
height = svg.height.baseVal.value;
|
|
1930
|
+
} catch (e) {
|
|
1931
|
+
// Fallback for relative units - use viewBox or rendered size
|
|
1932
|
+
if (svg.viewBox && svg.viewBox.baseVal) {
|
|
1933
|
+
width = svg.viewBox.baseVal.width;
|
|
1934
|
+
height = svg.viewBox.baseVal.height;
|
|
1935
|
+
} else {
|
|
1936
|
+
// Use the natural size of the loaded image
|
|
1937
|
+
width = img.naturalWidth || img.width || 200;
|
|
1938
|
+
height = img.naturalHeight || img.height || 50;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// Scale down for much smaller paste sizes
|
|
1943
|
+
const targetMaxWidth = 150; // Further reduced
|
|
1944
|
+
const targetMaxHeight = 45; // Further reduced
|
|
1945
|
+
|
|
1946
|
+
// Apply aggressive downsizing for MathJax SVGs
|
|
1947
|
+
let scaleFactor = 0.04; // Further reduced for smaller output
|
|
1948
|
+
|
|
1949
|
+
let scaledWidth = width * scaleFactor;
|
|
1950
|
+
let scaledHeight = height * scaleFactor;
|
|
1951
|
+
|
|
1952
|
+
// If still too large after base scaling, scale down further
|
|
1953
|
+
if (scaledWidth > targetMaxWidth || scaledHeight > targetMaxHeight) {
|
|
1954
|
+
const scaleX = targetMaxWidth / scaledWidth;
|
|
1955
|
+
const scaleY = targetMaxHeight / scaledHeight;
|
|
1956
|
+
scaleFactor *= Math.min(scaleX, scaleY);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
width *= scaleFactor;
|
|
1960
|
+
height *= scaleFactor;
|
|
1961
|
+
|
|
1962
|
+
// Use higher DPR for crisp rendering at smaller sizes
|
|
1963
|
+
const dpr = 2; // Fixed 2x for consistent quality
|
|
1964
|
+
canvas.width = width * dpr;
|
|
1965
|
+
canvas.height = height * dpr;
|
|
1966
|
+
canvas.style.width = width + 'px';
|
|
1967
|
+
canvas.style.height = height + 'px';
|
|
1968
|
+
|
|
1969
|
+
const ctx = canvas.getContext('2d');
|
|
1970
|
+
ctx.scale(dpr, dpr);
|
|
1971
|
+
|
|
1972
|
+
// White background
|
|
1973
|
+
ctx.fillStyle = "#FFFFFF";
|
|
1974
|
+
ctx.fillRect(0, 0, width, height);
|
|
1975
|
+
|
|
1976
|
+
// Draw the SVG image at logical size
|
|
1977
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
1978
|
+
|
|
1979
|
+
// Clean up URL
|
|
1980
|
+
URL.revokeObjectURL(url);
|
|
1981
|
+
|
|
1982
|
+
// Return data URL
|
|
1983
|
+
resolve(canvas.toDataURL('image/png'));
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1986
|
+
img.onerror = () => {
|
|
1987
|
+
URL.revokeObjectURL(url);
|
|
1988
|
+
reject(new Error('Failed to load SVG image'));
|
|
1989
|
+
};
|
|
1990
|
+
|
|
1991
|
+
img.src = url;
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1994
|
+
// Replace math element with img tag containing the PNG data URL
|
|
1995
|
+
const imgElement = document.createElement('img');
|
|
1996
|
+
imgElement.src = dataUrl;
|
|
1997
|
+
|
|
1998
|
+
// Extract dimensions from the data URL canvas
|
|
1999
|
+
const img2 = new Image();
|
|
2000
|
+
img2.src = dataUrl;
|
|
2001
|
+
await new Promise((resolve) => {
|
|
2002
|
+
img2.onload = resolve;
|
|
2003
|
+
img2.onerror = resolve;
|
|
2004
|
+
setTimeout(resolve, 100); // Timeout fallback
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
// Set explicit dimensions (accounting for DPR)
|
|
2008
|
+
const displayWidth = img2.naturalWidth / 2; // Divide by DPR
|
|
2009
|
+
const displayHeight = img2.naturalHeight / 2;
|
|
2010
|
+
|
|
2011
|
+
imgElement.width = displayWidth;
|
|
2012
|
+
imgElement.height = displayHeight;
|
|
2013
|
+
imgElement.style.cssText = `display:inline-block;margin:0.5em 0;width:${displayWidth}px;height:${displayHeight}px;vertical-align:middle;`;
|
|
2014
|
+
imgElement.alt = 'Math equation';
|
|
2015
|
+
|
|
2016
|
+
mathEl.parentNode.replaceChild(imgElement, mathEl);
|
|
2017
|
+
} catch (error) {
|
|
2018
|
+
console.error('Failed to convert math element to image:', error);
|
|
2019
|
+
// Keep the original element if conversion fails
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// 2. Process GeoJSON maps - convert to static images (following Gem's guide)
|
|
2025
|
+
const geojsonContainers = clone.querySelectorAll('.geojson-container');
|
|
2026
|
+
if (geojsonContainers.length > 0) {
|
|
2027
|
+
|
|
2028
|
+
for (const clonedContainer of geojsonContainers) {
|
|
2029
|
+
try {
|
|
2030
|
+
// Find the corresponding live container by matching data-original-source
|
|
2031
|
+
const originalSource = clonedContainer.getAttribute('data-original-source');
|
|
2032
|
+
if (!originalSource) {
|
|
2033
|
+
console.warn('No original source found for GeoJSON container');
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Find live container with same source
|
|
2038
|
+
let liveContainer = null;
|
|
2039
|
+
const allLiveContainers = previewPanel.querySelectorAll('.geojson-container');
|
|
2040
|
+
for (const candidate of allLiveContainers) {
|
|
2041
|
+
if (candidate.getAttribute('data-original-source') === originalSource) {
|
|
2042
|
+
liveContainer = candidate;
|
|
2043
|
+
break;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
if (!liveContainer) {
|
|
2048
|
+
console.warn('Could not find live GeoJSON container');
|
|
2049
|
+
const placeholder = document.createElement('div');
|
|
2050
|
+
placeholder.style.cssText = 'padding: 12px; background-color: #f0f0f0; border: 1px solid #ccc; text-align: center; margin: 0.5em 0; border-radius: 4px;';
|
|
2051
|
+
placeholder.textContent = '[GeoJSON Map - Interactive content not available in copy]';
|
|
2052
|
+
clonedContainer.parentNode.replaceChild(placeholder, clonedContainer);
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Check if map is ready
|
|
2057
|
+
const map = liveContainer._map;
|
|
2058
|
+
if (!map) {
|
|
2059
|
+
console.warn('Map not initialized yet');
|
|
2060
|
+
const placeholder = document.createElement('div');
|
|
2061
|
+
placeholder.style.cssText = 'padding: 12px; background-color: #f0f0f0; border: 1px solid #ccc; text-align: center; margin: 0.5em 0; border-radius: 4px;';
|
|
2062
|
+
placeholder.textContent = '[GeoJSON Map - Still loading]';
|
|
2063
|
+
clonedContainer.parentNode.replaceChild(placeholder, clonedContainer);
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// Rasterize the map to PNG
|
|
2068
|
+
const dataUrl = await rasterizeGeoJSONMap(liveContainer);
|
|
2069
|
+
|
|
2070
|
+
if (dataUrl) {
|
|
2071
|
+
// Replace with image
|
|
2072
|
+
const img = document.createElement('img');
|
|
2073
|
+
img.src = dataUrl;
|
|
2074
|
+
img.style.cssText = 'width: 100%; height: 300px; border: 1px solid #ddd; border-radius: 4px; margin: 0.5em 0;';
|
|
2075
|
+
img.alt = 'GeoJSON Map';
|
|
2076
|
+
clonedContainer.parentNode.replaceChild(img, clonedContainer);
|
|
2077
|
+
} else {
|
|
2078
|
+
// Fallback placeholder
|
|
2079
|
+
const placeholder = document.createElement('div');
|
|
2080
|
+
placeholder.style.cssText = 'padding: 12px; background-color: #f0f0f0; border: 1px solid #ccc; text-align: center; margin: 0.5em 0; border-radius: 4px;';
|
|
2081
|
+
placeholder.textContent = '[GeoJSON Map - Interactive content not available in copy]';
|
|
2082
|
+
clonedContainer.parentNode.replaceChild(placeholder, clonedContainer);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
} catch (error) {
|
|
2086
|
+
console.error('Failed to process GeoJSON container:', error);
|
|
2087
|
+
// Replace with placeholder
|
|
2088
|
+
const placeholder = document.createElement('div');
|
|
2089
|
+
placeholder.style.cssText = 'padding: 12px; background-color: #f0f0f0; border: 1px solid #ccc; text-align: center; margin: 0.5em 0; border-radius: 4px;';
|
|
2090
|
+
placeholder.textContent = '[GeoJSON Map - Interactive content not available in copy]';
|
|
2091
|
+
clonedContainer.parentNode.replaceChild(placeholder, clonedContainer);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
|
|
2097
|
+
|
|
2098
|
+
// 6. Process GeoJSON/Leaflet maps - capture as single image (compose tiles + overlays)
|
|
2099
|
+
const mapContainers = clone.querySelectorAll('[data-qd-lang="geojson"]');
|
|
2100
|
+
for (const container of mapContainers) {
|
|
2101
|
+
try {
|
|
2102
|
+
const containerId = container.id;
|
|
2103
|
+
const originalContainer = containerId ? previewPanel.querySelector(`#${containerId}`) : null;
|
|
2104
|
+
if (!originalContainer) continue;
|
|
2105
|
+
const leafletContainer = originalContainer.querySelector('.leaflet-container');
|
|
2106
|
+
if (!leafletContainer) continue;
|
|
2107
|
+
|
|
2108
|
+
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
2109
|
+
const width = leafletContainer.clientWidth || 600;
|
|
2110
|
+
const height = leafletContainer.clientHeight || 400;
|
|
2111
|
+
const canvas = document.createElement('canvas');
|
|
2112
|
+
canvas.width = Math.round(width * dpr);
|
|
2113
|
+
canvas.height = Math.round(height * dpr);
|
|
2114
|
+
const ctx = canvas.getContext('2d');
|
|
2115
|
+
ctx.scale(dpr, dpr);
|
|
2116
|
+
ctx.fillStyle = '#FFFFFF';
|
|
2117
|
+
ctx.fillRect(0, 0, width, height);
|
|
2118
|
+
|
|
2119
|
+
const leafRect = leafletContainer.getBoundingClientRect();
|
|
2120
|
+
|
|
2121
|
+
// Draw tiles (snap to integer pixels to avoid seams)
|
|
2122
|
+
const tiles = Array.from(leafletContainer.querySelectorAll('img.leaflet-tile'));
|
|
2123
|
+
for (const tile of tiles) {
|
|
2124
|
+
try {
|
|
2125
|
+
const r = tile.getBoundingClientRect();
|
|
2126
|
+
const x = Math.round(r.left - leafRect.left);
|
|
2127
|
+
const y = Math.round(r.top - leafRect.top);
|
|
2128
|
+
const w = Math.round(r.width);
|
|
2129
|
+
const h = Math.round(r.height);
|
|
2130
|
+
const overlaps = !(r.right <= leafRect.left || r.left >= leafRect.right || r.bottom <= leafRect.top || r.top >= leafRect.bottom);
|
|
2131
|
+
const style = window.getComputedStyle(tile);
|
|
2132
|
+
if (w > 0 && h > 0 && overlaps && style.display !== 'none' && style.visibility !== 'hidden') {
|
|
2133
|
+
ctx.drawImage(tile, x, y, w + 1, h + 1);
|
|
2134
|
+
}
|
|
2135
|
+
} catch (e) {
|
|
2136
|
+
console.warn('Failed to draw tile:', e);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// Draw SVG overlays (paths, markers)
|
|
2141
|
+
const overlaySvgs = originalContainer.querySelectorAll('.leaflet-overlay-pane svg');
|
|
2142
|
+
for (const svg of overlaySvgs) {
|
|
2143
|
+
try {
|
|
2144
|
+
const svgStr = new XMLSerializer().serializeToString(svg);
|
|
2145
|
+
const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr);
|
|
2146
|
+
const img = new Image();
|
|
2147
|
+
await new Promise((resolve) => { img.onload = resolve; img.onerror = resolve; img.src = dataUrl; });
|
|
2148
|
+
const r = svg.getBoundingClientRect();
|
|
2149
|
+
const x = Math.round(r.left - leafRect.left);
|
|
2150
|
+
const y = Math.round(r.top - leafRect.top);
|
|
2151
|
+
const w = Math.round(r.width);
|
|
2152
|
+
const h = Math.round(r.height);
|
|
2153
|
+
const overlaps = !(r.right <= leafRect.left || r.left >= leafRect.right || r.bottom <= leafRect.top || r.top >= leafRect.bottom);
|
|
2154
|
+
if (w > 0 && h > 0 && overlaps) ctx.drawImage(img, x, y, w, h);
|
|
2155
|
+
} catch (e) {
|
|
2156
|
+
console.warn('Failed to draw overlay SVG:', e);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// Draw marker icons (PNG/SVG img elements)
|
|
2161
|
+
const markerIcons = originalContainer.querySelectorAll('.leaflet-marker-pane img.leaflet-marker-icon');
|
|
2162
|
+
for (const icon of markerIcons) {
|
|
2163
|
+
try {
|
|
2164
|
+
const r = icon.getBoundingClientRect();
|
|
2165
|
+
const x = Math.round(r.left - leafRect.left);
|
|
2166
|
+
const y = Math.round(r.top - leafRect.top);
|
|
2167
|
+
const w = Math.round(r.width);
|
|
2168
|
+
const h = Math.round(r.height);
|
|
2169
|
+
const overlaps = !(r.right <= leafRect.left || r.left >= leafRect.right || r.bottom <= leafRect.top || r.top >= leafRect.bottom);
|
|
2170
|
+
const style = window.getComputedStyle(icon);
|
|
2171
|
+
if (w > 0 && h > 0 && overlaps && style.display !== 'none' && style.visibility !== 'hidden') {
|
|
2172
|
+
ctx.drawImage(icon, x, y, w, h);
|
|
2173
|
+
}
|
|
2174
|
+
} catch (e) {
|
|
2175
|
+
console.warn('Failed to draw marker icon:', e);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
// Try to produce a data URL (may fail if canvas tainted by CORS tiles)
|
|
2180
|
+
let mapDataUrl = '';
|
|
2181
|
+
try {
|
|
2182
|
+
mapDataUrl = canvas.toDataURL('image/png', 1.0);
|
|
2183
|
+
} catch (e) {
|
|
2184
|
+
console.warn('Map canvas tainted; falling back to placeholder');
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
const img = document.createElement('img');
|
|
2188
|
+
if (mapDataUrl) {
|
|
2189
|
+
img.src = mapDataUrl;
|
|
2190
|
+
img.width = width;
|
|
2191
|
+
img.height = height;
|
|
2192
|
+
img.setAttribute('width', String(width));
|
|
2193
|
+
img.setAttribute('height', String(height));
|
|
2194
|
+
img.style.width = width + 'px';
|
|
2195
|
+
img.style.height = height + 'px';
|
|
2196
|
+
img.style.display = 'block';
|
|
2197
|
+
img.style.border = '1px solid #ddd';
|
|
2198
|
+
img.setAttribute('v:shapes', 'image' + Math.random().toString(36).substr(2, 9));
|
|
2199
|
+
img.alt = 'Map';
|
|
2200
|
+
} else {
|
|
2201
|
+
img.alt = 'Map';
|
|
2202
|
+
img.style.width = width + 'px';
|
|
2203
|
+
img.style.height = height + 'px';
|
|
2204
|
+
img.style.border = '1px solid #ddd';
|
|
2205
|
+
img.style.backgroundColor = '#f0f0f0';
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
container.parentNode.replaceChild(img, container);
|
|
2209
|
+
} catch (err) {
|
|
2210
|
+
console.warn('Failed to process map container:', err);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// 7. Process HTML fence blocks - render the HTML content and process images
|
|
2215
|
+
const htmlContainers = clone.querySelectorAll('.qde-html-container');
|
|
2216
|
+
for (const container of htmlContainers) {
|
|
2217
|
+
try {
|
|
2218
|
+
// Get the original source HTML
|
|
2219
|
+
const source = container.getAttribute('data-qd-source');
|
|
2220
|
+
|
|
2221
|
+
// Check if there's a pre element (fallback display) or actual HTML content
|
|
2222
|
+
const pre = container.querySelector('pre');
|
|
2223
|
+
|
|
2224
|
+
if (source) {
|
|
2225
|
+
// Parse the source HTML
|
|
2226
|
+
const tempDiv = document.createElement('div');
|
|
2227
|
+
tempDiv.innerHTML = source;
|
|
2228
|
+
|
|
2229
|
+
// Process all images in the HTML block
|
|
2230
|
+
const htmlImages = tempDiv.querySelectorAll('img');
|
|
2231
|
+
for (const img of htmlImages) {
|
|
2232
|
+
// Preserve original dimensions from HTML attributes
|
|
2233
|
+
const widthAttr = img.getAttribute('width');
|
|
2234
|
+
const heightAttr = img.getAttribute('height');
|
|
2235
|
+
|
|
2236
|
+
if (widthAttr) {
|
|
2237
|
+
img.width = parseInt(widthAttr);
|
|
2238
|
+
img.style.width = widthAttr.includes('%') ? widthAttr : `${img.width}px`;
|
|
2239
|
+
}
|
|
2240
|
+
if (heightAttr) {
|
|
2241
|
+
img.height = parseInt(heightAttr);
|
|
2242
|
+
img.style.height = heightAttr.includes('%') ? heightAttr : `${img.height}px`;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// Convert to data URL using canvas (like squibview)
|
|
2246
|
+
if (img.src && !img.src.startsWith('data:')) {
|
|
2247
|
+
try {
|
|
2248
|
+
// Use canvas to convert image to data URL (avoids CORS issues)
|
|
2249
|
+
const canvas = document.createElement('canvas');
|
|
2250
|
+
const ctx = canvas.getContext('2d');
|
|
2251
|
+
|
|
2252
|
+
// Create new image and wait for it to load
|
|
2253
|
+
const tempImg = new Image();
|
|
2254
|
+
tempImg.crossOrigin = 'anonymous';
|
|
2255
|
+
|
|
2256
|
+
await new Promise((resolve, reject) => {
|
|
2257
|
+
tempImg.onload = function() {
|
|
2258
|
+
|
|
2259
|
+
// Calculate dimensions preserving aspect ratio
|
|
2260
|
+
let displayWidth = 0;
|
|
2261
|
+
let displayHeight = 0;
|
|
2262
|
+
|
|
2263
|
+
// Use the width specified in HTML (e.g. width="250")
|
|
2264
|
+
if (widthAttr && !widthAttr.includes('%')) {
|
|
2265
|
+
displayWidth = parseInt(widthAttr);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Use the height if specified
|
|
2269
|
+
if (heightAttr && !heightAttr.includes('%')) {
|
|
2270
|
+
displayHeight = parseInt(heightAttr);
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
// If only width is specified, calculate height based on aspect ratio
|
|
2275
|
+
if (displayWidth > 0 && displayHeight === 0) {
|
|
2276
|
+
if (tempImg.naturalWidth > 0) {
|
|
2277
|
+
const aspectRatio = tempImg.naturalHeight / tempImg.naturalWidth;
|
|
2278
|
+
displayHeight = Math.round(displayWidth * aspectRatio);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
// If only height is specified, calculate width based on aspect ratio
|
|
2282
|
+
else if (displayHeight > 0 && displayWidth === 0) {
|
|
2283
|
+
if (tempImg.naturalHeight > 0) {
|
|
2284
|
+
const aspectRatio = tempImg.naturalWidth / tempImg.naturalHeight;
|
|
2285
|
+
displayWidth = Math.round(displayHeight * aspectRatio);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
// If neither specified, use natural dimensions
|
|
2289
|
+
else if (displayWidth === 0 && displayHeight === 0) {
|
|
2290
|
+
displayWidth = tempImg.naturalWidth || 250;
|
|
2291
|
+
displayHeight = tempImg.naturalHeight || 200;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
|
|
2295
|
+
canvas.width = displayWidth;
|
|
2296
|
+
canvas.height = displayHeight;
|
|
2297
|
+
|
|
2298
|
+
// Draw image to canvas
|
|
2299
|
+
ctx.drawImage(tempImg, 0, 0, displayWidth, displayHeight);
|
|
2300
|
+
|
|
2301
|
+
// Convert to data URL
|
|
2302
|
+
const dataUrl = canvas.toDataURL('image/png', 1.0);
|
|
2303
|
+
|
|
2304
|
+
// Update original image
|
|
2305
|
+
img.src = dataUrl;
|
|
2306
|
+
img.width = displayWidth;
|
|
2307
|
+
img.height = displayHeight;
|
|
2308
|
+
img.setAttribute('width', displayWidth.toString());
|
|
2309
|
+
img.setAttribute('height', displayHeight.toString());
|
|
2310
|
+
img.style.width = displayWidth + 'px';
|
|
2311
|
+
img.style.height = displayHeight + 'px';
|
|
2312
|
+
|
|
2313
|
+
resolve();
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
tempImg.onerror = function() {
|
|
2317
|
+
console.warn('Failed to load HTML fence image:', img.src);
|
|
2318
|
+
reject(new Error('Image load failed'));
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
// Set source - resolve relative paths
|
|
2322
|
+
if (img.src.startsWith('http') || img.src.startsWith('//')) {
|
|
2323
|
+
tempImg.src = img.src;
|
|
2324
|
+
} else {
|
|
2325
|
+
// Relative path - let browser resolve it
|
|
2326
|
+
const absoluteImg = new Image();
|
|
2327
|
+
absoluteImg.src = img.src;
|
|
2328
|
+
tempImg.src = absoluteImg.src;
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
} catch (err) {
|
|
2332
|
+
console.warn('Failed to convert HTML fence image:', img.src, err);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// Add v:shapes for Word compatibility
|
|
2337
|
+
img.setAttribute('v:shapes', 'image' + Math.random().toString(36).substr(2, 9));
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// Replace container content with processed HTML (whether it had pre or not)
|
|
2341
|
+
container.innerHTML = tempDiv.innerHTML;
|
|
2342
|
+
} else if (!pre) {
|
|
2343
|
+
// Container has rendered HTML already, process its images directly
|
|
2344
|
+
const htmlImages = container.querySelectorAll('img');
|
|
2345
|
+
for (const img of htmlImages) {
|
|
2346
|
+
// Same image processing as above
|
|
2347
|
+
const widthAttr = img.getAttribute('width');
|
|
2348
|
+
const heightAttr = img.getAttribute('height');
|
|
2349
|
+
|
|
2350
|
+
if (widthAttr) {
|
|
2351
|
+
img.width = parseInt(widthAttr);
|
|
2352
|
+
img.style.width = widthAttr.includes('%') ? widthAttr : `${img.width}px`;
|
|
2353
|
+
}
|
|
2354
|
+
if (heightAttr) {
|
|
2355
|
+
img.height = parseInt(heightAttr);
|
|
2356
|
+
img.style.height = heightAttr.includes('%') ? heightAttr : `${img.height}px`;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
if (img.src && !img.src.startsWith('data:')) {
|
|
2360
|
+
try {
|
|
2361
|
+
// Use same canvas approach as above
|
|
2362
|
+
const canvas = document.createElement('canvas');
|
|
2363
|
+
const ctx = canvas.getContext('2d');
|
|
2364
|
+
const tempImg = new Image();
|
|
2365
|
+
tempImg.crossOrigin = 'anonymous';
|
|
2366
|
+
|
|
2367
|
+
await new Promise((resolve, reject) => {
|
|
2368
|
+
tempImg.onload = function() {
|
|
2369
|
+
// Calculate dimensions preserving aspect ratio
|
|
2370
|
+
let displayWidth = img.width || 0;
|
|
2371
|
+
let displayHeight = img.height || 0;
|
|
2372
|
+
|
|
2373
|
+
// If only width is specified, calculate height based on aspect ratio
|
|
2374
|
+
if (displayWidth && !displayHeight) {
|
|
2375
|
+
const aspectRatio = tempImg.naturalHeight / tempImg.naturalWidth;
|
|
2376
|
+
displayHeight = Math.round(displayWidth * aspectRatio);
|
|
2377
|
+
}
|
|
2378
|
+
// If only height is specified, calculate width based on aspect ratio
|
|
2379
|
+
else if (displayHeight && !displayWidth) {
|
|
2380
|
+
const aspectRatio = tempImg.naturalWidth / tempImg.naturalHeight;
|
|
2381
|
+
displayWidth = Math.round(displayHeight * aspectRatio);
|
|
2382
|
+
}
|
|
2383
|
+
// If neither specified, use natural dimensions
|
|
2384
|
+
else if (!displayWidth && !displayHeight) {
|
|
2385
|
+
displayWidth = tempImg.naturalWidth || 250;
|
|
2386
|
+
displayHeight = tempImg.naturalHeight || Math.round(250 * (tempImg.naturalHeight / tempImg.naturalWidth));
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
canvas.width = displayWidth;
|
|
2390
|
+
canvas.height = displayHeight;
|
|
2391
|
+
ctx.drawImage(tempImg, 0, 0, displayWidth, displayHeight);
|
|
2392
|
+
|
|
2393
|
+
const dataUrl = canvas.toDataURL('image/png', 1.0);
|
|
2394
|
+
img.src = dataUrl;
|
|
2395
|
+
img.width = displayWidth;
|
|
2396
|
+
img.height = displayHeight;
|
|
2397
|
+
img.setAttribute('width', displayWidth.toString());
|
|
2398
|
+
img.setAttribute('height', displayHeight.toString());
|
|
2399
|
+
img.style.width = displayWidth + 'px';
|
|
2400
|
+
img.style.height = displayHeight + 'px';
|
|
2401
|
+
|
|
2402
|
+
resolve();
|
|
2403
|
+
};
|
|
2404
|
+
|
|
2405
|
+
tempImg.onerror = function() {
|
|
2406
|
+
console.warn('Failed to load HTML fence image:', img.src);
|
|
2407
|
+
reject(new Error('Image load failed'));
|
|
2408
|
+
};
|
|
2409
|
+
|
|
2410
|
+
if (img.src.startsWith('http') || img.src.startsWith('//')) {
|
|
2411
|
+
tempImg.src = img.src;
|
|
2412
|
+
} else {
|
|
2413
|
+
const absoluteImg = new Image();
|
|
2414
|
+
absoluteImg.src = img.src;
|
|
2415
|
+
tempImg.src = absoluteImg.src;
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
console.warn('Failed to convert HTML fence image:', img.src, err);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
img.setAttribute('v:shapes', 'image' + Math.random().toString(36).substr(2, 9));
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
} catch (err) {
|
|
2427
|
+
console.warn('Failed to process HTML container:', err);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// 8. Tables are already HTML tables from the built-in renderer
|
|
2432
|
+
// No processing needed
|
|
2433
|
+
|
|
2434
|
+
// Wrap in proper HTML structure for rich text editors
|
|
2435
|
+
const fragment = clone.innerHTML;
|
|
2436
|
+
const htmlContent = `
|
|
2437
|
+
<!DOCTYPE html>
|
|
2438
|
+
<html xmlns:v="urn:schemas-microsoft-com:vml"
|
|
2439
|
+
xmlns:o="urn:schemas-microsoft-com:office:office"
|
|
2440
|
+
xmlns:w="urn:schemas-microsoft-com:office:word">
|
|
2441
|
+
<head>
|
|
2442
|
+
<meta charset="utf-8">
|
|
2443
|
+
<style>
|
|
2444
|
+
/* Table styling */
|
|
2445
|
+
table { border-collapse: collapse; width: 100%; margin-bottom: 1em; }
|
|
2446
|
+
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
|
|
2447
|
+
th { background-color: #f0f0f0; font-weight: bold; }
|
|
2448
|
+
|
|
2449
|
+
/* Code block styling */
|
|
2450
|
+
pre { background-color: #f4f4f4; padding: 1em; border-radius: 4px; overflow-x: auto; }
|
|
2451
|
+
code { font-family: monospace; background-color: #f4f4f4; padding: 0.2em 0.4em; border-radius: 3px; }
|
|
2452
|
+
|
|
2453
|
+
/* Image handling */
|
|
2454
|
+
img { display: block; max-width: 100%; height: auto; margin: 0.5em 0; }
|
|
2455
|
+
|
|
2456
|
+
/* Blockquote */
|
|
2457
|
+
blockquote { border-left: 4px solid #ddd; margin-left: 0; padding-left: 1em; color: #666; }
|
|
2458
|
+
|
|
2459
|
+
/* Math equations centered like squibview */
|
|
2460
|
+
.math-display { text-align: center; margin: 1em 0; }
|
|
2461
|
+
.math-display img { display: inline-block; margin: 0 auto; }
|
|
2462
|
+
</style>
|
|
2463
|
+
</head>
|
|
2464
|
+
<body><!--StartFragment-->${fragment}<!--EndFragment--></body>
|
|
2465
|
+
</html>`;
|
|
2466
|
+
|
|
2467
|
+
// Get plain text version
|
|
2468
|
+
const text = clone.textContent || clone.innerText || '';
|
|
2469
|
+
|
|
2470
|
+
// Get platform for clipboard strategy (like squibview)
|
|
2471
|
+
const platform = getPlatform();
|
|
2472
|
+
|
|
2473
|
+
if (platform === 'macos') {
|
|
2474
|
+
// macOS approach (like squibview)
|
|
2475
|
+
try {
|
|
2476
|
+
await navigator.clipboard.write([
|
|
2477
|
+
new ClipboardItem({
|
|
2478
|
+
'text/html': new Blob([htmlContent], { type: 'text/html' }),
|
|
2479
|
+
'text/plain': new Blob([text], { type: 'text/plain' })
|
|
2480
|
+
})
|
|
2481
|
+
]);
|
|
2482
|
+
return { success: true, html: htmlContent, text };
|
|
2483
|
+
} catch (modernErr) {
|
|
2484
|
+
console.warn('Modern clipboard API failed, trying Safari fallback:', modernErr);
|
|
2485
|
+
// Safari fallback (selection-based HTML of fragment)
|
|
2486
|
+
if (copyToClipboard(fragment)) {
|
|
2487
|
+
return { success: true, html: htmlContent, text };
|
|
2488
|
+
}
|
|
2489
|
+
throw new Error('Fallback copy failed');
|
|
2490
|
+
}
|
|
2491
|
+
} else {
|
|
2492
|
+
// Windows/Linux approach (like squibview)
|
|
2493
|
+
const tempDiv = document.createElement('div');
|
|
2494
|
+
tempDiv.style.position = 'fixed';
|
|
2495
|
+
tempDiv.style.left = '-9999px';
|
|
2496
|
+
tempDiv.style.top = '0';
|
|
2497
|
+
// Use fragment for selection-based fallback copy
|
|
2498
|
+
tempDiv.innerHTML = fragment;
|
|
2499
|
+
document.body.appendChild(tempDiv);
|
|
2500
|
+
|
|
2501
|
+
try {
|
|
2502
|
+
await navigator.clipboard.write([
|
|
2503
|
+
new ClipboardItem({
|
|
2504
|
+
'text/html': new Blob([htmlContent], { type: 'text/html' }),
|
|
2505
|
+
'text/plain': new Blob([text], { type: 'text/plain' })
|
|
2506
|
+
})
|
|
2507
|
+
]);
|
|
2508
|
+
return { success: true, html: htmlContent, text };
|
|
2509
|
+
} catch (modernErr) {
|
|
2510
|
+
console.warn('Modern clipboard API failed, trying execCommand fallback:', modernErr);
|
|
2511
|
+
const selection = window.getSelection();
|
|
2512
|
+
const range = document.createRange();
|
|
2513
|
+
range.selectNodeContents(tempDiv);
|
|
2514
|
+
selection.removeAllRanges();
|
|
2515
|
+
selection.addRange(range);
|
|
2516
|
+
|
|
2517
|
+
const successful = document.execCommand('copy');
|
|
2518
|
+
if (!successful) {
|
|
2519
|
+
throw new Error('Fallback copy failed');
|
|
2520
|
+
}
|
|
2521
|
+
return { success: true, html: htmlContent, text };
|
|
2522
|
+
} finally {
|
|
2523
|
+
if (tempDiv && tempDiv.parentNode) {
|
|
2524
|
+
document.body.removeChild(tempDiv);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
} catch (err) {
|
|
2530
|
+
console.error('Failed to copy rendered content:', err);
|
|
2531
|
+
throw err;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
1069
2535
|
/**
|
|
1070
2536
|
* Quikdown Editor - A drop-in markdown editor control
|
|
1071
2537
|
* @version 1.0.5
|
|
@@ -1213,7 +2679,8 @@ class QuikdownEditor {
|
|
|
1213
2679
|
// Copy buttons
|
|
1214
2680
|
const copyButtons = [
|
|
1215
2681
|
{ action: 'copy-markdown', text: 'Copy MD', title: 'Copy markdown to clipboard' },
|
|
1216
|
-
{ action: 'copy-html', text: 'Copy HTML', title: 'Copy HTML to clipboard' }
|
|
2682
|
+
{ action: 'copy-html', text: 'Copy HTML', title: 'Copy HTML to clipboard' },
|
|
2683
|
+
{ action: 'copy-rendered', text: 'Copy Rendered', title: 'Copy rich text to clipboard' }
|
|
1217
2684
|
];
|
|
1218
2685
|
|
|
1219
2686
|
copyButtons.forEach(({ action, text, title }) => {
|
|
@@ -1619,6 +3086,24 @@ class QuikdownEditor {
|
|
|
1619
3086
|
this.previewPanel.innerHTML = this._html;
|
|
1620
3087
|
// Make all fence blocks non-editable
|
|
1621
3088
|
this.makeFencesNonEditable();
|
|
3089
|
+
|
|
3090
|
+
// Process all math elements with MathJax if loaded (like squibview)
|
|
3091
|
+
if (window.MathJax && window.MathJax.typesetPromise) {
|
|
3092
|
+
const mathElements = this.previewPanel.querySelectorAll('.math-display');
|
|
3093
|
+
if (mathElements.length > 0) {
|
|
3094
|
+
mathElements.forEach(el => {
|
|
3095
|
+
});
|
|
3096
|
+
window.MathJax.typesetPromise(Array.from(mathElements))
|
|
3097
|
+
.then(() => {
|
|
3098
|
+
mathElements.forEach(el => {
|
|
3099
|
+
el.querySelector('mjx-container');
|
|
3100
|
+
});
|
|
3101
|
+
})
|
|
3102
|
+
.catch(err => {
|
|
3103
|
+
console.warn('MathJax batch processing failed:', err);
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
1622
3107
|
}
|
|
1623
3108
|
}
|
|
1624
3109
|
|
|
@@ -1640,7 +3125,7 @@ class QuikdownEditor {
|
|
|
1640
3125
|
|
|
1641
3126
|
this._html = this.previewPanel.innerHTML;
|
|
1642
3127
|
this._markdown = quikdown_bd.toMarkdown(clonedPanel, {
|
|
1643
|
-
fence_plugin: this.
|
|
3128
|
+
fence_plugin: this.createFencePlugin()
|
|
1644
3129
|
});
|
|
1645
3130
|
|
|
1646
3131
|
// Update source if visible
|
|
@@ -1660,7 +3145,6 @@ class QuikdownEditor {
|
|
|
1660
3145
|
preprocessSpecialElements(panel) {
|
|
1661
3146
|
if (!panel) return;
|
|
1662
3147
|
|
|
1663
|
-
|
|
1664
3148
|
// Restore non-editable complex fences from their data attributes
|
|
1665
3149
|
const complexFences = panel.querySelectorAll('[contenteditable="false"][data-qd-source]');
|
|
1666
3150
|
complexFences.forEach(element => {
|
|
@@ -1758,7 +3242,6 @@ class QuikdownEditor {
|
|
|
1758
3242
|
return this.renderHTML(code);
|
|
1759
3243
|
|
|
1760
3244
|
case 'math':
|
|
1761
|
-
case 'katex':
|
|
1762
3245
|
case 'tex':
|
|
1763
3246
|
case 'latex':
|
|
1764
3247
|
return this.renderMath(code, lang);
|
|
@@ -1772,11 +3255,20 @@ class QuikdownEditor {
|
|
|
1772
3255
|
case 'json5':
|
|
1773
3256
|
return this.renderJSON(code, lang);
|
|
1774
3257
|
|
|
3258
|
+
case 'katex': // Use MathJax for katex fence blocks (backward compatibility)
|
|
3259
|
+
return this.renderMath(code, 'katex');
|
|
3260
|
+
|
|
1775
3261
|
case 'mermaid':
|
|
1776
3262
|
if (window.mermaid) {
|
|
1777
3263
|
return this.renderMermaid(code);
|
|
1778
3264
|
}
|
|
1779
3265
|
break;
|
|
3266
|
+
|
|
3267
|
+
case 'geojson':
|
|
3268
|
+
return this.renderGeoJSON(code);
|
|
3269
|
+
|
|
3270
|
+
case 'stl':
|
|
3271
|
+
return this.renderSTL(code);
|
|
1780
3272
|
}
|
|
1781
3273
|
}
|
|
1782
3274
|
|
|
@@ -1791,8 +3283,37 @@ class QuikdownEditor {
|
|
|
1791
3283
|
return undefined;
|
|
1792
3284
|
};
|
|
1793
3285
|
|
|
1794
|
-
//
|
|
1795
|
-
|
|
3286
|
+
// Reverse function to extract raw source from rendered HTML
|
|
3287
|
+
const reverse = (element) => {
|
|
3288
|
+
// Get the language from data attribute
|
|
3289
|
+
const lang = element.getAttribute('data-qd-lang') || '';
|
|
3290
|
+
let content = '';
|
|
3291
|
+
|
|
3292
|
+
// For syntax-highlighted code, extract the raw text
|
|
3293
|
+
if (element.querySelector('code.hljs')) {
|
|
3294
|
+
const code = element.querySelector('code.hljs');
|
|
3295
|
+
content = code.textContent || code.innerText || '';
|
|
3296
|
+
}
|
|
3297
|
+
// For other code blocks, just get the text content
|
|
3298
|
+
else if (element.querySelector('code')) {
|
|
3299
|
+
const codeEl = element.querySelector('code');
|
|
3300
|
+
content = codeEl.textContent || codeEl.innerText || '';
|
|
3301
|
+
}
|
|
3302
|
+
// Fallback to element text
|
|
3303
|
+
else {
|
|
3304
|
+
content = element.textContent || element.innerText || '';
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
// Return in the format quikdown_bd expects
|
|
3308
|
+
return {
|
|
3309
|
+
content: content,
|
|
3310
|
+
lang: lang,
|
|
3311
|
+
fence: '```'
|
|
3312
|
+
};
|
|
3313
|
+
};
|
|
3314
|
+
|
|
3315
|
+
// Return object format for v1.1.0 API with both render and reverse
|
|
3316
|
+
return { render, reverse };
|
|
1796
3317
|
}
|
|
1797
3318
|
|
|
1798
3319
|
/**
|
|
@@ -1904,80 +3425,88 @@ class QuikdownEditor {
|
|
|
1904
3425
|
}
|
|
1905
3426
|
|
|
1906
3427
|
/**
|
|
1907
|
-
* Render math with
|
|
3428
|
+
* Render math with MathJax (SVG output for better copy support)
|
|
1908
3429
|
*/
|
|
1909
3430
|
renderMath(code, lang) {
|
|
1910
|
-
const id = `math-${
|
|
3431
|
+
const id = `math-${Math.random().toString(36).substring(2, 15)}`;
|
|
1911
3432
|
|
|
1912
|
-
//
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
const errorContainer = document.createElement('pre');
|
|
1932
|
-
errorContainer.className = 'qde-error';
|
|
1933
|
-
errorContainer.contentEditable = 'false';
|
|
1934
|
-
errorContainer.setAttribute('data-qd-fence', '```');
|
|
1935
|
-
errorContainer.setAttribute('data-qd-lang', lang);
|
|
1936
|
-
errorContainer.setAttribute('data-qd-source', code);
|
|
1937
|
-
errorContainer.textContent = `Math error: ${err.message}`;
|
|
1938
|
-
return errorContainer.outerHTML;
|
|
1939
|
-
}
|
|
3433
|
+
// Create container exactly like squibview
|
|
3434
|
+
const container = document.createElement('div');
|
|
3435
|
+
container.id = id;
|
|
3436
|
+
container.className = 'math-display';
|
|
3437
|
+
container.contentEditable = 'false';
|
|
3438
|
+
container.setAttribute('data-source-type', 'math');
|
|
3439
|
+
|
|
3440
|
+
// Format content for MathJax (display mode with $$) - exactly like squibview
|
|
3441
|
+
const singleLineContent = code.replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
3442
|
+
container.textContent = `$$${singleLineContent}$$`;
|
|
3443
|
+
|
|
3444
|
+
// Add centering style
|
|
3445
|
+
container.style.textAlign = 'center';
|
|
3446
|
+
container.style.margin = '1em 0';
|
|
3447
|
+
|
|
3448
|
+
|
|
3449
|
+
// Ensure MathJax will be loaded (if not already)
|
|
3450
|
+
if (!window.MathJax || !window.MathJax.typesetPromise) {
|
|
3451
|
+
this.ensureMathJaxLoaded();
|
|
1940
3452
|
}
|
|
1941
3453
|
|
|
1942
|
-
//
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
3454
|
+
// MathJax will be processed in batch after preview update
|
|
3455
|
+
return container.outerHTML;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
/**
|
|
3459
|
+
* Ensures MathJax is loaded (but doesn't process elements)
|
|
3460
|
+
*/
|
|
3461
|
+
ensureMathJaxLoaded() {
|
|
3462
|
+
if (typeof window.MathJax === 'undefined' && !window.mathJaxLoading) {
|
|
3463
|
+
window.mathJaxLoading = true;
|
|
3464
|
+
|
|
3465
|
+
// Configure MathJax before loading
|
|
3466
|
+
if (!window.MathJax) {
|
|
3467
|
+
window.MathJax = {
|
|
3468
|
+
loader: { load: ['input/tex', 'output/svg'] },
|
|
3469
|
+
tex: {
|
|
3470
|
+
packages: { '[+]': ['ams'] },
|
|
3471
|
+
inlineMath: [['$', '$'], ['\\(', '\\)']],
|
|
3472
|
+
displayMath: [['$$', '$$'], ['\\[', '\\]']],
|
|
3473
|
+
processEscapes: true,
|
|
3474
|
+
processEnvironments: true
|
|
3475
|
+
},
|
|
3476
|
+
options: {
|
|
3477
|
+
renderActions: { addMenu: [] },
|
|
3478
|
+
ignoreHtmlClass: 'tex2jax_ignore',
|
|
3479
|
+
processHtmlClass: 'tex2jax_process'
|
|
3480
|
+
},
|
|
3481
|
+
svg: {
|
|
3482
|
+
fontCache: 'none' // Important: self-contained SVGs for copy
|
|
3483
|
+
},
|
|
3484
|
+
startup: { typeset: false }
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
const script = document.createElement('script');
|
|
3489
|
+
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js';
|
|
3490
|
+
script.async = true;
|
|
3491
|
+
script.onload = () => {
|
|
3492
|
+
window.mathJaxLoading = false;
|
|
3493
|
+
|
|
3494
|
+
// Process any existing math elements (like squibview)
|
|
3495
|
+
if (window.MathJax && window.MathJax.typesetPromise) {
|
|
3496
|
+
const mathElements = document.querySelectorAll('.math-display');
|
|
3497
|
+
if (mathElements.length > 0) {
|
|
3498
|
+
window.MathJax.typesetPromise(Array.from(mathElements)).catch(err => {
|
|
3499
|
+
console.warn('Initial MathJax processing failed:', err);
|
|
1956
3500
|
});
|
|
1957
|
-
// Update attributes after rendering
|
|
1958
|
-
element.setAttribute('data-qd-source', code);
|
|
1959
|
-
element.setAttribute('data-qd-fence', '```');
|
|
1960
|
-
element.setAttribute('data-qd-lang', lang);
|
|
1961
|
-
} catch (err) {
|
|
1962
|
-
element.innerHTML = `<pre class="qde-error">Math error: ${this.escapeHtml(err.message)}</pre>`;
|
|
1963
3501
|
}
|
|
1964
3502
|
}
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
placeholder.contentEditable = 'false';
|
|
1973
|
-
placeholder.setAttribute('data-qd-fence', '```');
|
|
1974
|
-
placeholder.setAttribute('data-qd-lang', lang);
|
|
1975
|
-
placeholder.setAttribute('data-qd-source', code);
|
|
1976
|
-
const pre = document.createElement('pre');
|
|
1977
|
-
pre.textContent = code;
|
|
1978
|
-
placeholder.appendChild(pre);
|
|
1979
|
-
|
|
1980
|
-
return placeholder.outerHTML;
|
|
3503
|
+
};
|
|
3504
|
+
script.onerror = () => {
|
|
3505
|
+
window.mathJaxLoading = false;
|
|
3506
|
+
console.error('Failed to load MathJax');
|
|
3507
|
+
};
|
|
3508
|
+
document.head.appendChild(script);
|
|
3509
|
+
}
|
|
1981
3510
|
}
|
|
1982
3511
|
|
|
1983
3512
|
/**
|
|
@@ -2084,6 +3613,237 @@ class QuikdownEditor {
|
|
|
2084
3613
|
return `<pre class="qde-json" data-qd-fence="\`\`\`" data-qd-lang="${lang}">${this.escapeHtml(code)}</pre>`;
|
|
2085
3614
|
}
|
|
2086
3615
|
|
|
3616
|
+
/**
|
|
3617
|
+
* Render GeoJSON map
|
|
3618
|
+
*/
|
|
3619
|
+
renderGeoJSON(code) {
|
|
3620
|
+
// Generate unique map ID (following SquibView pattern)
|
|
3621
|
+
const mapId = `map-${Math.random().toString(36).substr(2, 15)}`;
|
|
3622
|
+
|
|
3623
|
+
// Function to render the map
|
|
3624
|
+
const renderMap = () => {
|
|
3625
|
+
const container = document.getElementById(mapId + '-container');
|
|
3626
|
+
if (!container || !window.L) return;
|
|
3627
|
+
|
|
3628
|
+
try {
|
|
3629
|
+
const data = JSON.parse(code);
|
|
3630
|
+
|
|
3631
|
+
// Clear container and set deterministic size for rasterization
|
|
3632
|
+
const mapDiv = document.createElement('div');
|
|
3633
|
+
mapDiv.id = mapId;
|
|
3634
|
+
mapDiv.style.cssText = 'width: 100%; height: 300px;';
|
|
3635
|
+
container.innerHTML = '';
|
|
3636
|
+
container.appendChild(mapDiv);
|
|
3637
|
+
|
|
3638
|
+
// Create the map
|
|
3639
|
+
const map = L.map(mapId);
|
|
3640
|
+
|
|
3641
|
+
// Store back-reference for capture (per Gem's guide)
|
|
3642
|
+
container._map = map; // Avoid window pollution
|
|
3643
|
+
|
|
3644
|
+
// Add tile layer with CORS support
|
|
3645
|
+
const tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
3646
|
+
attribution: '',
|
|
3647
|
+
crossOrigin: 'anonymous' // Important for canvas capture
|
|
3648
|
+
});
|
|
3649
|
+
tileLayer.addTo(map);
|
|
3650
|
+
|
|
3651
|
+
// Add GeoJSON layer
|
|
3652
|
+
const geoJsonLayer = L.geoJSON(data);
|
|
3653
|
+
geoJsonLayer.addTo(map);
|
|
3654
|
+
|
|
3655
|
+
// Fit bounds if valid
|
|
3656
|
+
if (geoJsonLayer.getBounds().isValid()) {
|
|
3657
|
+
map.fitBounds(geoJsonLayer.getBounds());
|
|
3658
|
+
} else {
|
|
3659
|
+
map.setView([0, 0], 2);
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
// Store references for copy-time capture
|
|
3663
|
+
container._tileLayer = tileLayer;
|
|
3664
|
+
container._geoJsonLayer = geoJsonLayer;
|
|
3665
|
+
|
|
3666
|
+
// Optional: Wait for tiles to load for better capture
|
|
3667
|
+
tileLayer.on('load', () => {
|
|
3668
|
+
container.setAttribute('data-tiles-loaded', 'true');
|
|
3669
|
+
});
|
|
3670
|
+
|
|
3671
|
+
} catch (err) {
|
|
3672
|
+
container.innerHTML = `<pre class="qde-error">GeoJSON error: ${this.escapeHtml(err.message)}</pre>`;
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
3675
|
+
|
|
3676
|
+
// Check if Leaflet is already loaded
|
|
3677
|
+
if (window.L) {
|
|
3678
|
+
// Render after DOM update
|
|
3679
|
+
setTimeout(renderMap, 0);
|
|
3680
|
+
} else {
|
|
3681
|
+
// Lazy load Leaflet only if not already loading
|
|
3682
|
+
if (!window._qde_leaflet_loading) {
|
|
3683
|
+
window._qde_leaflet_loading = this.lazyLoadLibrary(
|
|
3684
|
+
'Leaflet',
|
|
3685
|
+
() => window.L,
|
|
3686
|
+
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
|
|
3687
|
+
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'
|
|
3688
|
+
).catch(err => {
|
|
3689
|
+
console.warn('Failed to load Leaflet:', err);
|
|
3690
|
+
// Clear the loading promise so it can be retried
|
|
3691
|
+
window._qde_leaflet_loading = null;
|
|
3692
|
+
return false;
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
window._qde_leaflet_loading.then(loaded => {
|
|
3697
|
+
if (loaded) {
|
|
3698
|
+
renderMap();
|
|
3699
|
+
} else {
|
|
3700
|
+
const element = document.getElementById(id);
|
|
3701
|
+
if (element) {
|
|
3702
|
+
element.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">Failed to load map library</div>';
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
}).catch(() => {
|
|
3706
|
+
// Error already handled above
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
// Return container following SquibView pattern
|
|
3711
|
+
const container = document.createElement('div');
|
|
3712
|
+
container.className = 'geojson-container';
|
|
3713
|
+
container.id = mapId + '-container';
|
|
3714
|
+
container.style.cssText = 'width: 100%; height: 300px; border: 1px solid #ddd; border-radius: 4px; margin: 0.5em 0; background: #f0f0f0;';
|
|
3715
|
+
container.contentEditable = 'false';
|
|
3716
|
+
|
|
3717
|
+
// Preserve source for copy-time identification (per Gem's guide)
|
|
3718
|
+
container.setAttribute('data-source-type', 'geojson');
|
|
3719
|
+
container.setAttribute('data-original-source', this.escapeHtml(code));
|
|
3720
|
+
|
|
3721
|
+
// For bidirectional editing
|
|
3722
|
+
container.setAttribute('data-qd-fence', '```');
|
|
3723
|
+
container.setAttribute('data-qd-lang', 'geojson');
|
|
3724
|
+
container.setAttribute('data-qd-source', code);
|
|
3725
|
+
|
|
3726
|
+
container.textContent = 'Loading map...';
|
|
3727
|
+
|
|
3728
|
+
return container.outerHTML;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
/**
|
|
3732
|
+
* Render STL 3D model
|
|
3733
|
+
*/
|
|
3734
|
+
renderSTL(code) {
|
|
3735
|
+
const id = `qde-stl-viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
3736
|
+
|
|
3737
|
+
// Function to render the 3D model
|
|
3738
|
+
const render3D = () => {
|
|
3739
|
+
const element = document.getElementById(id);
|
|
3740
|
+
if (!element) return;
|
|
3741
|
+
|
|
3742
|
+
// Check if Three.js is available
|
|
3743
|
+
if (typeof window.THREE === 'undefined') {
|
|
3744
|
+
element.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">Three.js library not loaded. Add <script src="https://unpkg.com/three@0.147.0/build/three.min.js"></script> to your HTML.</div>';
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
try {
|
|
3749
|
+
const THREE = window.THREE;
|
|
3750
|
+
|
|
3751
|
+
// Create scene
|
|
3752
|
+
const scene = new THREE.Scene();
|
|
3753
|
+
scene.background = new THREE.Color(0xf0f0f0);
|
|
3754
|
+
|
|
3755
|
+
// Create camera
|
|
3756
|
+
const camera = new THREE.PerspectiveCamera(75, element.clientWidth / 400, 0.1, 1000);
|
|
3757
|
+
|
|
3758
|
+
// Create renderer
|
|
3759
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
3760
|
+
renderer.setSize(element.clientWidth, 400);
|
|
3761
|
+
element.innerHTML = '';
|
|
3762
|
+
element.appendChild(renderer.domElement);
|
|
3763
|
+
|
|
3764
|
+
// Store Three.js references for copy functionality (like squibview)
|
|
3765
|
+
element._threeScene = scene;
|
|
3766
|
+
element._threeCamera = camera;
|
|
3767
|
+
element._threeRenderer = renderer;
|
|
3768
|
+
|
|
3769
|
+
// Parse STL data (ASCII format)
|
|
3770
|
+
const geometry = this.parseSTL(code);
|
|
3771
|
+
const material = new THREE.MeshLambertMaterial({ color: 0x0066ff });
|
|
3772
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
3773
|
+
scene.add(mesh);
|
|
3774
|
+
|
|
3775
|
+
// Add lighting
|
|
3776
|
+
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
|
|
3777
|
+
scene.add(ambientLight);
|
|
3778
|
+
|
|
3779
|
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
3780
|
+
directionalLight.position.set(1, 1, 1).normalize();
|
|
3781
|
+
scene.add(directionalLight);
|
|
3782
|
+
|
|
3783
|
+
// Position camera based on object bounds
|
|
3784
|
+
const box = new THREE.Box3().setFromObject(mesh);
|
|
3785
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
3786
|
+
const size = box.getSize(new THREE.Vector3());
|
|
3787
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
3788
|
+
|
|
3789
|
+
camera.position.set(center.x + maxDim, center.y + maxDim, center.z + maxDim);
|
|
3790
|
+
camera.lookAt(center);
|
|
3791
|
+
|
|
3792
|
+
// Animate
|
|
3793
|
+
const animate = () => {
|
|
3794
|
+
requestAnimationFrame(animate);
|
|
3795
|
+
mesh.rotation.y += 0.01;
|
|
3796
|
+
renderer.render(scene, camera);
|
|
3797
|
+
};
|
|
3798
|
+
animate();
|
|
3799
|
+
} catch (err) {
|
|
3800
|
+
console.error('STL rendering error:', err);
|
|
3801
|
+
element.innerHTML = `<pre class="qde-error">STL error: ${this.escapeHtml(err.message)}</pre>`;
|
|
3802
|
+
}
|
|
3803
|
+
};
|
|
3804
|
+
|
|
3805
|
+
// Render after DOM update
|
|
3806
|
+
setTimeout(render3D, 0);
|
|
3807
|
+
|
|
3808
|
+
// Return placeholder with data-stl-id for copy functionality
|
|
3809
|
+
return `<div id="${id}" class="qde-stl-container" data-stl-id="${id}" data-qd-fence="\`\`\`" data-qd-lang="stl" data-qd-source="${this.escapeHtml(code)}" contenteditable="false" style="height: 400px; background: #f0f0f0; display: flex; align-items: center; justify-content: center;">Loading 3D model...</div>`;
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
/**
|
|
3813
|
+
* Parse ASCII STL format
|
|
3814
|
+
* @param {string} stlData - The STL file content
|
|
3815
|
+
* @returns {THREE.BufferGeometry} - The parsed geometry
|
|
3816
|
+
*/
|
|
3817
|
+
parseSTL(stlData) {
|
|
3818
|
+
const THREE = window.THREE;
|
|
3819
|
+
const geometry = new THREE.BufferGeometry();
|
|
3820
|
+
const vertices = [];
|
|
3821
|
+
const normals = [];
|
|
3822
|
+
|
|
3823
|
+
const lines = stlData.split('\n');
|
|
3824
|
+
let currentNormal = null;
|
|
3825
|
+
|
|
3826
|
+
for (let line of lines) {
|
|
3827
|
+
line = line.trim();
|
|
3828
|
+
|
|
3829
|
+
if (line.startsWith('facet normal')) {
|
|
3830
|
+
const parts = line.split(/\s+/);
|
|
3831
|
+
currentNormal = [parseFloat(parts[2]), parseFloat(parts[3]), parseFloat(parts[4])];
|
|
3832
|
+
} else if (line.startsWith('vertex')) {
|
|
3833
|
+
const parts = line.split(/\s+/);
|
|
3834
|
+
vertices.push(parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3]));
|
|
3835
|
+
if (currentNormal) {
|
|
3836
|
+
normals.push(currentNormal[0], currentNormal[1], currentNormal[2]);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
3842
|
+
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
|
|
3843
|
+
|
|
3844
|
+
return geometry;
|
|
3845
|
+
}
|
|
3846
|
+
|
|
2087
3847
|
/**
|
|
2088
3848
|
* Render Mermaid diagram
|
|
2089
3849
|
*/
|
|
@@ -2251,7 +4011,7 @@ class QuikdownEditor {
|
|
|
2251
4011
|
this.options.lazy_linefeeds = enabled;
|
|
2252
4012
|
// Re-render if we have content
|
|
2253
4013
|
if (this._markdown) {
|
|
2254
|
-
this.
|
|
4014
|
+
this.updateFromMarkdown(this._markdown);
|
|
2255
4015
|
}
|
|
2256
4016
|
}
|
|
2257
4017
|
|
|
@@ -2322,6 +4082,9 @@ class QuikdownEditor {
|
|
|
2322
4082
|
case 'copy-html':
|
|
2323
4083
|
this.copy('html');
|
|
2324
4084
|
break;
|
|
4085
|
+
case 'copy-rendered':
|
|
4086
|
+
this.copyRendered();
|
|
4087
|
+
break;
|
|
2325
4088
|
case 'remove-hr':
|
|
2326
4089
|
this.removeHR();
|
|
2327
4090
|
break;
|
|
@@ -2441,6 +4204,28 @@ class QuikdownEditor {
|
|
|
2441
4204
|
}
|
|
2442
4205
|
}
|
|
2443
4206
|
|
|
4207
|
+
/**
|
|
4208
|
+
* Copy rendered content as rich text
|
|
4209
|
+
*/
|
|
4210
|
+
async copyRendered() {
|
|
4211
|
+
try {
|
|
4212
|
+
const result = await getRenderedContent(this.previewPanel);
|
|
4213
|
+
if (result.success) {
|
|
4214
|
+
// Visual feedback
|
|
4215
|
+
const btn = this.toolbar?.querySelector('[data-action="copy-rendered"]');
|
|
4216
|
+
if (btn) {
|
|
4217
|
+
const originalText = btn.textContent;
|
|
4218
|
+
btn.textContent = 'Copied!';
|
|
4219
|
+
setTimeout(() => {
|
|
4220
|
+
btn.textContent = originalText;
|
|
4221
|
+
}, 1500);
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
} catch (err) {
|
|
4225
|
+
console.error('Failed to copy rendered content:', err);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
|
|
2444
4229
|
/**
|
|
2445
4230
|
* Destroy the editor
|
|
2446
4231
|
*/
|