Files
mp-units/docs/javascripts/ce_embed.js

169 lines
7.6 KiB
JavaScript

// The MIT License (MIT)
//
// Copyright (c) 2018 Mateusz Pusz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Transform marked code blocks into Compiler Explorer (CE) iframes at runtime.
// Usage in Markdown: place a directive as the FIRST non-empty line inside a fenced code block.
//
// ```cpp
// // ce-embed height=650 compiler=clang2110 flags="-std=c++23 -stdlib=libc++ -O3"
// #include <mp-units/systems/si.h>
// ...
// ```
//
// Optional parameters on the directive:
// - height: number (default 650)
// - compiler: string (default clang2110)
// - flags: string (default "-std=c++23 -stdlib=libc++ -O3")
(function (root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) {
// Node/CommonJS: export only (used by tests)
module.exports = api;
} else {
// Browser: attach and bootstrap
root.ceEmbed = api;
if (typeof root !== 'undefined') {
try { api.bootstrap(); } catch (e) { /* noop */ }
}
}
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
function encodeCode(code) {
// Strict percent-encode all except: a-z, A-Z, 0-9, '-', '_', '.', '~', '/', ':', ',', '(', ')'
// Space -> '+'; Newline -> '%0A'; Single quote -> !'
// This matches CE's encoding for share links.
const safe = /[a-zA-Z0-9\-_.~/:,()]/;
let out = '';
for (let i = 0; i < code.length; ++i) {
const ch = code[i];
if (ch === ' ') {
out += '+';
} else if (ch === '\n') {
out += '%0A';
} else if (ch === "'") {
out += "!'";
} else if (safe.test(ch)) {
out += ch;
} else {
// Use encodeURIComponent for all other chars (including &)
out += encodeURIComponent(ch);
}
}
return out;
}
function buildSrc(code, compiler, flags, mpUnits) {
// Normalize line endings and trim one trailing newline to match CE share output
let norm = code.replace(/\r\n/g, '\n');
if (norm.endsWith('\n')) norm = norm.slice(0, -1);
// Replace all single '!' with '!!' for CE link encoding, as required
norm = norm.replace(/!/g, '!!');
const CODE = encodeCode(norm)
// Prefer unencoded slashes to better match CE's own sharing format
.replace(/%2F/g, '/');
const FLAGS = encodeURIComponent(flags).replace(/%20/g, '+');
return (
"https://godbolt.org/e?hideEditorToolbars=true#g:!((g:!((g:!((h:codeEditor,i:(" +
"filename:'1',fontScale:15,fontUsePx:'0',j:1,lang:c%2B%2B,selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:'" + CODE + "'),l:'5',n:'0',o:'C%2B%2B+source+%231',t:'0')),k:100,l:'4',m:65.44554455445545,n:'0',o:'',s:0,t:'0'),(g:!((h:executor,i:(argsPanelShown:'1',compilationPanelShown:'1',compiler:" + compiler + ",compilerName:'',compilerOutShown:'0',execArgs:'',execStdin:'',fontScale:16,fontUsePx:'0',j:1,lang:c%2B%2B,libs:!((name:mp-units,ver:" + mpUnits + ")),options:'" + FLAGS + "',overrides:!(),runtimeTools:!(),source:1,stdinPanelShown:'1',wrap:'0'),l:'5',n:'0',o:'Executor+x86-64+clang+21.1.0+(C%2B%2B,+Editor+%231)',t:'0')),header:(),l:'4',m:34.554455445544555,n:'0',o:'',s:0,t:'0')),l:'3',n:'0',o:'',t:'0')),version:4"
);
}
function parseInfo(info) {
// Parse key=value pairs where value may be quoted and contain spaces.
// Supports: key=value | key="value with spaces" | key='value with spaces'
const meta = { height: 650, compiler: 'clang2110', flags: '-std=c++23 -stdlib=libc++ -O3', mpUnits: 'trunk' };
const regex = /(\w+)=((?:"[^"]*")|(?:'[^']*')|[^\s]+)/g;
let match;
while ((match = regex.exec(info)) !== null) {
const key = match[1];
let val = match[2];
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
if (key === 'height') meta.height = parseInt(val, 10) || meta.height;
else if (key === 'compiler') meta.compiler = val;
else if (key === 'flags') meta.flags = val;
else if (key === 'mp-units') meta.mpUnits = val;
}
return meta;
}
function transform() {
// Find fenced code blocks rendered by MkDocs Material
const pres = document.querySelectorAll('pre code');
let transformed = 0;
pres.forEach((codeEl) => {
const raw = codeEl.textContent.replace(/^\uFEFF/, ''); // strip BOM if present
const lines = raw.split('\n');
// Find first non-empty line
let idx = 0;
while (idx < lines.length && lines[idx].trim() === '') idx++;
const firstLine = (lines[idx] || '').trimStart();
if (!firstLine.startsWith('// ce-embed')) return;
// Parse attributes after marker
const attr = firstLine.replace(/^\/\/\s*ce-embed\s*/, '');
const meta = parseInfo('ce-embed ' + attr);
const code = lines.slice(idx + 1).join('\n');
const src = buildSrc(code, meta.compiler, meta.flags, meta.mpUnits);
const iframe = document.createElement('iframe');
iframe.setAttribute('src', src);
iframe.setAttribute('loading', 'lazy');
iframe.setAttribute('title', 'Compiler Explorer - mp-units (Headers)');
iframe.style.width = '100%';
iframe.style.height = `${meta.height}px`;
iframe.style.border = '1px solid #ccc';
iframe.style.borderRadius = '6px';
// Replace the containing highlight block if present, else the pre
const highlight = codeEl.closest('div.highlight, pre, figure');
if (highlight) {
highlight.replaceWith(iframe);
transformed++;
}
});
if (transformed > 0 || window.localStorage.getItem('ceEmbedDebug') === '1') {
// Lightweight debug line in console to confirm script ran
// Enable verbose logging by running in console: localStorage.setItem('ceEmbedDebug','1')
console.log(`[ce-embed] transformed blocks: ${transformed}`);
}
}
function bootstrap() {
// Only run in browser environments with a DOM
if (typeof window === 'undefined' || typeof document === 'undefined') return;
// Hook into MkDocs Material page lifecycle
if (window.document$ && typeof window.document$.subscribe === 'function') {
// Run immediately for the initial page load
try { transform(); } catch (e) { console.error('[ce-embed] initial transform error', e); }
// And again on each SPA navigation
window.document$.subscribe(() => transform());
} else if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', transform);
} else {
transform();
}
}
return { encodeCode, buildSrc, parseInfo, transform, bootstrap };
});