diff --git a/docs/dist/mdvega/v1/mdvega.js b/docs/dist/mdvega/v1/mdvega.js new file mode 100644 index 000000000..dd702d42c --- /dev/null +++ b/docs/dist/mdvega/v1/mdvega.js @@ -0,0 +1,1504 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vega'), require('vega-lite')) : + typeof define === 'function' && define.amd ? define(['exports', 'vega', 'vega-lite'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MdVega = {}, global.vega, global.vegaLite)); +})(this, (function (exports, vega, vegaLite) { 'use strict'; + + const R=/[^\t\n\f />"'=]/,v=" ",E="=",k=".",$="#",p=(e,t,{left:i,right:n,allowed:r})=>{let s="",l="",o=!0,h=!1;const f=[];for(let a=t+i.length;ar.some(c=>c instanceof RegExp?c.test(a):c===a)):f},u=(e,t)=>{t&&e.forEach(i=>{const[n,r]=i;n==="class"?t.attrJoin("class",r):n==="css-module"?t.attrJoin("css-module",r):t.attrPush(i);});},w=(e,t)=>t>=0?e[t]:e[e.length+t],b=e=>e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),I=e=>Array.isArray(e)&&!!e.length&&e.every(t=>typeof t=="function"),S$1=e=>Array.isArray(e)&&!!e.length&&e.every(t=>typeof t=="object"),g=(e,t,i)=>n=>{const r=t.length,s=i.length,l=r+1+s;if(!n||typeof n!="string"||n.length{const x=d.charAt(r)===".",O=d.charAt(r)==="#";return x||O?d.length>=l+1:d.length>=l};let h,f,a,c;const y=l-s;switch(e){case"start":a=n.slice(0,r),h=a===t?0:-1,f=h===-1?-1:n.indexOf(i,y),c=n.charAt(f+s),c&&i.includes(c)&&(f=-1);break;case"end":h=n.lastIndexOf(t),f=h===-1?-1:n.indexOf(i,h+y),f=f===n.length-s?f:-1;break;case"only":a=n.slice(0,r),h=a===t?0:-1,a=n.slice(n.length-s),f=a===i?n.length-s:-1;break}return h!==-1&&f!==-1&&o(n.substring(h,f+s))},j=(e,t,i)=>{const n=b(t),r=b(i),s=e.search(new RegExp(`[ \\n]?${n}[^${n}${r}]+${r}$`));return s!==-1?e.slice(0,s):e},m=(e,t)=>{if(e[t].type==="softbreak")return null;if(e[t].nesting===0)return e[t];const i=e[t].level,n=e[t].type.replace("_close","_open");for(;t>=0;--t)if(e[t].type===n&&e[t].level===i)return e[t];return null},D=e=>({name:"code-block",tests:[{shift:0,block:!0,info:g("end",e.left,e.right)}],transform:(t,i)=>{const n=t[i];let r="";const s=/{(?:[\d,-]+)}/.exec(n.info);s&&(n.info=n.info.replace(s[0],""),r=s[0]);const l=n.info.lastIndexOf(e.left),o=p(n.info,l,e);u(o,n),n.info=`${j(n.info,e.left,e.right)} ${r}`;}}),K=e=>[{name:"inline nesting 0",tests:[{shift:0,type:"inline",children:[{shift:-1,type:t=>t==="image"||t==="code_inline"},{shift:0,type:"text",content:g("start",e.left,e.right)}]}],transform:(t,i,n)=>{const r=t[i].children[n],s=r.content.indexOf(e.right),l=t[i].children[n-1],o=p(r.content,0,e);u(o,l),r.content.length===s+e.right.length?t[i].children.splice(n,1):r.content=r.content.slice(s+e.right.length);}},{name:"inline attributes",tests:[{shift:0,type:"inline",children:[{shift:-1,nesting:-1},{shift:0,type:"text",content:g("start",e.left,e.right)}]}],transform:(t,i,n)=>{const r=t[i].children[n],{content:s}=r,l=p(s,0,e),o=m(t[i].children,n-1);u(l,o),r.content=s.slice(s.indexOf(e.right)+e.right.length);}}],P=e=>({name:"table",tests:[{shift:0,type:"table_close"},{shift:1,type:"paragraph_open"},{shift:2,type:"inline",content:g("only",e.left,e.right)}],transform:(t,i)=>{const n=t[i+2],r=m(t,i),s=p(n.content,0,e);u(s,r),t.splice(i+1,3);}}),T=e=>[{name:"list softbreak",tests:[{shift:-2,type:"list_item_open"},{shift:0,type:"inline",children:[{position:-2,type:"softbreak"},{position:-1,type:"text",content:g("only",e.left,e.right)}]}],transform:(t,i,n)=>{const r=t[i].children[n],s=p(r.content,0,e);let l=i-2;for(;t[l-1]&&t[l-1].type!=="ordered_list_open"&&t[l-1].type!=="bullet_list_open";)l--;u(s,t[l-1]),t[i].children=t[i].children.slice(0,-2);}},{name:"list double softbreak",tests:[{shift:0,type:t=>t==="bullet_list_close"||t==="ordered_list_close"},{shift:1,type:"paragraph_open"},{shift:2,type:"inline",content:g("only",e.left,e.right),children:t=>t.length===1},{shift:3,type:"paragraph_close"}],transform:(t,i)=>{const n=t[i+2],r=p(n.content,0,e),s=m(t,i);u(r,s),t.splice(i+1,3);}},{name:"list item end",tests:[{shift:-2,type:"list_item_open"},{shift:0,type:"inline",children:[{position:-1,type:"text",content:g("end",e.left,e.right)}]}],transform:(t,i,n)=>{const r=t[i].children[n],{content:s}=r,l=p(s,s.lastIndexOf(e.left),e);u(l,t[i-2]);const o=s.slice(0,s.lastIndexOf(e.left));r.content=o[o.length-1]===" "?o.slice(0,-1):o;}}],L=e=>({name:` +{.a} softbreak then curly in start`,tests:[{shift:0,type:"inline",children:[{position:-2,type:"softbreak"},{position:-1,type:"text",content:g("only",e.left,e.right)}]}],transform:(t,i,n)=>{const r=t[i].children[n],s=p(r.content,0,e);let l=i+1;for(;t[l+1]&&t[l+1].nesting===-1;)l++;const o=m(t,l);u(s,o),t[i].children=t[i].children.slice(0,-2);}}),M=e=>({name:"horizontal rule",tests:[{shift:0,type:"paragraph_open"},{shift:1,type:"inline",children:t=>t.length===1,content:t=>new RegExp(`^ {0,3}[-*_]{3,} ?${b(e.left)}[^${b(e.right)}]`).test(t)},{shift:2,type:"paragraph_close"}],transform:(t,i)=>{const n=t[i];n.type="hr",n.tag="hr",n.nesting=0;const{content:r}=t[i+1],s=r.lastIndexOf(e.left),l=p(r,s,e);u(l,n),n.markup=r,t.splice(i+1,2);}}),C=e=>({name:"end of block",tests:[{shift:0,type:"inline",children:[{position:-1,content:g("end",e.left,e.right),type:t=>t!=="code_inline"&&t!=="math_inline"}]}],transform:(t,i,n)=>{const r=t[i].children[n],{content:s}=r,l=p(s,s.lastIndexOf(e.left),e);let o=i+1;for(;t[o+1]&&t[o+1].nesting===-1;)o++;const h=m(t,o);u(l,h);const f=s.slice(0,s.lastIndexOf(e.left));r.content=f[f.length-1]===" "?f.slice(0,-1):f;}}),A=["fence","inline","table","list","hr","softbreak","block"],J=e=>{const t=e.rule===!1?[]:Array.isArray(e.rule)?e.rule.filter(n=>A.includes(n)):A,i=[];return t.includes("fence")&&i.push(D(e)),t.includes("inline")&&i.push(...K(e)),t.includes("list")&&i.push(...T(e)),t.includes("table")&&i.push(P(e)),t.includes("softbreak")&&i.push(L(e)),t.includes("hr")&&i.push(M(e)),t.includes("block")&&i.push(C(e)),i},_=(e,t,i)=>{const n={match:!1,position:null},r=i.shift!==void 0?t+i.shift:i.position;if(i.shift!==void 0&&r<0)return n;const s=w(e,r);if(s===void 0)return n;for(const l of Object.keys(i)){if(l==="shift"||l==="position")continue;if(s[l]===void 0)return n;if(l==="children"&&S$1(i.children)){if(s.children?.length===0)return n;let h;const f=i.children,a=s.children;if(f?.every(c=>c.position!==void 0)){if(h=f.every(c=>_(a,c.position,c).match),h){const c=f[f.length-1]?.position??0;n.position=c>=0?c:a.length+c;}}else for(let c=0;c_(a,c,y).match),h){n.position=c;break}if(h===!1)return n;continue}const o=i[l];switch(typeof o){case"boolean":case"number":case"string":if(s[l]!==o)return n;break;case"function":if(!o(s[l]))return n;break;case"object":if(I(o)){if(o.every(h=>h(s[l]))===!1)return n;break}continue;default:throw new Error(`Unknown type of pattern test (key: ${l}). Test should be of type boolean, number, string, function or array of functions.`)}}return n.match=!0,n},Y=(e,{left:t="{",right:i="}",allowed:n=[],rule:r="all"}={})=>{const s=J({left:t,right:i,allowed:n,rule:r}),l=({tokens:o})=>{for(let h=0;h{const d=_(o,h,y);return d.position!==null&&({position:c}=d),d.match})&&(a.transform(o,h,c),(a.name==="inline attributes"||a.name==="inline nesting 0")&&f--);}};e.core.ruler.before("linkify","attrs",l);}; + + const S=(b,{name:c,marker:p=":",validate:m=a=>a.trim().split(" ",2)[0]===c,openRender:h=(a,o,l,e,s)=>(a[o].attrJoin("class",c),s.renderToken(a,o,l)),closeRender:T=(a,o,l,e,s)=>s.renderToken(a,o,l)}={name:""})=>{const a=p[0],o=p.length,l=(e,s,$,x)=>{let t=e.bMarks[s]+e.tShift[s],i=e.eMarks[s];if(a!==e.src[t])return !1;let n=t+1;for(;n<=i&&p[(n-t)%o]===e.src[n];)n++;const k=Math.floor((n-t)/o);if(k<3)return !1;n-=(n-t)%o;const d=e.src.slice(t,n),u=e.src.slice(n,i);if(!m(u,d))return !1;if(x)return !0;let r=s,M=!1;for(;r<$&&(r++,t=e.bMarks[r]+e.tShift[r],i=e.eMarks[r],!(t=k&&(n-=(n-t)%o,n=e.skipSpaces(n),n>=i)){M=!0;break}}const y=e.parentType,v=e.lineMax;e.parentType="container",e.lineMax=r;const f=e.push(`container_${c}_open`,"div",1);f.markup=d,f.block=!0,f.info=u,f.map=[s,r],e.md.block.tokenize(e,s+1,r);const _=e.push(`container_${c}_close`,"div",-1);return _.markup=e.src.slice(t,n),_.block=!0,e.parentType=y,e.lineMax=v,e.line=r+(M?1:0),!0};b.block.ruler.before("fence",`container_${c}`,l,{alt:["paragraph","reference","blockquote","list"]}),b.renderer.rules[`container_${c}_open`]=h,b.renderer.rules[`container_${c}_close`]=T;}; + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + const plugins = []; + function registerMarkdownPlugin(plugin) { + plugins.push(plugin); + return 'register'; + } + function create(options) { + var _a; + const md = new markdownit(); + for (const plugin of plugins) { + plugin.initializePlugin(md); + } + md.use(Y); + (_a = options === null || options === void 0 ? void 0 : options.classList) === null || _a === void 0 ? void 0 : _a.forEach(name => { + const containerOptions = { name }; + md.use(S, containerOptions); + }); + // Default handler to preserve existing functionality + const originalFence = md.renderer.rules.fence; + // Modified fence renderer to dynamically use handlers + md.renderer.rules.fence = function (tokens, idx, options, env, slf) { + const token = tokens[idx]; + const info = token.info.trim(); + // Check if a handler exists for the block type and use it + const plugin = plugins.filter(p => p.name === info)[0]; + if (plugin && plugin.fence) { + return plugin.fence(token, idx); + } + if (originalFence) { + return originalFence(tokens, idx, options, env, slf); + } + else { + return ''; + } + }; + return md; + } + function definePlugin(md, pluginName) { + md.block.ruler.before('fence', `${pluginName}_block`, function (state, startLine, endLine) { + const start = state.bMarks[startLine] + state.tShift[startLine]; + const max = state.eMarks[startLine]; + if (state.src.slice(start, max).trim() !== '```' + pluginName) { + return false; + } + let nextLine = startLine; + while (nextLine < endLine) { + nextLine++; + if (state.src.slice(state.bMarks[nextLine] + state.tShift[nextLine], state.eMarks[nextLine]).trim() === '```') { + break; + } + } + state.line = nextLine + 1; + const token = state.push('fence', 'code', 0); + token.info = pluginName; + token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true); + token.map = [startLine, state.line]; + return true; + }); + } + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var __awaiter$8 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + var LogLevel; + (function (LogLevel) { + LogLevel[LogLevel["none"] = 0] = "none"; + LogLevel[LogLevel["some"] = 1] = "some"; + LogLevel[LogLevel["all"] = 2] = "all"; + })(LogLevel || (LogLevel = {})); + // Signal Bus to manage shared signals + class SignalBus { + constructor(dataSignalPrefix) { + this.dataSignalPrefix = dataSignalPrefix; + this.logLevel = LogLevel.none; + this.logWatchIds = []; + this.reset(); + } + log(id, message, ...optionalParams) { + if (this.logLevel === LogLevel.none) + return; + if (this.logWatchIds.length > 0 && !this.logWatchIds.includes(id)) + return; + console.log(`[Signal Bus][${id}] ${message}`, ...optionalParams); + } + broadcast(originId, batch) { + return __awaiter$8(this, void 0, void 0, function* () { + //TODO handle multiple broadcasts + //TODO handle circular dependencies + if (this.broadcastingStack.includes(originId)) { + this.log(originId, 'Additional broadcast from', originId, this.broadcastingStack.join(' -> ')); + //return; + } + this.log(originId, 'Broadcasting batch from', originId, batch); + this.broadcastingStack.push(originId); + for (const peerId of this.peerDependencies[originId]) { + const peer = this.peers.find(p => p.id === peerId); + //create a new batch with the signals that the peer is interested in + const peerBatch = {}; + let hasBatch = false; + for (const signalName in batch) { + if (peer.initialSignals.some(s => s.name === signalName) + && batch[signalName].value !== this.signalDeps[signalName].value) { + peerBatch[signalName] = batch[signalName]; + hasBatch = true; + } + } + if (!hasBatch) + continue; + peer.recieveBatch && (yield peer.recieveBatch(peerBatch, originId)); + } + this.broadcastingStack.pop(); + //set current values + for (const signalName in batch) { + const signalDep = this.signalDeps[signalName]; + signalDep.value = batch[signalName].value; + } + if (this.broadcastingStack.length === 0) { + //broadcast complete + for (const peer of this.peers) { + peer.broadcastComplete && (yield peer.broadcastComplete()); + } + } + }); + } + getPriorityPeer(signalName) { + const signalDep = this.signalDeps[signalName]; + if (!signalDep) + return null; + return this.peers.find(p => p.id === signalDep.initialPriorityId); + } + registerPeer(peer) { + this.peers.push(peer); + for (const initialSignal of peer.initialSignals) { + if (!(initialSignal.name in this.signalDeps)) { + //first encounter with this signal + this.signalDeps[initialSignal.name] = { + deps: [peer], + priority: initialSignal.priority, + initialPriorityId: peer.id, + value: initialSignal.value, + isData: initialSignal.isData, + }; + } + else { + //signal exists, add the peer to the deps and check to override the priority + const signalDep = this.signalDeps[initialSignal.name]; + if (!signalDep.deps.includes(peer)) { + signalDep.deps.push(peer); + } + if (initialSignal.priority > signalDep.priority) { + signalDep.priority = initialSignal.priority; + signalDep.initialPriorityId = peer.id; + signalDep.value = initialSignal.value; + signalDep.isData = initialSignal.isData; + } + } + } + } + beginListening() { + //set the initial batch on each peer + this.log('beginListening', 'begin initial batch', this.signalDeps); + for (const peer of this.peers) { + const batch = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + const { value, isData } = signalDep; + batch[signalName] = { value, isData }; + } + peer.recieveBatch && peer.recieveBatch(batch, 'initial'); + } + this.log('beginListening', 'end initial batch'); + //for all signalDeps, compile a list of signals for each peer depending on the signal + const peerSignals = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + if (signalDep.deps.length === 1) + continue; // No need to share signals if only one peer depends on it + for (const peer of signalDep.deps) { + if (!(peer.id in peerSignals)) { + peerSignals[peer.id] = []; + this.peerDependencies[peer.id] = []; + } + peerSignals[peer.id].push({ signalName, isData: signalDep.isData }); + // Add other peers sharing this signal to the dependencies map + for (const otherPeer of signalDep.deps) { + if (otherPeer.id !== peer.id && !this.peerDependencies[peer.id].includes(otherPeer.id)) { + this.peerDependencies[peer.id].push(otherPeer.id); + } + } + } + } + this.log('beginListening', '======= dependencies =========', peerSignals, this.peerDependencies); + // Begin listening and logging the shared dependencies + for (const peer of this.peers) { + const sharedSignals = peerSignals[peer.id]; + if (sharedSignals) { + this.log(peer.id, 'Shared signals:', sharedSignals); + if (this.peerDependencies[peer.id]) { + this.log(peer.id, 'Shared dependencies:', this.peerDependencies[peer.id]); + } + peer.beginListening && peer.beginListening(sharedSignals); + } + else { + this.log(peer.id, 'No shared signals'); + } + } + this.active = true; + } + reset() { + this.signalDeps = {}; + this.active = false; + this.peers = []; + this.broadcastingStack = []; + this.peerDependencies = {}; + } + } + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var __awaiter$7 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + const defaultRendererOptions = { + vegaRenderer: 'canvas', + dataSignalPrefix: 'data-signal:', + classList: ['markdown-block'], + }; + class Renderer { + constructor(element, options) { + this.element = element; + this.options = Object.assign(Object.assign({}, defaultRendererOptions), options); + this.md = create({ classList: this.options.classList }); + this.signalBus = this.options.signalBus || new SignalBus(this.options.dataSignalPrefix); + this.instances = {}; + } + render(markdown, errorHandler) { + return __awaiter$7(this, void 0, void 0, function* () { + if (!errorHandler) { + errorHandler = (error, pluginName, instanceIndex, phase) => { + console.error(`Error in plugin ${pluginName} instance ${instanceIndex} phase ${phase}`, error); + }; + } + //loop through all the destroy handlers and call them. have the key there to help us debug + yield this.destroy(); + const parsedHTML = this.md.render(markdown); + this.element.innerHTML = parsedHTML; + //loop through all the plugins and render them + this.signalBus.log('Renderer', 'rendering DOM'); + const hydrationPromises = []; + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.hydrateComponent) { + //make a new promise that returns IInstances but adds the plugin name + hydrationPromises.push(plugin.hydrateComponent(this, errorHandler).then(instances => { + return { + pluginName: plugin.name, + instances, + }; + })); + } + } + try { + const pluginHydrations = yield Promise.all(hydrationPromises); + for (const hydration of pluginHydrations) { + if (hydration && hydration.instances) { + this.instances[hydration.pluginName] = hydration.instances; + //registration phase + for (const instance of hydration.instances) { + this.signalBus.registerPeer(instance); + } + } + } + this.signalBus.beginListening(); + } + catch (error) { + console.error('Error in rendering plugins', error); + } + }); + } + destroy() { + return __awaiter$7(this, void 0, void 0, function* () { + this.signalBus.reset(); + for (const pluginName of Object.keys(this.instances)) { + const instances = this.instances[pluginName]; + for (const instance of instances) { + instance.destroy && (yield instance.destroy()); + } + } + this.instances = {}; + }); + } + } + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + function sanitizedHTML(tagName, attributes, content) { + // Create a temp element with the specified tag name + const element = document.createElement(tagName); + // Iterate over the attribute list and set each attribute + Object.keys(attributes).forEach(key => { + element.setAttribute(key, attributes[key]); + }); + // Set the textContent to automatically escape the content + element.textContent = content; + // Return the outer HTML of the element + return element.outerHTML; + } + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var __awaiter$6 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + const dropdownPlugin = { + name: 'dropdown', + initializePlugin: (md) => definePlugin(md, 'dropdown'), + fence: (token, idx) => { + const DropdownId = `Dropdown-${idx}`; + return sanitizedHTML('div', { id: DropdownId, class: 'dropdown' }, token.content.trim()); + }, + hydrateComponent: (renderer, errorHandler) => __awaiter$6(void 0, void 0, void 0, function* () { + const dropdownInstances = []; + const containers = renderer.element.querySelectorAll('.dropdown'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) + continue; + try { + const spec = JSON.parse(container.textContent); + const html = `
+
+ +
+
`; + container.innerHTML = html; + const element = container.querySelector('select'); + const dropdownInstance = { id: container.id, spec, element }; + dropdownInstances.push(dropdownInstance); + } + catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'Dropdown', index, 'parse', container); + continue; + } + } + const instances = dropdownInstances.map((dropdownInstance, index) => { + const { element, spec } = dropdownInstance; + const initialSignals = [{ + name: spec.name, + value: spec.value || null, + priority: 1, + isData: false, + }]; + if (spec.dynamicOptions) { + initialSignals.push({ + name: spec.dynamicOptions.dataSignalName, + value: null, + priority: -1, + isData: true, + }); + } + return Object.assign(Object.assign({}, dropdownInstance), { initialSignals, recieveBatch: (batch) => __awaiter$6(void 0, void 0, void 0, function* () { + var _a, _b; + const { dynamicOptions } = spec; + if (dynamicOptions === null || dynamicOptions === void 0 ? void 0 : dynamicOptions.dataSignalName) { + const newData = (_a = batch[dynamicOptions.dataSignalName]) === null || _a === void 0 ? void 0 : _a.value; + if (newData) { + //pluck the field from the data and add options to the select + let hasFieldName = false; + //remove duplicates from the options array + const uniqueOptions = new Set(); + newData.forEach((d) => { + //check if the field exists in the data + if (d.hasOwnProperty(dynamicOptions.fieldName)) { + hasFieldName = true; + uniqueOptions.add(d[dynamicOptions.fieldName]); + } + }); + if (hasFieldName) { + const options = Array.from(uniqueOptions); + const existingSelection = spec.multiple ? Array.from(element.selectedOptions).map(option => option.value) : element.value; + element.innerHTML = getOptions(spec.multiple, options, existingSelection); + if (!spec.multiple) { + element.value = ((_b = batch[spec.name]) === null || _b === void 0 ? void 0 : _b.value) || options[0]; + } + } + else { + //if the field doesn't exist, set the select to the first option + element.innerHTML = ``; + element.value = ''; + } + } + } + if (batch[spec.name]) { + const value = batch[spec.name].value; + if (spec.multiple) { + Array.from(element.options).forEach((option) => { + option.selected = value && value.includes(option.value); + }); + } + else { + element.value = value; + } + } + }), beginListening() { + //wire up handler to send the selected value to the signal bus + element.addEventListener('change', (e) => { + const value = spec.multiple + ? Array.from(e.target.selectedOptions).map(option => option.value) + : e.target.value; + const batch = { + [spec.name]: { + value, + isData: false, + }, + }; + renderer.signalBus.broadcast(dropdownInstance.id, batch); + }); + }, getCurrentSignalValue: () => { + if (spec.multiple) { + return Array.from(element.selectedOptions).map(option => option.value); + } + return element.value; + }, destroy: () => __awaiter$6(void 0, void 0, void 0, function* () { + element.removeEventListener('change', dropdownInstance.element.onchange); + }) }); + }); + return instances; + }) + }; + function getOptions(multiple, options, selected) { + if (!options) { + if (multiple) { + if (Array.isArray(selected)) { + options = selected; + } + else { + if (selected) { + options = [selected]; + } + } + } + else { + if (selected) { + options = [selected]; + } + } + } + if (!options) { + return ''; + } + return options.map((option) => { + let attr = ''; + if (multiple) { + attr = (selected || []).includes(option) ? 'selected' : ''; + } + else { + attr = selected === option ? 'selected' : ''; + } + return ``; + }).join('\n'); + } + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var __awaiter$5 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + var ImageOpacity; + (function (ImageOpacity) { + ImageOpacity["full"] = "1"; + ImageOpacity["loading"] = "0.1"; + ImageOpacity["error"] = "0.5"; + })(ImageOpacity || (ImageOpacity = {})); + const imagePlugin = { + name: 'image', + initializePlugin: (md) => definePlugin(md, 'image'), + fence: (token, idx) => { + const ImageId = `Image-${idx}`; + return sanitizedHTML('div', { id: ImageId, class: 'image' }, token.content.trim()); + }, + hydrateComponent: (renderer, errorHandler) => __awaiter$5(void 0, void 0, void 0, function* () { + const imageInstances = []; + const containers = renderer.element.querySelectorAll('.image'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) + continue; + try { + const spec = JSON.parse(container.textContent); + const element = document.createElement('img'); + const spinner = document.createElement('div'); + spinner.innerHTML = ` + + + + + `; + if (spec.alt) + element.alt = spec.alt; + if (spec.width) + element.width = spec.width; + if (spec.height) + element.height = spec.height; + element.onload = () => { + spinner.style.display = 'none'; + element.style.opacity = ImageOpacity.full; + }; + element.onerror = () => { + spinner.style.display = 'none'; + element.style.opacity = ImageOpacity.error; + errorHandler(new Error('Image failed to load'), 'image', index, 'load', container, element.src); + }; + container.style.position = 'relative'; + spinner.style.position = 'absolute'; + container.innerHTML = ''; + container.appendChild(spinner); + container.appendChild(element); + const imageInstance = { id: container.id, spec, element, spinner }; + imageInstances.push(imageInstance); + } + catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'Image', index, 'parse', container); + } + } + const instances = imageInstances.map((imageInstance, index) => { + const { element, spinner, id, spec } = imageInstance; + return { + id, + initialSignals: [ + { + name: spec.srcSignalName, + value: null, + priority: -1, + isData: false, + } + ], + destroy: () => __awaiter$5(void 0, void 0, void 0, function* () { + if (element) { + element.remove(); + } + if (spinner) { + spinner.remove(); + } + }), + recieveBatch: (batch, from) => __awaiter$5(void 0, void 0, void 0, function* () { + if (spec.srcSignalName in batch) { + const src = batch[spec.srcSignalName].value; + if (src) { + spinner.style.display = ''; + element.src = src.toString(); + element.style.opacity = ImageOpacity.loading; + } + else { + element.src = ''; //TODO placeholder image + spinner.style.display = 'none'; + element.style.opacity = ImageOpacity.full; + } + } + }) + }; + }); + return instances; + }) + }; + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var __awaiter$4 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + function createTemplateFunction(template) { + const parts = template.split(/(%7B%7B.*?%7D%7D)/g).map(part => { + if (part.startsWith('%7B%7B') && part.endsWith('%7D%7D')) { + const key = part.slice(6, -6); // Extract key from %7B%7Bkey%7D%7D + return (batch) => { var _a, _b; return ((_b = (_a = batch[key]) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.toString()) || ''; }; + } + else { + return () => part; // Static part of the template + } + }); + return (batch) => parts.map(fn => fn(batch)).join(''); + } + function handleDynamicUrl(tokens, idx, attrName, elementType) { + const token = tokens[idx]; + const attrValue = token.attrGet(attrName); + if (attrValue && attrValue.includes('%7B%7B')) { + // Ensure token.attrs is initialized + if (!token.attrs) { + token.attrs = []; + } + token.attrSet('data-template-url', attrValue); // Store original template + } + return token; + } + const placeholdersPlugin = { + name: 'placeholders', + initializePlugin: (md) => __awaiter$4(void 0, void 0, void 0, function* () { + // Custom plugin to handle dynamic placeholders + md.use(function (md) { + // Add a custom rule to handle {{...}} placeholders + md.inline.ruler.after('emphasis', 'dynamic_placeholder', function (state, silent) { + let token; + const max = state.posMax; + const start = state.pos; + // Look for double curly braces {{ + if (state.src.charCodeAt(start) !== 0x7B /* { */ || + state.src.charCodeAt(start + 1) !== 0x7B /* { */) { + return false; + } + for (let pos = start + 2; pos < max; pos++) { + if (state.src.charCodeAt(pos) === 0x7D /* } */ && + state.src.charCodeAt(pos + 1) === 0x7D /* } */) { + if (!silent) { + state.pos = start + 2; + state.posMax = pos; + token = state.push('dynamic_placeholder', '', 0); + token.markup = state.src.slice(start, pos + 2); + token.content = state.src.slice(state.pos, state.posMax); + state.pos = pos + 2; + state.posMax = max; + } + return true; + } + } + return false; + }); + // Renderer rule for dynamic placeholders + md.renderer.rules['dynamic_placeholder'] = function (tokens, idx) { + const key = tokens[idx].content.trim(); + return `{${key}}`; + }; + }); + md.renderer.rules['link_open'] = function (tokens, idx, options, env, slf) { + handleDynamicUrl(tokens, idx, 'href'); + return slf.renderToken(tokens, idx, options); + }; + md.renderer.rules['image'] = function (tokens, idx, options, env, slf) { + handleDynamicUrl(tokens, idx, 'src'); + return slf.renderToken(tokens, idx, options); + }; + }), + hydrateComponent: (renderer) => __awaiter$4(void 0, void 0, void 0, function* () { + const templateFunctionMap = new WeakMap(); + const placeholders = renderer.element.querySelectorAll('.dynamic-placeholder'); + const dynamicUrls = renderer.element.querySelectorAll('[data-template-url]'); + const elementsByKeys = new Map(); + // Collect placeholders + for (const placeholder of placeholders) { + const key = placeholder.getAttribute('data-key'); + if (elementsByKeys.has(key)) { + elementsByKeys.get(key).push(placeholder); + } + else { + elementsByKeys.set(key, [placeholder]); + } + } + // Collect dynamic URLs + for (const element of dynamicUrls) { + const templateUrl = element.getAttribute('data-template-url'); + const keys = Array.from(templateUrl.matchAll(/%7B%7B(.*?)%7D%7D/g)).map(match => match[1]); + const templateFunction = createTemplateFunction(templateUrl); + templateFunctionMap.set(element, { templateFunction, batch: {} }); + for (const key of keys) { + if (elementsByKeys.has(key)) { + elementsByKeys.get(key).push(element); + } + else { + elementsByKeys.set(key, [element]); + } + } + } + // Create initial signals + const initialSignals = Array.from(elementsByKeys.keys()).map(name => { + const prioritizedSignal = { + name, + value: null, + priority: -1, + isData: false, + }; + return prioritizedSignal; + }); + const instances = [ + { + id: 'placeholders', + initialSignals, + recieveBatch: (batch) => __awaiter$4(void 0, void 0, void 0, function* () { + var _a; + for (const key of Object.keys(batch)) { + const elements = elementsByKeys.get(key) || []; + for (const element of elements) { + if (element.classList.contains('dynamic-placeholder')) { + // Update placeholder content + const markdownContent = ((_a = batch[key].value) === null || _a === void 0 ? void 0 : _a.toString()) || ''; + const parsedMarkdown = isMarkdownInline(markdownContent) + ? renderer.md.renderInline(markdownContent) + : renderer.md.render(markdownContent); + element.innerHTML = parsedMarkdown; + } + else if (element.hasAttribute('data-template-url')) { + // Update dynamic URL + const templateData = templateFunctionMap.get(element); + if (templateData) { + // Merge the new batch with the stored batch + templateData.batch = Object.assign(Object.assign({}, templateData.batch), batch); + const updatedUrl = templateData.templateFunction(templateData.batch); + if (element.tagName === 'A') { + element.setAttribute('href', updatedUrl); + } + else if (element.tagName === 'IMG') { + element.setAttribute('src', updatedUrl); + } + } + } + } + } + }), + }, + ]; + return instances; + }), + }; + function isMarkdownInline(markdown) { + // Inline markdown typically does not contain newlines + if (!markdown.includes('\n')) { + return true; + } + // Block markdown typically contains newlines and block elements + const blockElements = ['#', '-', '*', '>', '```', '~~~']; + for (const element of blockElements) { + if (markdown.trim().startsWith(element)) { + return false; + } + } + return true; + } + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var __awaiter$3 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + const presetsPlugin = { + name: 'presets', + initializePlugin: (md) => definePlugin(md, 'presets'), + fence: (token, idx) => { + const spec = JSON.parse(token.content.trim()); + const pluginId = `preset-${idx}`; + return sanitizedHTML('div', { id: pluginId, class: 'presets' }, JSON.stringify(spec)); + }, + hydrateComponent: (renderer, errorHandler) => __awaiter$3(void 0, void 0, void 0, function* () { + const presetsInstances = []; + const containers = renderer.element.querySelectorAll('.presets'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) + continue; + const id = `presets${index}`; + let presets; + try { + presets = JSON.parse(container.textContent); + } + catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'presets', index, 'parse', container); + continue; + } + if (!Array.isArray(presets)) { + container.innerHTML = '
Expected an array of presets
'; + continue; + } + //clear the container + container.innerHTML = ''; + const ul = document.createElement('ul'); + const presetsInstance = { id, presets, element: ul }; + container.appendChild(ul); + for (const preset of presets) { + //make a button for each preset + const li = document.createElement('li'); + if (!preset.name || !preset.state) { + const span = document.createElement('span'); + span.className = 'error'; + span.textContent = 'Each preset must have a name and state'; + li.appendChild(span); + } + else { + const button = document.createElement('button'); + button.textContent = preset.name; + button.onclick = () => { + const batch = {}; + for (const [signalName, value] of Object.entries(preset.state)) { + batch[signalName] = { value, isData: false }; + } + renderer.signalBus.broadcast(id, batch); + }; + li.appendChild(button); + li.appendChild(document.createTextNode('\u00A0')); + if (preset.description) { + button.title = preset.description; + } + } + ul.appendChild(li); + } + presetsInstances.push(presetsInstance); + } + const instances = presetsInstances.map((presetsInstance, index) => { + const initialSignals = presetsInstance.presets.flatMap(preset => { + return Object.keys(preset.state).map(signalName => { + return { + name: signalName, + value: null, + priority: -1, + isData: undefined, // we do not know if it is data or not + }; + }); + }); + return Object.assign(Object.assign({}, presetsInstance), { initialSignals, broadcastComplete: () => __awaiter$3(void 0, void 0, void 0, function* () { + //populate state from the renderer.signalBus.signalDeps + const state = {}; + for (const signalName of Object.keys(renderer.signalBus.signalDeps)) { + state[signalName] = renderer.signalBus.signalDeps[signalName].value; + } + // highlight any presets that have the same signals and values as the current state + setAllPresetsActiveState(presetsInstance, state); + }) }); + }); + return instances; + }), + }; + function isPresetActive(preset, state) { + for (const [signalName, value] of Object.entries(preset.state)) { + if (state[signalName] !== value) { + return false; + } + } + return true; + } + function setAllPresetsActiveState(presetsInstance, state) { + for (const [presetIndex, preset] of presetsInstance.presets.entries()) { + const { classList } = presetsInstance.element.children[presetIndex]; + if (isPresetActive(preset, state)) { + classList.add('active'); + } + else { + classList.remove('active'); + } + } + } + + const dataNameSelectedSuffix = '-selected'; + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var __awaiter$2 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + const tabulatorPlugin = { + name: 'tabulator', + initializePlugin: (md) => definePlugin(md, 'tabulator'), + fence: (token, idx) => { + const tabulatorId = `tabulator-${idx}`; + return sanitizedHTML('div', { id: tabulatorId, class: 'tabulator', style: 'box-sizing: border-box;' }, token.content.trim()); + }, + hydrateComponent: (renderer, errorHandler) => __awaiter$2(void 0, void 0, void 0, function* () { + const tabulatorInstances = []; + const containers = renderer.element.querySelectorAll('.tabulator'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) + continue; + if (!window.Tabulator) { + errorHandler(new Error('Tabulator not found'), 'tabulator', index, 'init', container); + continue; + } + try { + const spec = JSON.parse(container.textContent); + let options = { + autoColumns: true, + layout: 'fitColumns', + maxHeight: '200px', + }; + //see if default options is an object with no properties + if (spec.options && Object.keys(spec.options).length > 0) { + options = spec.options; + } + const table = new window.Tabulator(container, options); + const tabulatorInstance = { id: container.id, spec, table }; + tabulatorInstances.push(tabulatorInstance); + } + catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'tabulator', index, 'parse', container); + continue; + } + } + const instances = tabulatorInstances.map((tabulatorInstance, index) => { + const initialSignals = [{ + name: tabulatorInstance.spec.dataSignalName, + value: null, + priority: -1, + isData: true, + }]; + if (tabulatorInstance.spec.options.selectableRows) { + initialSignals.push({ + name: `${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`, + value: [], + priority: -1, + isData: true, + }); + } + return Object.assign(Object.assign({}, tabulatorInstance), { initialSignals, recieveBatch: (batch) => __awaiter$2(void 0, void 0, void 0, function* () { + const newData = batch[tabulatorInstance.spec.dataSignalName].value; + if (newData) { + tabulatorInstance.table.setData(newData); + } + }), beginListening(sharedSignals) { + if (tabulatorInstance.spec.options.selectableRows) { + for (const { isData, signalName } of sharedSignals) { + if (isData) { + const matchData = signalName === `${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`; + if (matchData) { + tabulatorInstance.table.on('rowSelectionChanged', (e, rows) => { + const selectedData = tabulatorInstance.table.getSelectedData(); + const batch = { + [`${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`]: { + value: selectedData, + isData: true, + }, + }; + renderer.signalBus.log(tabulatorInstance.id, 'sending batch', batch); + renderer.signalBus.broadcast(tabulatorInstance.id, batch); + }); + } + } + } + } + }, + getCurrentSignalValue() { + return tabulatorInstance.table.getSelectedData(); + }, destroy: () => __awaiter$2(void 0, void 0, void 0, function* () { + tabulatorInstance.table.destroy(); + }) }); + }); + return instances; + }) + }; + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + const vegaLitePlugin = { + name: 'vega-lite', + initializePlugin: (md) => definePlugin(md, 'vega-lite'), + fence: (token, idx) => { + const vegaLiteId = `vega-lite-${idx}`; + return sanitizedHTML('div', { id: vegaLiteId, class: 'vega-chart' }, token.content.trim()); + }, + }; + + var __awaiter$1 = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + function resolveSpec(textContent) { + return __awaiter$1(this, void 0, void 0, function* () { + try { + const either = JSON.parse(textContent); + if (typeof either === 'object') { + return resolveToVega(either); + } + else { + return { error: new Error(`Spec must be either a JSON object or a string url, found type ${typeof either}`) }; + } + } + catch (error) { + //see if it is a url, then await to load the json for a spec + if (textContent.startsWith('http://') || textContent.startsWith('https://') || textContent.startsWith('//')) { + try { + const response = yield fetch(textContent); + const either = yield response.json(); + if (typeof either === 'object') { + return resolveToVega(either); + } + else { + return { error: new Error(`Expected a JSON object, found type ${typeof either}`) }; + } + } + catch (error) { + return { error }; + } + } + else { + return { error: new Error('Spec string must be a url') }; + } + } + }); + } + function resolveToVega(either) { + if ('$schema' in either && typeof either.$schema === 'string') { + if (either.$schema.includes('vega-lite')) { + //compile to vega + try { + const runtime = vegaLite.compile(either); + const { spec } = runtime; + return { spec }; + } + catch (error) { + //did not compile + return { error }; + } + } + else if (either.$schema.includes('vega')) { + return { spec: either }; + } + else { + return { error: new Error('$schema property must be a string with vega or vega-lite version.') }; + } + } + else { + return { error: new Error('Missing $schema property, must be a string with vega or vega-lite version.') }; + } + } + + function urlParam(urlParamName, value) { + if (value === undefined || value === null) + return ''; + if (Array.isArray(value)) { + return value.map(vn => `${urlParamName}[]=${encodeURIComponent(vn)}`).join('&'); + } + else { + return `${urlParamName}=${encodeURIComponent(value)}`; + } + } + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; + const ignoredSignals = ['width', 'height', 'padding', 'autosize', 'background', 'style', 'parent', 'datum', 'item', 'event', 'cursor']; + const vegaPlugin = { + name: 'vega', + initializePlugin: (md) => definePlugin(md, 'vega'), + fence: (token, idx) => { + const vegaId = `vega-${idx}`; + return sanitizedHTML('div', { id: vegaId, class: 'vega-chart' }, token.content.trim()); + }, + hydrateComponent: (renderer, errorHandler) => __awaiter(void 0, void 0, void 0, function* () { + const vegaInstances = []; + const containers = renderer.element.querySelectorAll('.vega-chart'); + const specInits = []; + for (const [index, container] of containers.entries()) { + const specInit = yield createSpecInit(container, index, renderer, errorHandler); + if (specInit) { + specInits.push(specInit); + } + } + prioritizeSignalValues(specInits); + for (const specInit of specInits) { + const vegaInstance = yield createVegaInstance(specInit, renderer, errorHandler); + if (vegaInstance) { + vegaInstances.push(vegaInstance); + } + } + //make a single array of all the initialSignals that are marked isData + const dataSignals = vegaInstances.map(vegaInstance => vegaInstance.initialSignals.filter(signal => signal.isData)).flat(); + //spin through all instances to see if its spec has data that matches with dataSignals + for (const vegaInstance of vegaInstances) { + if (!vegaInstance.spec.data) + continue; + for (const data of vegaInstance.spec.data) { + //find a matching data signal + const dataSignal = dataSignals.find(signal => (signal.name === data.name) //exact match + || + (`${signal.name}${dataNameSelectedSuffix}` === data.name) //match a selection from Tabulator + ); + if (dataSignal) { + //if we find a match, add it to our initialSignals + vegaInstance.initialSignals.push({ + name: data.name, + value: data.values, + priority: data.values ? 1 : 0, + isData: true, + }); + } + } + } + const instances = vegaInstances.map((vegaInstance) => { + const { spec, view, initialSignals } = vegaInstance; + const startBatch = (from) => { + if (!vegaInstance.batch) { + renderer.signalBus.log(vegaInstance.id, 'starting batch', from); + vegaInstance.batch = {}; + view.runAfter(() => { + const { batch } = vegaInstance; + vegaInstance.batch = undefined; + renderer.signalBus.log(vegaInstance.id, 'sending batch', batch); + renderer.signalBus.broadcast(vegaInstance.id, batch); + }); + } + }; + return Object.assign(Object.assign({}, vegaInstance), { initialSignals, recieveBatch: (batch, from) => __awaiter(void 0, void 0, void 0, function* () { + renderer.signalBus.log(vegaInstance.id, 'recieved batch', batch, from); + return new Promise(resolve => { + view.runAfter(() => __awaiter(void 0, void 0, void 0, function* () { + if (recieveBatch(batch, renderer, vegaInstance)) { + renderer.signalBus.log(vegaInstance.id, 'running after _pulse, changes from', from); + vegaInstance.needToRun = true; + } + else { + renderer.signalBus.log(vegaInstance.id, 'no changes'); + } + renderer.signalBus.log(vegaInstance.id, 'running view after _pulse finished'); + resolve(); + })); + }); + }), broadcastComplete: () => __awaiter(void 0, void 0, void 0, function* () { + renderer.signalBus.log(vegaInstance.id, 'broadcastComplete'); + if (vegaInstance.needToRun) { + view.runAfter(() => { + view.runAsync(); //do not await, since we are already in a runAfter + vegaInstance.needToRun = false; + renderer.signalBus.log(vegaInstance.id, 'running view after broadcastComplete'); + }); + } + }), beginListening: (sharedSignals) => { + var _a, _b; + for (const { isData, signalName } of sharedSignals) { + if (ignoredSignals.includes(signalName)) + return; + if (isData) { + const matchData = (_a = spec.data) === null || _a === void 0 ? void 0 : _a.find(data => data.name === signalName); + if (matchData && vegaInstance.dataSignals.includes(matchData.name)) { + renderer.signalBus.log(vegaInstance.id, 'listening to data', signalName); + view.addDataListener(signalName, (name, value) => __awaiter(void 0, void 0, void 0, function* () { + startBatch(`data:${signalName}`); + vegaInstance.batch[name] = { value, isData }; + })); + } + } + const matchSignal = (_b = spec.signals) === null || _b === void 0 ? void 0 : _b.find(signal => signal.name === signalName); + if (matchSignal) { + //only listen to signals that are change sources + const isChangeSource = matchSignal.on || // event streams + matchSignal.bind || // ui elements + matchSignal.update // calculations + ; + if (isChangeSource) { + renderer.signalBus.log(vegaInstance.id, 'listening to signal', signalName); + view.addSignalListener(signalName, (name, value) => __awaiter(void 0, void 0, void 0, function* () { + startBatch(`signal:${signalName}`); + vegaInstance.batch[name] = { value, isData }; + })); + } + } + } + }, getCurrentSignalValue: (signalName) => { + var _a; + const matchSignal = (_a = spec.signals) === null || _a === void 0 ? void 0 : _a.find(signal => signal.name === signalName); + if (matchSignal) { + return view.signal(signalName); + } + else { + return undefined; + } + }, destroy: () => __awaiter(void 0, void 0, void 0, function* () { + vegaInstance.view.finalize(); + }) }); + }); + return instances; + }), + }; + function recieveBatch(batch, renderer, vegaInstance) { + var _a, _b; + const { spec, view } = vegaInstance; + const doLog = renderer.signalBus.logLevel === LogLevel.all; + doLog && renderer.signalBus.log(vegaInstance.id, 'recieveBatch', batch); + let hasAnyChange = false; + for (const signalName in batch) { + const batchItem = batch[signalName]; + if (ignoredSignals.includes(signalName)) { + doLog && renderer.signalBus.log(vegaInstance.id, 'ignoring reverved signal name', signalName, batchItem.value); + continue; + } + if (batchItem.isData) { + let logReason; + if (!batchItem.value) { + logReason = 'not updating data, no value'; + } + else { + const matchData = (_a = spec.data) === null || _a === void 0 ? void 0 : _a.find(data => data.name === signalName); + if (!matchData) { + logReason = 'not updating data, no match'; + } + else { + logReason = 'updating data'; + view.change(signalName, vega.changeset().remove(() => true).insert(batchItem.value)); + hasAnyChange = true; + } + } + doLog && renderer.signalBus.log(vegaInstance.id, `(isData) ${logReason}`, signalName, batchItem.value); + } + let logReason = ''; + const matchSignal = (_b = spec.signals) === null || _b === void 0 ? void 0 : _b.find(signal => signal.name === signalName); + if (!matchSignal) { + logReason = 'not updating signal, no match'; + } + else { + if (matchSignal.update) { + logReason = 'not updating signal, it is a calculation'; + } + else { + if (isSignalDataBridge(matchSignal)) { + logReason = 'not updating signal, data bridge'; + } + else { + const oldValue = view.signal(signalName); + if (oldValue === batchItem.value) { + logReason = 'not updating signal, same value'; + } + else { + logReason = 'updating signal'; + view.signal(signalName, batchItem.value); + hasAnyChange = true; + } + } + } + } + doLog && renderer.signalBus.log(vegaInstance.id, logReason, signalName, batchItem.value); + } + return hasAnyChange; + } + function createSpecInit(container, index, renderer, errorHandler) { + return __awaiter(this, void 0, void 0, function* () { + var _a; + if (!container.textContent) { + container.innerHTML = '
Expected a spec object or a url
'; + return; + } + let result; + try { + result = yield resolveSpec(container.textContent); + } + catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'vega', index, 'resolve', container); + return; + } + if (result.error) { + container.innerHTML = `
${result.error.toString()}
`; + errorHandler(result.error, 'vega', index, 'resolve', container); + return; + } + if (!result.spec) { + container.innerHTML = '
Expected a spec object
'; + return; + } + const { spec } = result; + const initialSignals = ((_a = spec.signals) === null || _a === void 0 ? void 0 : _a.map((signal) => { + if (ignoredSignals.includes(signal.name)) + return; + let isData = isSignalDataBridge(signal); + //support legacy dataPrefix + if (signal.name.startsWith(renderer.options.dataSignalPrefix)) { + isData = true; + } + return { + name: signal.name, + value: signal.value, + priority: signal.bind ? 1 : 0, + isData, + }; + }).filter(Boolean)) || []; + const specInit = { container, index, initialSignals, spec }; + return specInit; + }); + } + function createVegaInstance(specInit, renderer, errorHandler) { + return __awaiter(this, void 0, void 0, function* () { + const { container, index, initialSignals, spec } = specInit; + const id = `vega-${index}`; + let runtime; + let view; + try { + runtime = vega.parse(spec); + } + catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'vega', index, 'parse', container); + return; + } + try { + view = new vega.View(runtime, { + container, + renderer: renderer.options.vegaRenderer, + logger: new VegaLogger(error => { + errorHandler(error, 'vega', index, 'view', container); + }), + }); + view.run(); + //fix up initial signals + for (const signal of initialSignals) { + if (signal.isData) + continue; //skip data signals + const currentValue = view.signal(signal.name); + if (currentValue !== signal.value) { + renderer.signalBus.log(id, 're-setting initial signal', signal.name, signal.value, currentValue); + signal.value = currentValue; + } + } + } + catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'vega', index, 'view', container); + return; + } + //make a dataSignals array that is made of all the signals that are marked as isData, where the name is in the spec data + const dataSignals = initialSignals.filter(signal => { var _a; return signal.isData && ((_a = spec.data) === null || _a === void 0 ? void 0 : _a.some(data => data.name === signal.name)); }).map(signal => signal.name); + const instance = Object.assign(Object.assign({}, specInit), { view, id, dataSignals }); + return instance; + }); + } + function isSignalDataBridge(signal) { + return signal.update === `data('${signal.name}')`; + } + function prioritizeSignalValues(specInits) { + var _a; + const highPrioritySignals = specInits.map(specInit => specInit.initialSignals.filter(signal => signal.priority > 0)).flat(); + for (const specInit of specInits) { + for (const prioritySignal of highPrioritySignals) { + const matchSignal = (_a = specInit.spec.signals) === null || _a === void 0 ? void 0 : _a.find(signal => signal.name === prioritySignal.name); + if (matchSignal && matchSignal.value !== undefined && matchSignal.value !== prioritySignal.value) { + matchSignal.value = prioritySignal.value; + } + } + } + } + vega.expressionFunction('urlParam', urlParam); + class VegaLogger { + constructor(errorHandler) { + this.errorHandler = errorHandler; + this.logLevel = 0; + this.error = this.error.bind(this); + this.warn = this.warn.bind(this); + this.info = this.info.bind(this); + this.debug = this.debug.bind(this); + } + level(level) { + if (level === undefined) { + return this.logLevel; + } + this.logLevel = level; + return this; + } + error(...args) { + if (this.errorHandler) { + this.errorHandler(args[0]); + } + if (this.logLevel >= 1) { + console.error(...args); + } + return this; + } + warn(...args) { + if (this.logLevel >= 2) { + console.warn(...args); + } + return this; + } + info(...args) { + if (this.logLevel >= 3) { + console.info(...args); + } + return this; + } + debug(...args) { + if (this.logLevel >= 4) { + console.debug(...args); + } + return this; + } + } + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + function registerNativePlugins() { + registerMarkdownPlugin(dropdownPlugin); + registerMarkdownPlugin(imagePlugin); + registerMarkdownPlugin(placeholdersPlugin); + registerMarkdownPlugin(presetsPlugin); + registerMarkdownPlugin(tabulatorPlugin); + registerMarkdownPlugin(vegaLitePlugin); + registerMarkdownPlugin(vegaPlugin); + } + + var interfaces = /*#__PURE__*/Object.freeze({ + __proto__: null + }); + + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + registerNativePlugins(); + + exports.Plugins = interfaces; + exports.Renderer = Renderer; + exports.definePlugin = definePlugin; + exports.plugins = plugins; + exports.registerMarkdownPlugin = registerMarkdownPlugin; + +})); diff --git a/package-lock.json b/package-lock.json index d6b2f808e..6e768c9f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@deck.gl/layers": "8.3.7", "@fluentui/react": "^8", "@luma.gl/core": "8.3.1", + "@mdit/plugin-attrs": "^0.22.0", + "@mdit/plugin-container": "^0.22.0", "@parcel/transformer-sass": "^2.15.2", "@rollup/plugin-buble": "^1.0.3", "@rollup/plugin-commonjs": "^28.0.0", @@ -25,8 +27,10 @@ "@types/d3-color": "^3.1.0", "@types/d3-ease": "^3.0.0", "@types/d3-format": "^1.3.1", + "@types/markdown-it": "^14.1.2", "@types/react": ">=16.8.0 <18.0.0", "@types/react-dom": ">=16.8.0 <18.0.0", + "@types/tabulator-tables": "^6.2.6", "@types/temp-write": "^3.3.0", "@types/vscode": "^1.53.0", "@typescript-eslint/eslint-plugin": "^8.33.1", @@ -46,6 +50,7 @@ "inuitcss": "^6.0.0", "is-plain-object": "^5.0.0", "just-compare": "^1.3.0", + "markdown-it": "^14.1.0", "marked": "^4.0.10", "mdast-util-toc": "^3.0.1", "mocha": "^10.1.0", @@ -62,6 +67,7 @@ "regenerator-runtime": "^0.13.3", "rimraf": "^2.6.2", "rollup": "^4.22.5", + "rollup-plugin-postcss": "^4.0.2", "sass": "^1.32.4", "streamlit-component-lib": "^2.0.0", "temp-write": "^3.4.0", @@ -71,6 +77,7 @@ "unified": "^7.0.2", "vega": "^5.32.0", "vega-datasets": "2.8.1", + "vega-lite": "^5.23.0", "vega-typings": "^1.5.0", "vscode-test": "^1.3.0", "webpack": "^5.52.0", @@ -89,7 +96,7 @@ } }, "extensions/azdata-sanddance": { - "version": "4.1.1", + "version": "4.2.0", "license": "MIT", "engines": { "azdata": "*", @@ -105,7 +112,7 @@ "license": "MIT" }, "extensions/vscode-sanddance": { - "version": "4.1.0", + "version": "4.2.0", "license": "MIT", "engines": { "vscode": "^1.53.0" @@ -1155,6 +1162,67 @@ "gl-matrix": "^3.4.0" } }, + "node_modules/@mdit/helper": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@mdit/helper/-/helper-0.22.0.tgz", + "integrity": "sha512-2iJYa2PW59VhZgUYZKB88eYsq7Rn20gZbpe+Pnz3bRH/8goMA1YfbRM6oMhKSMX5C8IaK66IGutRJkHrRHhJEA==", + "dev": true, + "dependencies": { + "@types/markdown-it": "^14.1.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "markdown-it": "^14.1.0" + }, + "peerDependenciesMeta": { + "markdown-it": { + "optional": true + } + } + }, + "node_modules/@mdit/plugin-attrs": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@mdit/plugin-attrs/-/plugin-attrs-0.22.0.tgz", + "integrity": "sha512-2lBb6tYDIEhExl0oC+lsi0ldy4zj/K2Nt/VqPZoUdqZVRh3+8uspRd687iZnIT7pL1q1Y3bKqFmyxvhyBtyrmA==", + "dev": true, + "dependencies": { + "@mdit/helper": "0.22.0", + "@types/markdown-it": "^14.1.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "markdown-it": "^14.1.0" + }, + "peerDependenciesMeta": { + "markdown-it": { + "optional": true + } + } + }, + "node_modules/@mdit/plugin-container": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@mdit/plugin-container/-/plugin-container-0.22.0.tgz", + "integrity": "sha512-m/wpPPfNMYmRz6DJi0JBOVe/A+MtMJVFlIjCvxR7kF2bA4WuJdUn9GFy95DDMegp4bpQMUwOwDoFsxAuPGu5Vw==", + "dev": true, + "dependencies": { + "@types/markdown-it": "^14.1.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "markdown-it": "^14.1.0" + }, + "peerDependenciesMeta": { + "markdown-it": { + "optional": true + } + } + }, "node_modules/@microsoft/load-themed-styles": { "version": "1.10.295", "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", @@ -1272,6 +1340,14 @@ "resolved": "packages/fluentui-react-cdn-typings", "link": true }, + "node_modules/@msrvida/mdvega": { + "resolved": "packages/mdvega", + "link": true + }, + "node_modules/@msrvida/mdvega-sanddance": { + "resolved": "packages/mdvega-sanddance", + "link": true + }, "node_modules/@msrvida/sanddance": { "resolved": "packages/sanddance", "link": true @@ -3633,6 +3709,15 @@ "node": ">= 6" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@types/azdata": { "version": "1.46.6", "resolved": "https://registry.npmjs.org/@types/azdata/-/azdata-1.46.6.tgz", @@ -3838,6 +3923,28 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -3958,6 +4065,12 @@ "@types/node": "*" } }, + "node_modules/@types/tabulator-tables": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/@types/tabulator-tables/-/tabulator-tables-6.2.6.tgz", + "integrity": "sha512-A+2VrqDluI6hNw5dQl1Z7b8pjQfAE62+3Kj0cFfenWzj0T0ewMicPrpPINHL7ASqz9u9FTDn1Mz1Ige2tF4Wlw==", + "dev": true + }, "node_modules/@types/temp-write": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@types/temp-write/-/temp-write-3.3.0.tgz", @@ -4582,6 +4695,15 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/@vscode/vsce/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/@vscode/vsce/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -4621,6 +4743,37 @@ "node": ">=4" } }, + "node_modules/@vscode/vsce/node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/@vscode/vsce/node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/@vscode/vsce/node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, "node_modules/@vscode/vsce/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4633,6 +4786,12 @@ "node": ">=4" } }, + "node_modules/@vscode/vsce/node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6001,6 +6160,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001721", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", @@ -6266,6 +6437,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -6472,6 +6649,24 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/concat-with-sourcemaps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -6637,6 +6832,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, "node_modules/css-loader": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", @@ -6688,6 +6895,28 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -6712,6 +6941,94 @@ "node": ">=4" } }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -8652,6 +8969,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", + "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", + "dev": true, + "dependencies": { + "loader-utils": "^3.2.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -9268,6 +9594,12 @@ "node": ">=0.10.0" } }, + "node_modules/icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", + "dev": true + }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -9334,6 +9666,18 @@ "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "dev": true }, + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "dependencies": { + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9350,6 +9694,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-from/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -10196,6 +10561,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -10731,13 +11102,22 @@ "node": ">=8" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, "dependencies": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, "node_modules/listenercount": { @@ -10823,6 +11203,15 @@ "node": ">=6.11.5" } }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -10903,6 +11292,12 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10982,28 +11377,20 @@ } }, "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, "dependencies": { "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/marked": { @@ -11069,10 +11456,16 @@ "unist-util-visit": "^1.1.0" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true }, "node_modules/media-typer": { @@ -11753,10 +12146,22 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "dev": true, "dependencies": { "ansi-styles": "^3.2.1", @@ -12158,6 +12563,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -12188,6 +12602,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", @@ -12205,6 +12635,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -12611,6 +13053,286 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-calc/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dev": true, + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz", + "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==", + "dev": true, + "dependencies": { + "generic-names": "^4.0.0", + "icss-replace-symbols": "^1.1.0", + "lodash.camelcase": "^4.3.0", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "string-hash": "^1.1.1" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/postcss-modules-extract-imports": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", @@ -12640,40 +13362,265 @@ "postcss": "^8.1.0" } }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dev": true, + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dev": true, + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", "dev": true, "dependencies": { - "postcss-selector-parser": "^7.0.0" + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": "^10 || ^12 || >=14.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "postcss": "^8.2.15" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", "dev": true, "dependencies": { - "icss-utils": "^5.0.0" + "postcss-selector-parser": "^6.0.5" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": "^10 || ^12 || >=14.0" }, "peerDependencies": { - "postcss": "^8.1.0" + "postcss": "^8.2.15" } }, - "node_modules/postcss-selector-parser": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", - "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "node_modules/postcss-unique-selectors/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -12991,6 +13938,15 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/promise.series": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", + "integrity": "sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -13070,6 +14026,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", @@ -13635,6 +14600,60 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-postcss": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz", + "integrity": "sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "concat-with-sourcemaps": "^1.1.0", + "cssnano": "^5.0.1", + "import-cwd": "^3.0.0", + "p-queue": "^6.6.2", + "pify": "^5.0.0", + "postcss-load-config": "^3.0.0", + "postcss-modules": "^4.0.0", + "promise.series": "^0.2.0", + "resolve": "^1.19.0", + "rollup-pluginutils": "^2.8.2", + "safe-identifier": "^0.4.2", + "style-inject": "^0.3.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "8.x" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -13714,6 +14733,12 @@ } ] }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", + "dev": true + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -14414,6 +15439,13 @@ "node": ">= 6" } }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -14556,6 +15588,12 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", + "dev": true + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -14778,6 +15816,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-inject": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", + "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", + "dev": true + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/stylehacks/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14807,6 +15880,104 @@ "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==" }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/svgo/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/table-layout": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", @@ -15441,9 +16612,9 @@ } }, "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true }, "node_modules/unbox-primitive": { @@ -15945,6 +17116,120 @@ "vega-util": "^1.17.3" } }, + "node_modules/vega-lite": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.23.0.tgz", + "integrity": "sha512-l4J6+AWE3DIjvovEoHl2LdtCUkfm4zs8Xxx7INwZEAv+XVb6kR6vIN1gt3t2gN2gs/y4DYTs/RPoTeYAuEg6mA==", + "dev": true, + "dependencies": { + "json-stringify-pretty-compact": "~4.0.0", + "tslib": "~2.8.1", + "vega-event-selector": "~3.0.1", + "vega-expression": "~5.1.1", + "vega-util": "~1.17.2", + "yargs": "~17.7.2" + }, + "bin": { + "vl2pdf": "bin/vl2pdf", + "vl2png": "bin/vl2png", + "vl2svg": "bin/vl2svg", + "vl2vg": "bin/vl2vg" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "vega": "^5.24.0" + } + }, + "node_modules/vega-lite/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/vega-lite/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/vega-lite/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vega-lite/node_modules/vega-expression": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.1.2.tgz", + "integrity": "sha512-fFeDTh4UtOxlZWL54jf1ZqJHinyerWq/ROiqrQxqLkNJRJ86RmxYTgXwt65UoZ/l4VUv9eAd2qoJeDEf610Umw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "vega-util": "^1.17.3" + } + }, + "node_modules/vega-lite/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/vega-lite/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/vega-lite/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/vega-loader": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-4.5.3.tgz", @@ -17156,6 +18441,15 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -17287,11 +18581,12 @@ }, "packages/fluentui-icons": { "name": "@msrvida/fluentui-icons", - "version": "1.0.0" + "version": "1.0.2", + "license": "MIT" }, "packages/fluentui-react-cdn-typings": { "name": "@msrvida/fluentui-react-cdn-typings", - "version": "2.0.1", + "version": "2.1.0", "license": "MIT", "dependencies": { "@fluentui/react": "^8" @@ -17300,6 +18595,25 @@ "@types/react": ">=16.8.0 <18.0.0" } }, + "packages/mdvega": { + "name": "@msrvida/mdvega", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@mdit/plugin-attrs": "^0.22.0", + "@mdit/plugin-container": "^0.22.0", + "@types/markdown-it": "^14.1.2", + "@types/tabulator-tables": "^6.2.6", + "vega-typings": "~1.5.0" + } + }, + "packages/mdvega-sanddance": { + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@msrvida/mdvega": "^1" + } + }, "packages/powerbi": { "dependencies": { "@babel/runtime": "^7.26.10", @@ -17345,7 +18659,7 @@ }, "packages/sanddance-embed": { "name": "@msrvida/sanddance-embed", - "version": "4.4.6", + "version": "4.5.0", "license": "MIT", "dependencies": { "@msrvida/sanddance-explorer": "^4" @@ -17353,7 +18667,7 @@ }, "packages/sanddance-explorer": { "name": "@msrvida/sanddance-explorer", - "version": "4.1.7", + "version": "4.1.8", "license": "MIT", "dependencies": { "@msrvida/chart-recommender": "^3", diff --git a/package.json b/package.json index 3d50e156d..3699566b3 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,8 @@ "@deck.gl/layers": "8.3.7", "@fluentui/react": "^8", "@luma.gl/core": "8.3.1", + "@mdit/plugin-attrs": "^0.22.0", + "@mdit/plugin-container": "^0.22.0", "@parcel/transformer-sass": "^2.15.2", "@rollup/plugin-buble": "^1.0.3", "@rollup/plugin-commonjs": "^28.0.0", @@ -69,8 +71,10 @@ "@types/d3-color": "^3.1.0", "@types/d3-ease": "^3.0.0", "@types/d3-format": "^1.3.1", + "@types/markdown-it": "^14.1.2", "@types/react": ">=16.8.0 <18.0.0", "@types/react-dom": ">=16.8.0 <18.0.0", + "@types/tabulator-tables": "^6.2.6", "@types/temp-write": "^3.3.0", "@types/vscode": "^1.53.0", "@typescript-eslint/eslint-plugin": "^8.33.1", @@ -90,6 +94,7 @@ "inuitcss": "^6.0.0", "is-plain-object": "^5.0.0", "just-compare": "^1.3.0", + "markdown-it": "^14.1.0", "marked": "^4.0.10", "mdast-util-toc": "^3.0.1", "mocha": "^10.1.0", @@ -106,6 +111,7 @@ "regenerator-runtime": "^0.13.3", "rimraf": "^2.6.2", "rollup": "^4.22.5", + "rollup-plugin-postcss": "^4.0.2", "sass": "^1.32.4", "streamlit-component-lib": "^2.0.0", "temp-write": "^3.4.0", @@ -115,6 +121,7 @@ "unified": "^7.0.2", "vega": "^5.32.0", "vega-datasets": "2.8.1", + "vega-lite": "^5.23.0", "vega-typings": "^1.5.0", "vscode-test": "^1.3.0", "webpack": "^5.52.0", diff --git a/packages/mdvega-sanddance/LICENSE b/packages/mdvega-sanddance/LICENSE new file mode 100644 index 000000000..21071075c --- /dev/null +++ b/packages/mdvega-sanddance/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + 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 diff --git a/packages/mdvega-sanddance/package.json b/packages/mdvega-sanddance/package.json new file mode 100644 index 000000000..2c179faf7 --- /dev/null +++ b/packages/mdvega-sanddance/package.json @@ -0,0 +1,24 @@ +{ + "name": "@msrvida/mdvega-sanddance", + "version": "1.0.0", + "description": "SandDance plugin for mdvega", + "main": "dist/es6/index.js", + "types": "dist/es6/index.d.ts", + "devDependencies": { + "@msrvida/mdvega": "^1" + }, + "scripts": { + "deploy": "node ./scripts/deploy.js", + "watch-typescript": "tsc -p . -w", + "build-typescript": "tsc -p .", + "bundle": "rollup -c", + "build:02": "npm run build-typescript && npm run bundle" + }, + "keywords": [ + "markdown", + "vega", + "sanddance" + ], + "author": "Dan Marshall", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/mdvega-sanddance/rollup.config.mjs b/packages/mdvega-sanddance/rollup.config.mjs new file mode 100644 index 000000000..62f762178 --- /dev/null +++ b/packages/mdvega-sanddance/rollup.config.mjs @@ -0,0 +1,27 @@ +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import resolve from '@rollup/plugin-node-resolve'; +import postcss from 'rollup-plugin-postcss'; + +export default { + input: './dist/es6/index.js', + output: { + file: './dist/umd/mdvega-sanddance.js', + format: 'umd', + name: 'MdVegaSandDance', + }, + external: [ + 'vega', + 'vega-lite', + ], + plugins: [ + json(), + resolve({ jsnext: true }), + commonjs({ sourceMap: false }), + postcss({ + extract: false, // Don't extract to separate file - bundle inline + inject: true, // Inject CSS into the head + minimize: false, + }), + ], +}; diff --git a/packages/mdvega-sanddance/scripts/deploy.js b/packages/mdvega-sanddance/scripts/deploy.js new file mode 100644 index 000000000..03416dc01 --- /dev/null +++ b/packages/mdvega-sanddance/scripts/deploy.js @@ -0,0 +1,2 @@ +const fs = require('fs'); +fs.copyFileSync('./dist/umd/mdvega-sanddance.js', '../../docs/dist/mdvega-sanddance/v1/mdvega-sanddance.js'); diff --git a/packages/mdvega-sanddance/src/index.ts b/packages/mdvega-sanddance/src/index.ts new file mode 100644 index 000000000..5e2ded358 --- /dev/null +++ b/packages/mdvega-sanddance/src/index.ts @@ -0,0 +1,130 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { Batch, IInstance, Plugin, MdVega } from '@msrvida/mdvega'; +import * as SandDance from '@msrvida/sanddance'; +import '@msrvida/sanddance/dist/css/sanddance.css'; + +declare const vega: SandDance.VegaMorphCharts.types.VegaBase; +declare const MdVega: MdVega; + +interface SandDanceInstance { + id: string; + spec: SdMdSpec; + viewer: SandDance.Viewer; + viewIndex: number; +} + +export interface SdMdSpec { + dataSignalName?: string; + views: (SandDance.specs.Insight | SandDance.types.Snapshot)[]; + viewIndexSignalName?: string; +} + +export const sanddancePlugin: Plugin = { + name: 'sanddance', + initializePlugin: (md) => MdVega.definePlugin(md, 'sanddance'), + fence: (token, idx) => { + const id = `SandDance-${idx}`; + return MdVega.sanitizedHTML('div', { id, class: 'sanddance' }, token.content.trim()); + }, + hydrateComponent: async (renderer, errorHandler) => { + + SandDance.use(vega); + + const sanddanceInstances: SandDanceInstance[] = []; + const containers = renderer.element.querySelectorAll('.sanddance'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) continue; + + try { + const spec: SdMdSpec = JSON.parse(container.textContent); + const viewer = new SandDance.Viewer(container as HTMLElement, {}); + const sanddanceInstance: SandDanceInstance = { id: container.id, spec, viewer, viewIndex: 0 }; + sanddanceInstances.push(sanddanceInstance); + } catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'SandDance', index, 'parse', container); + continue; + } + } + const instances: IInstance[] = sanddanceInstances.map((sanddanceInstance, index) => { + const { viewer, spec } = sanddanceInstance; + const initialSignals = [ + { + name: sanddanceInstance.spec.dataSignalName, + value: null, + priority: 1, + isData: false, + }, + { + name: `${sanddanceInstance.spec.dataSignalName}${MdVega.common.dataNameSelectedSuffix}`, + value: null, + priority: 1, + isData: false, + }, + ]; + return { + ...sanddanceInstance, + initialSignals, + recieveBatch: async (batch) => { + let viewIndex = sanddanceInstance.viewIndex; + if (spec.viewIndexSignalName) { + const viewIndexSignal = batch[spec.viewIndexSignalName]; + if (viewIndexSignal && typeof viewIndexSignal.value === 'number') { + viewIndex = viewIndexSignal.value; + } + } + if (spec.dataSignalName) { + const newData = batch[spec.dataSignalName]?.value as object[]; + if (newData) { + let insight: SandDance.specs.Insight | undefined; + let setup: SandDance.types.Setup | undefined; + if (spec.views && spec.views[viewIndex]) { + const view = spec.views[viewIndex]; + const snapshot = view as SandDance.types.Snapshot; + if (snapshot.insight) { + //this is a snapshot + insight = { + ...snapshot.insight, + size: { + //TODO make sure this is a reasonable size + height: 400, + width: 600, + }, + }; + setup = snapshot.setup; + } else { + //this is an insight + insight = view as SandDance.specs.Insight; + } + } + viewer.render({ insight, setup }, newData); + } + } else { + //no new data but check if in + } + }, + beginListening() { + sanddanceInstance.viewer.options.onSelectionChanged = (search: SandDance.searchExpression.Search, activeIndex?: number, selectedData?: object[]) => { + const batch: Batch = { + [`${spec.dataSignalName}${MdVega.common.dataNameSelectedSuffix}`]: { + value: selectedData || [], + isData: true, + }, + }; + renderer.signalBus.broadcast(sanddanceInstance.id, batch); + }; + }, + destroy: async () => { + viewer.finalize(); + }, + }; + }); + return instances; + }, +}; + +MdVega.registerMarkdownPlugin(sanddancePlugin); diff --git a/packages/mdvega-sanddance/test/index.html b/packages/mdvega-sanddance/test/index.html new file mode 100644 index 000000000..3c07b165d --- /dev/null +++ b/packages/mdvega-sanddance/test/index.html @@ -0,0 +1,147 @@ + + + + + + + MdVega test + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + \ No newline at end of file diff --git a/packages/mdvega-sanddance/tsconfig.json b/packages/mdvega-sanddance/tsconfig.json new file mode 100644 index 000000000..5eae73cbb --- /dev/null +++ b/packages/mdvega-sanddance/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "moduleResolution": "node", + "outDir": "dist/es6", + "skipLibCheck": true, + "target": "es6", + "allowSyntheticDefaultImports": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/mdvega/LICENSE b/packages/mdvega/LICENSE new file mode 100644 index 000000000..21071075c --- /dev/null +++ b/packages/mdvega/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + 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 diff --git a/packages/mdvega/README.md b/packages/mdvega/README.md new file mode 100644 index 000000000..8f55da3e9 --- /dev/null +++ b/packages/mdvega/README.md @@ -0,0 +1,23 @@ +# interactive-documents + +Add [Vega](https://vega.github.io/vega/), [Vega-lite](https://vega.github.io/vega-lite/), and other interactive components to your markdown pages. Communicate [signals](https://vega.github.io/vega/docs/signals/) between components via a shared signal bus. + +Powered by [markdown-it](https://markdown-it.github.io/) plugins. + +## Features +* Add interactive components to markdown +* Fully declarative, no build required +* Add DIVs with https://mdit-plugins.github.io/container.html and https://mdit-plugins.github.io/attrs.html +* Style with CSS + +## Core plugins +* Vega +* Vega-lite +* {{placeholders}} +* Tabulator +* Image +* (Dynamic) Dropdown +* Presets + +### Extensibility +Create new plugins to extend the functionality of your markdown pages. \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/1.vega.md b/packages/mdvega/examples/seattle-weather/1.vega.md new file mode 100644 index 000000000..e8a8a22ca --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/1.vega.md @@ -0,0 +1,32 @@ +# Seattle Weather + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"url": "https://vega.github.io/editor/data/seattle-weather.csv"}, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/2.vega.md b/packages/mdvega/examples/seattle-weather/2.vega.md new file mode 100644 index 000000000..67d0ad414 --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/2.vega.md @@ -0,0 +1,80 @@ +# Seattle Weather + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"url": "https://vega.github.io/editor/data/seattle-weather.csv"}, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` + +And here is a heatmap: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": { + "url": "https://vega.github.io/editor/data/seattle-weather.csv" + }, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "ordinal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "ordinal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} +``` \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/3.vega.md b/packages/mdvega/examples/seattle-weather/3.vega.md new file mode 100644 index 000000000..2449b2b5c --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/3.vega.md @@ -0,0 +1,100 @@ +# Seattle Weather + +```json vega +{ + "description": "This is the central brain of the page", + "$schema": "https://vega.github.io/schema/vega/v5.json", + "signals": [ + { + "name": "seattle-weather", + "update": "data('seattle-weather')" + } + ], + "data": [ + { + "name": "seattle-weather", + "url": "https://vega.github.io/editor/data/seattle-weather.csv", + "format": { + "type": "csv" + } + } + ] +} +``` + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` + +And here is a heatmap: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "ordinal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "ordinal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} +``` \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/4.vega.md b/packages/mdvega/examples/seattle-weather/4.vega.md new file mode 100644 index 000000000..057ff9923 --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/4.vega.md @@ -0,0 +1,109 @@ +# Seattle Weather + +```json vega +{ + "description": "This is the central brain of the page", + "$schema": "https://vega.github.io/schema/vega/v5.json", + "signals": [ + { + "name": "seattle-weather", + "update": "data('seattle-weather')" + } + ], + "data": [ + { + "name": "seattle-weather", + "url": "https://vega.github.io/editor/data/seattle-weather.csv", + "format": { + "type": "csv" + } + } + ] +} +``` + +Data table: + +```json tabulator +{ + "dataSignalName": "seattle-weather", + "options": {} +} +``` + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` + +And here is a heatmap: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "ordinal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "ordinal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} +``` \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/5.vega.md b/packages/mdvega/examples/seattle-weather/5.vega.md new file mode 100644 index 000000000..8a7217284 --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/5.vega.md @@ -0,0 +1,115 @@ +# Seattle Weather + +```json vega +{ + "description": "This is the central brain of the page", + "$schema": "https://vega.github.io/schema/vega/v5.json", + "signals": [ + { + "name": "seattle-weather", + "update": "data('seattle-weather')" + } + ], + "data": [ + { + "name": "seattle-weather", + "url": "https://vega.github.io/editor/data/seattle-weather.csv", + "format": { + "type": "csv" + }, + "transform": [ + { + "type": "filter", + "expr": "datum.weather=='sun'" + } + ] + } + ] +} +``` + +Data table: + +```json tabulator +{ + "dataSignalName": "seattle-weather", + "options": {} +} +``` + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` + +And here is a heatmap: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "ordinal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "ordinal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} +``` \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/6.vega.md b/packages/mdvega/examples/seattle-weather/6.vega.md new file mode 100644 index 000000000..685f147da --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/6.vega.md @@ -0,0 +1,123 @@ +# Seattle Weather + +```json vega +{ + "description": "This is the central brain of the page", + "$schema": "https://vega.github.io/schema/vega/v5.json", + "signals": [ + { + "name": "seattle-weather", + "update": "data('seattle-weather')" + }, + { + "name": "IncludeSun", + "value": true, + "bind": { + "input": "checkbox", + "name": "Include Sun: " + } + } + ], + "data": [ + { + "name": "seattle-weather", + "url": "https://vega.github.io/editor/data/seattle-weather.csv", + "format": { + "type": "csv" + }, + "transform": [ + { + "type": "filter", + "expr": "IncludeSun ? true : datum.weather != 'sun'" + } + ] + } + ] +} +``` + +Data table: + +```json tabulator +{ + "dataSignalName": "seattle-weather", + "options": {} +} +``` + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` + +And here is a heatmap: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "ordinal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "ordinal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} +``` \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/7.vega.md b/packages/mdvega/examples/seattle-weather/7.vega.md new file mode 100644 index 000000000..c934b01f4 --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/7.vega.md @@ -0,0 +1,156 @@ +# Seattle Weather + +```json vega +{ + "description": "This is the central brain of the page", + "$schema": "https://vega.github.io/schema/vega/v5.json", + "signals": [ + { + "name": "seattle-weather", + "update": "data('seattle-weather')" + }, + { + "name": "IncludeSun", + "value": true, + "bind": { "input": "checkbox", "name": "Include Sun: " } + }, + { + "name": "IncludeFog", + "value": true, + "bind": { "input": "checkbox", "name": "Include Fog: " } + }, + { + "name": "IncludeDrizzle", + "value": true, + "bind": { "input": "checkbox", "name": "Include Drizzle: " } + }, + { + "name": "IncludeRain", + "value": true, + "bind": { "input": "checkbox", "name": "Include Rain: " } + }, + { + "name": "IncludeSnow", + "value": true, + "bind": { "input": "checkbox", "name": "Include Snow: " } + } + ], + "data": [ + { + "name": "seattle-weather", + "url": "https://vega.github.io/editor/data/seattle-weather.csv", + "format": { + "type": "csv" + }, + "transform": [ + { + "type": "filter", + "expr": "IncludeSun ? true : datum.weather != 'sun'" + }, + { + "type": "filter", + "expr": "IncludeFog ? true : datum.weather != 'fog'" + }, + { + "type": "filter", + "expr": "IncludeDrizzle ? true : datum.weather != 'drizzle'" + }, + { + "type": "filter", + "expr": "IncludeRain ? true : datum.weather != 'rain'" + }, + { + "type": "filter", + "expr": "IncludeSnow ? true : datum.weather != 'snow'" + } + ] + } + ] +} +``` + +Data table: + +```json tabulator +{ + "dataSignalName": "seattle-weather", + "options": {} +} +``` + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` + +And here is a heatmap: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "ordinal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "ordinal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} +``` \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/8.vega.md b/packages/mdvega/examples/seattle-weather/8.vega.md new file mode 100644 index 000000000..640157e80 --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/8.vega.md @@ -0,0 +1,182 @@ +# Seattle Weather + +```json vega +{ + "description": "This is the central brain of the page", + "$schema": "https://vega.github.io/schema/vega/v5.json", + "signals": [ + { + "name": "seattle-weather", + "update": "data('seattle-weather')" + }, + { + "name": "IncludeSun", + "value": true, + "bind": { "input": "checkbox", "name": "Include Sun: " } + }, + { + "name": "IncludeFog", + "value": true, + "bind": { "input": "checkbox", "name": "Include Fog: " } + }, + { + "name": "IncludeDrizzle", + "value": true, + "bind": { "input": "checkbox", "name": "Include Drizzle: " } + }, + { + "name": "IncludeRain", + "value": true, + "bind": { "input": "checkbox", "name": "Include Rain: " } + }, + { + "name": "IncludeSnow", + "value": true, + "bind": { "input": "checkbox", "name": "Include Snow: " } + }, + { + "name": "MinWind", + "value": 0, + "bind": { + "input": "range", + "min": 0, + "max": 30, + "step": 1, + "name": "Min Wind: " + } + }, + { + "name": "MaxWind", + "value": 30, + "bind": { + "input": "range", + "min": 0, + "max": 30, + "step": 1, + "name": "Max Wind: " + } + } + ], + "data": [ + { + "name": "seattle-weather", + "url": "https://vega.github.io/editor/data/seattle-weather.csv", + "format": { + "type": "csv" + }, + "transform": [ + { + "type": "filter", + "expr": "IncludeSun ? true : datum.weather != 'sun'" + }, + { + "type": "filter", + "expr": "IncludeFog ? true : datum.weather != 'fog'" + }, + { + "type": "filter", + "expr": "IncludeDrizzle ? true : datum.weather != 'drizzle'" + }, + { + "type": "filter", + "expr": "IncludeRain ? true : datum.weather != 'rain'" + }, + { + "type": "filter", + "expr": "IncludeSnow ? true : datum.weather != 'snow'" + }, + { + "type": "filter", + "expr": "datum.wind >= MinWind && datum.wind <= MaxWind" + } + ] + } + ] +} +``` + +Data table: + +```json tabulator +{ + "dataSignalName": "seattle-weather", + "options": {} +} +``` + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` + +And here is a heatmap: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": {"name": "seattle-weather"}, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "ordinal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "ordinal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} +``` \ No newline at end of file diff --git a/packages/mdvega/examples/seattle-weather/9.vega.md b/packages/mdvega/examples/seattle-weather/9.vega.md new file mode 100644 index 000000000..e23a1f3a5 --- /dev/null +++ b/packages/mdvega/examples/seattle-weather/9.vega.md @@ -0,0 +1,249 @@ +# Seattle Weather + +```json vega +{ + "description": "This is the central brain of the page", + "$schema": "https://vega.github.io/schema/vega/v5.json", + "signals": [ + { + "name": "seattle-weather", + "update": "data('seattle-weather')" + }, + { + "name": "IncludeSun", + "value": true, + "bind": { "input": "checkbox", "name": "Include Sun: " } + }, + { + "name": "IncludeFog", + "value": true, + "bind": { "input": "checkbox", "name": "Include Fog: " } + }, + { + "name": "IncludeDrizzle", + "value": true, + "bind": { "input": "checkbox", "name": "Include Drizzle: " } + }, + { + "name": "IncludeRain", + "value": true, + "bind": { "input": "checkbox", "name": "Include Rain: " } + }, + { + "name": "IncludeSnow", + "value": true, + "bind": { "input": "checkbox", "name": "Include Snow: " } + }, + { + "name": "MinWind", + "value": 0, + "bind": { + "input": "range", + "min": 0, + "max": 30, + "step": 1, + "name": "Min Wind: " + } + }, + { + "name": "MaxWind", + "value": 30, + "bind": { + "input": "range", + "min": 0, + "max": 30, + "step": 1, + "name": "Max Wind: " + } + } + ], + "data": [ + { + "name": "seattle-weather", + "url": "https://vega.github.io/editor/data/seattle-weather.csv", + "format": { + "type": "csv" + }, + "transform": [ + { + "type": "filter", + "expr": "IncludeSun ? true : datum.weather != 'sun'" + }, + { + "type": "filter", + "expr": "IncludeFog ? true : datum.weather != 'fog'" + }, + { + "type": "filter", + "expr": "IncludeDrizzle ? true : datum.weather != 'drizzle'" + }, + { + "type": "filter", + "expr": "IncludeRain ? true : datum.weather != 'rain'" + }, + { + "type": "filter", + "expr": "IncludeSnow ? true : datum.weather != 'snow'" + }, + { + "type": "filter", + "expr": "datum.wind >= MinWind && datum.wind <= MaxWind" + } + ] + } + ] +} +``` + +Presets allow you to go pre-chosen values: + +```json presets +[ + { + "name": "Basic Gloom", + "state": { + "IncludeSun": false, + "IncludeFog": true, + "IncludeDrizzle": true, + "IncludeRain": false, + "IncludeSnow": false, + "MinWind": 0, + "MaxWind": 5 + } + }, + { + "name": "reset", + "state": { + "MinWind": 0, + "MaxWind": 30, + "IncludeSun": true, + "IncludeFog": true, + "IncludeDrizzle": true, + "IncludeRain": true, + "IncludeSnow": true + } + }, + { + "name": "sunny and calm", + "state": { + "IncludeSun": true, + "IncludeFog": false, + "IncludeDrizzle": false, + "IncludeRain": false, + "IncludeSnow": false, + "MinWind": 0, + "MaxWind": 5 + } + }, + { + "name": "stormy weather", + "state": { + "IncludeSun": false, + "IncludeFog": false, + "IncludeDrizzle": true, + "IncludeRain": true, + "IncludeSnow": true, + "MinWind": 5, + "MaxWind": 30 + } + }, + { + "name": "just snow", + "state": { + "IncludeSun": false, + "IncludeFog": false, + "IncludeDrizzle": false, + "IncludeRain": false, + "IncludeSnow": true, + "MinWind": 0, + "MaxWind": 10 + } + } +] +``` + +Data table: + +```json tabulator +{ + "dataSignalName": "seattle-weather", + "options": {} +} +``` + +Here is a stacked bar chart of Seattle weather: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": { "name": "seattle-weather" }, + "mark": "bar", + "encoding": { + "x": { + "timeUnit": "month", + "field": "date", + "type": "ordinal", + "title": "Month of the year" + }, + "y": { + "aggregate": "count", + "type": "quantitative" + }, + "color": { + "field": "weather", + "type": "nominal", + "scale": { + "domain": ["sun", "fog", "drizzle", "rain", "snow"], + "range": ["#e7ba52", "#c7c7c7", "#aec7e8", "#1f77b4", "#9467bd"] + }, + "title": "Weather type" + } + } +} +``` + +And here is a heatmap: + +```json vega-lite +{ + "$schema": "https://vega.github.io/schema/vega-lite/v6.json", + "data": { "name": "seattle-weather" }, + "title": "Daily Max Temperatures (C) in Seattle, WA", + "config": { + "view": { + "strokeWidth": 0, + "step": 13 + }, + "axis": { + "domain": false + } + }, + "mark": "rect", + "encoding": { + "x": { + "field": "date", + "timeUnit": "date", + "type": "ordinal", + "title": "Day", + "axis": { + "labelAngle": 0, + "format": "%e" + } + }, + "y": { + "field": "date", + "timeUnit": "month", + "type": "ordinal", + "title": "Month" + }, + "color": { + "field": "temp_max", + "aggregate": "max", + "type": "quantitative", + "legend": { + "title": null + } + } + } +} +``` diff --git a/packages/mdvega/package.json b/packages/mdvega/package.json new file mode 100644 index 000000000..c69c18cb6 --- /dev/null +++ b/packages/mdvega/package.json @@ -0,0 +1,28 @@ +{ + "name": "@msrvida/mdvega", + "version": "1.0.0", + "description": "Markdown pages with Vega signals", + "main": "dist/es6/index.js", + "types": "dist/es6/index.d.ts", + "devDependencies": { + "@mdit/plugin-attrs": "^0.22.0", + "@mdit/plugin-container": "^0.22.0", + "@types/markdown-it": "^14.1.2", + "@types/tabulator-tables": "^6.2.6", + "vega-typings": "~1.5.0" + }, + "scripts": { + "clean": "rimraf dist", + "deploy": "node ./scripts/deploy.js", + "watch-typescript": "tsc -p . -w", + "build-typescript": "tsc -p .", + "bundle": "rollup -c", + "build:01": "npm run build-typescript && npm run bundle" + }, + "keywords": [ + "markdown", + "vega" + ], + "author": "Dan Marshall", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/mdvega/rollup.config.mjs b/packages/mdvega/rollup.config.mjs new file mode 100644 index 000000000..8d9d082f7 --- /dev/null +++ b/packages/mdvega/rollup.config.mjs @@ -0,0 +1,22 @@ +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import resolve from '@rollup/plugin-node-resolve'; + +export default { + input: './dist/es6/index.js', + output: { + file: './dist/umd/mdvega.js', + format: 'umd', + name: 'MdVega', + }, + external: [ + 'markdown-it', + 'vega', + 'vega-lite', + ], + plugins: [ + json(), + resolve({ jsnext: true }), + commonjs({ sourceMap: false }), + ], +}; diff --git a/packages/mdvega/scripts/deploy.js b/packages/mdvega/scripts/deploy.js new file mode 100644 index 000000000..67cd96361 --- /dev/null +++ b/packages/mdvega/scripts/deploy.js @@ -0,0 +1,2 @@ +const fs = require('fs'); +fs.copyFileSync('./dist/umd/mdvega.js', '../../docs/dist/mdvega/v1/mdvega.js'); diff --git a/packages/mdvega/src/factory.ts b/packages/mdvega/src/factory.ts new file mode 100644 index 000000000..095e085e1 --- /dev/null +++ b/packages/mdvega/src/factory.ts @@ -0,0 +1,127 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import MarkdownIt, { Token } from 'markdown-it'; +import { attrs } from '@mdit/plugin-attrs'; +import { container, MarkdownItContainerOptions } from '@mdit/plugin-container'; +import { ErrorHandler, Renderer } from './renderer'; + +declare const markdownit: typeof MarkdownIt; + +export interface PrioritizedSignal { + name: string; + value: unknown; + priority: number; + isData: boolean; +} + +export interface BatchItem { + value: unknown; + isData: boolean; +} + +export interface Batch { + [name: string]: BatchItem; +} + +export interface IInstance { + id: string; + initialSignals: PrioritizedSignal[]; + recieveBatch?: (batch: Batch, from: string) => Promise; + beginListening?: (sharedSignals: { signalName: string, isData: boolean }[]) => void; + broadcastComplete?: () => Promise; + destroy?: () => Promise; + getCurrentSignalValue?: (signalName: string) => unknown; +} + +export interface Plugin { + name: string; + initializePlugin: (md: MarkdownIt) => void; + fence?: (token: Token, idx: number) => string; + hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler) => Promise; +} + +export const plugins: Plugin[] = []; + +export function registerMarkdownPlugin(plugin: Plugin) { + plugins.push(plugin); + return 'register'; +} + +export interface CreateOptions { + classList?: string[]; +} + +export function create(options?: CreateOptions) { + const md = new markdownit(); + for (const plugin of plugins) { + plugin.initializePlugin(md); + } + + md.use(attrs); + + options?.classList?.forEach(name => { + const containerOptions: MarkdownItContainerOptions = { name }; + md.use(container, containerOptions); + }); + + // Default handler to preserve existing functionality + const originalFence = md.renderer.rules.fence; + + // Modified fence renderer to dynamically use handlers + md.renderer.rules.fence = function (tokens, idx, options, env, slf) { + const token = tokens[idx]; + const info = token.info.trim(); + + // Check if the info starts with "json " and extract the plugin name + if (info.startsWith('json ')) { + const pluginName = info.slice(5).trim(); + + // Find the plugin by name + const plugin = plugins.find(p => p.name === pluginName); + if (plugin && plugin.fence) { + return plugin.fence(token, idx); + } + } + + // Fallback to the original fence renderer if no plugin matches + if (originalFence) { + return originalFence(tokens, idx, options, env, slf); + } else { + return ''; + } + }; + + return md; +} + +export function definePlugin(md: MarkdownIt, pluginName: string) { + md.block.ruler.before('fence', `${pluginName}_block`, function (state, startLine, endLine) { + const start = state.bMarks[startLine] + state.tShift[startLine]; + const max = state.eMarks[startLine]; + + // Check if the block starts with "```json " + const marker = `json ${pluginName}`; + if (!state.src.slice(start, max).trim().startsWith('```' + marker)) { + return false; + } + + let nextLine = startLine; + while (nextLine < endLine) { + nextLine++; + if (state.src.slice(state.bMarks[nextLine] + state.tShift[nextLine], state.eMarks[nextLine]).trim() === '```') { + break; + } + } + + state.line = nextLine + 1; + const token = state.push('fence', 'code', 0); + token.info = marker; + token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true); + token.map = [startLine, state.line]; + + return true; + }); +} diff --git a/packages/mdvega/src/index.ts b/packages/mdvega/src/index.ts new file mode 100644 index 000000000..d33b4fe41 --- /dev/null +++ b/packages/mdvega/src/index.ts @@ -0,0 +1,34 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { definePlugin, Plugin, plugins, registerMarkdownPlugin } from './factory'; +import { Renderer } from './renderer'; +import { registerNativePlugins } from './plugins'; +import { sanitizedHTML } from './sanitize'; +import * as common from './plugins/common'; + +registerNativePlugins(); + +export { + definePlugin, + Plugin, + plugins, + registerMarkdownPlugin, + Renderer, + sanitizedHTML, + common, +}; + +export type MdVega = { + definePlugin: typeof definePlugin; + plugins: typeof plugins; + registerMarkdownPlugin: typeof registerMarkdownPlugin; + Renderer: typeof Renderer; + sanitizedHTML: typeof sanitizedHTML; + common: typeof common; +}; + +export * as Plugins from './plugins/interfaces'; +export { Batch, IInstance } from './factory'; diff --git a/packages/mdvega/src/plugins/common.ts b/packages/mdvega/src/plugins/common.ts new file mode 100644 index 000000000..8725f99c3 --- /dev/null +++ b/packages/mdvega/src/plugins/common.ts @@ -0,0 +1 @@ +export const dataNameSelectedSuffix = '-selected'; diff --git a/packages/mdvega/src/plugins/dropdown.ts b/packages/mdvega/src/plugins/dropdown.ts new file mode 100644 index 000000000..842fdb067 --- /dev/null +++ b/packages/mdvega/src/plugins/dropdown.ts @@ -0,0 +1,186 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { Batch, definePlugin, IInstance, Plugin } from '../factory'; +import { sanitizedHTML } from '../sanitize'; + +interface DropdownInstance { + id: string; + spec: DropdownSpec; + element: HTMLSelectElement; +} + +interface DynamicDropdownOptions { + dataSignalName: string; + fieldName: string; +} + +export interface DropdownSpec { + name: string; + value?: string | string[]; + label?: string; + //one of either static or dynamic options must be set + options?: string[]; + multiple?: boolean; + size?: number; + dynamicOptions?: DynamicDropdownOptions; +} + +export const dropdownPlugin: Plugin = { + name: 'dropdown', + initializePlugin: (md) => definePlugin(md, 'dropdown'), + fence: (token, idx) => { + const DropdownId = `Dropdown-${idx}`; + return sanitizedHTML('div', { id: DropdownId, class: 'dropdown' }, token.content.trim()); + }, + hydrateComponent: async (renderer, errorHandler) => { + const dropdownInstances: DropdownInstance[] = []; + const containers = renderer.element.querySelectorAll('.dropdown'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) continue; + + try { + const spec: DropdownSpec = JSON.parse(container.textContent); + + const html = `
+
+ +
+
`; + container.innerHTML = html; + const element = container.querySelector('select') as HTMLSelectElement; + + const dropdownInstance: DropdownInstance = { id: container.id, spec, element }; + dropdownInstances.push(dropdownInstance); + } catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'Dropdown', index, 'parse', container); + continue; + } + } + const instances: IInstance[] = dropdownInstances.map((dropdownInstance, index) => { + const { element, spec } = dropdownInstance; + const initialSignals = [{ + name: spec.name, + value: spec.value || null, + priority: 1, + isData: false, + }]; + if (spec.dynamicOptions) { + initialSignals.push({ + name: spec.dynamicOptions.dataSignalName, + value: null, + priority: -1, + isData: true, + }); + } + return { + ...dropdownInstance, + initialSignals, + recieveBatch: async (batch) => { + const { dynamicOptions } = spec; + if (dynamicOptions?.dataSignalName) { + const newData = batch[dynamicOptions.dataSignalName]?.value as object[]; + if (newData) { + //pluck the field from the data and add options to the select + let hasFieldName = false; + //remove duplicates from the options array + const uniqueOptions = new Set(); + newData.forEach((d) => { + //check if the field exists in the data + if (d.hasOwnProperty(dynamicOptions.fieldName)) { + hasFieldName = true; + uniqueOptions.add(d[dynamicOptions.fieldName]); + } + }); + if (hasFieldName) { + const options = Array.from(uniqueOptions); + const existingSelection = spec.multiple ? Array.from(element.selectedOptions).map(option => option.value) : element.value; + element.innerHTML = getOptions(spec.multiple, options, existingSelection); + if (!spec.multiple) { + element.value = (batch[spec.name]?.value as string) || options[0]; + } + } else { + //if the field doesn't exist, set the select to the first option + element.innerHTML = ``; + element.value = ''; + } + } + } + if (batch[spec.name]) { + const value = batch[spec.name].value as string | string[]; + if (spec.multiple) { + Array.from(element.options).forEach((option) => { + option.selected = value && (value as string[]).includes(option.value); + }); + } else { + element.value = value as string; + } + } + }, + beginListening() { + //wire up handler to send the selected value to the signal bus + element.addEventListener('change', (e) => { + const value = spec.multiple + ? Array.from((e.target as HTMLSelectElement).selectedOptions).map(option => option.value) + : (e.target as HTMLSelectElement).value; + const batch: Batch = { + [spec.name]: { + value, + isData: false, + }, + }; + renderer.signalBus.broadcast(dropdownInstance.id, batch); + }); + }, + getCurrentSignalValue: () => { + if (spec.multiple) { + return Array.from(element.selectedOptions).map(option => option.value); + } + return element.value; + }, + destroy: async () => { + element.removeEventListener('change', dropdownInstance.element.onchange as EventListener); + }, + }; + }); + return instances; + }, +}; + +function getOptions(multiple: boolean, options: string[], selected: string | string[]) { + if (!options) { + if (multiple) { + if (Array.isArray(selected)) { + options = selected as string[]; + } else { + if (selected) { + options = [selected as string]; + } + } + } else { + if (selected) { + options = [selected as string]; + } + } + } + if (!options) { + return ''; + } + return options.map((option) => { + let attr = ''; + if (multiple) { + attr = (selected as string[] || []).includes(option) ? 'selected' : ''; + } else { + attr = selected === option ? 'selected' : ''; + } + return ``; + }).join('\n'); +} diff --git a/packages/mdvega/src/plugins/image.ts b/packages/mdvega/src/plugins/image.ts new file mode 100644 index 000000000..883af2105 --- /dev/null +++ b/packages/mdvega/src/plugins/image.ts @@ -0,0 +1,116 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { definePlugin, IInstance, Plugin } from '../factory'; +import { sanitizedHTML } from '../sanitize'; + +export interface ImageSpec { + srcSignalName: string; + alt?: string; + width?: number; + height?: number; +} + +interface ImageInstance { + id: string; + spec: ImageSpec; + element: HTMLImageElement; + spinner: HTMLDivElement; +} + +enum ImageOpacity { + full = '1', + loading = '0.1', + error = '0.5', +} + +export const imagePlugin: Plugin = { + name: 'image', + initializePlugin: (md) => definePlugin(md, 'image'), + fence: (token, idx) => { + const ImageId = `Image-${idx}`; + return sanitizedHTML('div', { id: ImageId, class: 'image' }, token.content.trim()); + }, + hydrateComponent: async (renderer, errorHandler) => { + const imageInstances: ImageInstance[] = []; + const containers = renderer.element.querySelectorAll('.image'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) continue; + try { + const spec: ImageSpec = JSON.parse(container.textContent); + const element = document.createElement('img'); + const spinner = document.createElement('div'); + spinner.innerHTML = ` + + + + + `; + + if (spec.alt) element.alt = spec.alt; + if (spec.width) element.width = spec.width; + if (spec.height) element.height = spec.height; + element.onload = () => { + spinner.style.display = 'none'; + element.style.opacity = ImageOpacity.full; + }; + element.onerror = () => { + spinner.style.display = 'none'; + element.style.opacity = ImageOpacity.error; + errorHandler(new Error('Image failed to load'), 'image', index, 'load', container, element.src); + }; + + (container as HTMLElement).style.position = 'relative'; + spinner.style.position = 'absolute'; + container.innerHTML = ''; + container.appendChild(spinner); + container.appendChild(element); + + const imageInstance: ImageInstance = { id: container.id, spec, element, spinner }; + imageInstances.push(imageInstance); + } catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'Image', index, 'parse', container); + } + } + const instances: IInstance[] = imageInstances.map((imageInstance, index) => { + const { element, spinner, id, spec } = imageInstance; + return { + id, + initialSignals: [ + { + name: spec.srcSignalName, + value: null, + priority: -1, + isData: false, + }, + ], + destroy: async () => { + if (element) { + element.remove(); + } + if (spinner) { + spinner.remove(); + } + }, + recieveBatch: async (batch, from) => { + if (spec.srcSignalName in batch) { + const src = batch[spec.srcSignalName].value; + if (src) { + spinner.style.display = ''; + element.src = src.toString(); + element.style.opacity = ImageOpacity.loading; + } else { + element.src = ''; //TODO placeholder image + spinner.style.display = 'none'; + element.style.opacity = ImageOpacity.full; + } + } + }, + }; + }); + return instances; + }, +}; diff --git a/packages/mdvega/src/plugins/index.ts b/packages/mdvega/src/plugins/index.ts new file mode 100644 index 000000000..57093b450 --- /dev/null +++ b/packages/mdvega/src/plugins/index.ts @@ -0,0 +1,24 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { registerMarkdownPlugin } from '../factory'; + +import { dropdownPlugin } from './dropdown'; +import { imagePlugin } from './image'; +import { placeholdersPlugin } from './placeholders'; +import { presetsPlugin } from './presets'; +import { tabulatorPlugin } from './tabulator'; +import { vegaLitePlugin } from './vega-lite'; +import { vegaPlugin } from './vega'; + +export function registerNativePlugins() { + registerMarkdownPlugin(dropdownPlugin); + registerMarkdownPlugin(imagePlugin); + registerMarkdownPlugin(placeholdersPlugin); + registerMarkdownPlugin(presetsPlugin); + registerMarkdownPlugin(tabulatorPlugin); + registerMarkdownPlugin(vegaLitePlugin); + registerMarkdownPlugin(vegaPlugin); +} diff --git a/packages/mdvega/src/plugins/interfaces.ts b/packages/mdvega/src/plugins/interfaces.ts new file mode 100644 index 000000000..a67a59b5a --- /dev/null +++ b/packages/mdvega/src/plugins/interfaces.ts @@ -0,0 +1,4 @@ +export { DropdownSpec } from './dropdown'; +export { ImageSpec } from './image'; +export { PresetsSpec } from './presets'; +export { TabulatorSpec } from './tabulator'; diff --git a/packages/mdvega/src/plugins/placeholders.ts b/packages/mdvega/src/plugins/placeholders.ts new file mode 100644 index 000000000..828bcef13 --- /dev/null +++ b/packages/mdvega/src/plugins/placeholders.ts @@ -0,0 +1,191 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { Token } from 'markdown-it'; +import { Batch, IInstance, Plugin, PrioritizedSignal } from '../factory'; + +function createTemplateFunction(template: string) { + const parts = template.split(/(%7B%7B.*?%7D%7D)/g).map(part => { + if (part.startsWith('%7B%7B') && part.endsWith('%7D%7D')) { + const key = part.slice(6, -6); // Extract key from %7B%7Bkey%7D%7D + return (batch: Batch) => batch[key]?.value?.toString() || ''; + } else { + return () => part; // Static part of the template + } + }); + + return (batch: Batch) => parts.map(fn => fn(batch)).join(''); +} + +function handleDynamicUrl(tokens: Token[], idx: number, attrName: string, elementType: string) { + const token = tokens[idx]; + const attrValue = token.attrGet(attrName); + + if (attrValue && attrValue.includes('%7B%7B')) { + // Ensure token.attrs is initialized + if (!token.attrs) { + token.attrs = []; + } + token.attrSet('data-template-url', attrValue); // Store original template + } + + return token; +} + +export const placeholdersPlugin: Plugin = { + name: 'placeholders', + initializePlugin: async (md) => { + // Custom plugin to handle dynamic placeholders + md.use(function (md) { + // Add a custom rule to handle {{...}} placeholders + md.inline.ruler.after('emphasis', 'dynamic_placeholder', function (state, silent) { + let token: Token; + const max = state.posMax; + const start = state.pos; + + // Look for double curly braces {{ + if (state.src.charCodeAt(start) !== 0x7B /* { */ || + state.src.charCodeAt(start + 1) !== 0x7B /* { */) { + return false; + } + + for (let pos = start + 2; pos < max; pos++) { + if (state.src.charCodeAt(pos) === 0x7D /* } */ && + state.src.charCodeAt(pos + 1) === 0x7D /* } */) { + if (!silent) { + state.pos = start + 2; + state.posMax = pos; + + token = state.push('dynamic_placeholder', '', 0); + token.markup = state.src.slice(start, pos + 2); + token.content = state.src.slice(state.pos, state.posMax); + + state.pos = pos + 2; + state.posMax = max; + } + return true; + } + } + return false; + }); + + // Renderer rule for dynamic placeholders + md.renderer.rules['dynamic_placeholder'] = function (tokens, idx) { + const key = tokens[idx].content.trim(); + return `{${key}}`; + }; + }); + + md.renderer.rules['link_open'] = function (tokens, idx, options, env, slf) { + handleDynamicUrl(tokens, idx, 'href', 'link'); + return slf.renderToken(tokens, idx, options); + }; + + md.renderer.rules['image'] = function (tokens, idx, options, env, slf) { + handleDynamicUrl(tokens, idx, 'src', 'image'); + return slf.renderToken(tokens, idx, options); + }; + + }, + + hydrateComponent: async (renderer) => { + const templateFunctionMap = new WeakMap string, batch: Batch }>(); + const placeholders = renderer.element.querySelectorAll('.dynamic-placeholder'); + const dynamicUrls = renderer.element.querySelectorAll('[data-template-url]'); + const elementsByKeys = new Map(); + + // Collect placeholders + for (const placeholder of placeholders) { + const key = placeholder.getAttribute('data-key'); + if (elementsByKeys.has(key)) { + elementsByKeys.get(key).push(placeholder); + } else { + elementsByKeys.set(key, [placeholder]); + } + } + + // Collect dynamic URLs + for (const element of dynamicUrls) { + const templateUrl = element.getAttribute('data-template-url'); + const keys = Array.from(templateUrl.matchAll(/%7B%7B(.*?)%7D%7D/g)).map(match => match[1]); + + const templateFunction = createTemplateFunction(templateUrl); + templateFunctionMap.set(element, { templateFunction, batch: {} }); + + for (const key of keys) { + if (elementsByKeys.has(key)) { + elementsByKeys.get(key).push(element); + } else { + elementsByKeys.set(key, [element]); + } + } + } + + // Create initial signals + const initialSignals = Array.from(elementsByKeys.keys()).map(name => { + const prioritizedSignal: PrioritizedSignal = { + name, + value: null, + priority: -1, + isData: false, + }; + return prioritizedSignal; + }); + + const instances: IInstance[] = [ + { + id: 'placeholders', + initialSignals, + recieveBatch: async (batch) => { + for (const key of Object.keys(batch)) { + const elements = elementsByKeys.get(key) || []; + for (const element of elements) { + if (element.classList.contains('dynamic-placeholder')) { + // Update placeholder content + const markdownContent = batch[key].value?.toString() || ''; + const parsedMarkdown = isMarkdownInline(markdownContent) + ? renderer.md.renderInline(markdownContent) + : renderer.md.render(markdownContent); + element.innerHTML = parsedMarkdown; + } else if (element.hasAttribute('data-template-url')) { + // Update dynamic URL + const templateData = templateFunctionMap.get(element); + if (templateData) { + // Merge the new batch with the stored batch + templateData.batch = { ...templateData.batch, ...batch }; + const updatedUrl = templateData.templateFunction(templateData.batch); + if (element.tagName === 'A') { + element.setAttribute('href', updatedUrl); + } else if (element.tagName === 'IMG') { + element.setAttribute('src', updatedUrl); + } + } + } + } + } + }, + }, + ]; + + return instances; + }, +}; + +function isMarkdownInline(markdown: string) { + // Inline markdown typically does not contain newlines + if (!markdown.includes('\n')) { + return true; + } + + // Block markdown typically contains newlines and block elements + const blockElements = ['#', '-', '*', '>', '```', '~~~']; + for (const element of blockElements) { + if (markdown.trim().startsWith(element)) { + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/packages/mdvega/src/plugins/presets.ts b/packages/mdvega/src/plugins/presets.ts new file mode 100644 index 000000000..ef4aba347 --- /dev/null +++ b/packages/mdvega/src/plugins/presets.ts @@ -0,0 +1,130 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { Batch, definePlugin, IInstance, Plugin, PrioritizedSignal } from '../factory'; +import { sanitizedHTML } from '../sanitize'; + +interface Preset { + name: string; + description?: string; + state: { [signalName: string]: unknown }; +} + +export type PresetsSpec = Preset[]; + +interface PresetsInstance { + id: string; + presets: Preset[]; + element: HTMLUListElement; +} + +export const presetsPlugin: Plugin = { + name: 'presets', + initializePlugin: (md) => definePlugin(md, 'presets'), + fence: (token, idx) => { + const spec = JSON.parse(token.content.trim()); + const pluginId = `preset-${idx}`; + return sanitizedHTML('div', { id: pluginId, class: 'presets' }, JSON.stringify(spec)); + }, + hydrateComponent: async (renderer, errorHandler) => { + const presetsInstances: PresetsInstance[] = []; + const containers = renderer.element.querySelectorAll('.presets'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) continue; + + const id = `presets${index}`; + let presets: Preset[]; + try { + presets = JSON.parse(container.textContent) as Preset[]; + } catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'presets', index, 'parse', container); + continue; + } + if (!Array.isArray(presets)) { + container.innerHTML = '
Expected an array of presets
'; + continue; + } + //clear the container + container.innerHTML = ''; + const ul = document.createElement('ul'); + const presetsInstance: PresetsInstance = { id, presets, element: ul }; + container.appendChild(ul); + for (const preset of presets) { + //make a button for each preset + const li = document.createElement('li'); + if (!preset.name || !preset.state) { + const span = document.createElement('span'); + span.className = 'error'; + span.textContent = 'Each preset must have a name and state'; + li.appendChild(span); + } else { + const button = document.createElement('button'); + button.textContent = preset.name; + button.onclick = () => { + const batch: Batch = {}; + for (const [signalName, value] of Object.entries(preset.state)) { + batch[signalName] = { value, isData: false }; + } + renderer.signalBus.broadcast(id, batch); + }; + li.appendChild(button); + li.appendChild(document.createTextNode('\u00A0')); + if (preset.description) { + button.title = preset.description; + } + } + ul.appendChild(li); + } + presetsInstances.push(presetsInstance); + } + const instances: IInstance[] = presetsInstances.map((presetsInstance, index) => { + const initialSignals: PrioritizedSignal[] = presetsInstance.presets.flatMap(preset => { + return Object.keys(preset.state).map(signalName => { + return { + name: signalName, + value: null, + priority: -1, + isData: undefined, // we do not know if it is data or not + }; + }); + }); + return { + ...presetsInstance, + initialSignals, + broadcastComplete: async () => { + //populate state from the renderer.signalBus.signalDeps + const state: { [signalName: string]: unknown } = {}; + for (const signalName of Object.keys(renderer.signalBus.signalDeps)) { + state[signalName] = renderer.signalBus.signalDeps[signalName].value; + } + // highlight any presets that have the same signals and values as the current state + setAllPresetsActiveState(presetsInstance, state); + }, + }; + }); + return instances; + }, +}; + +function isPresetActive(preset: Preset, state: { [signalName: string]: unknown }) { + for (const [signalName, value] of Object.entries(preset.state)) { + if (state[signalName] !== value) { + return false; + } + } + return true; +} + +function setAllPresetsActiveState(presetsInstance: PresetsInstance, state: { [signalName: string]: unknown }) { + for (const [presetIndex, preset] of presetsInstance.presets.entries()) { + const { classList } = presetsInstance.element.children[presetIndex]; + if (isPresetActive(preset, state)) { + classList.add('active'); + } else { + classList.remove('active'); + } + } +} diff --git a/packages/mdvega/src/plugins/tabulator.ts b/packages/mdvega/src/plugins/tabulator.ts new file mode 100644 index 000000000..6d92575d3 --- /dev/null +++ b/packages/mdvega/src/plugins/tabulator.ts @@ -0,0 +1,122 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { Batch, definePlugin, IInstance, Plugin } from '../factory'; +import { sanitizedHTML } from '../sanitize'; +import { Tabulator, Options as TabulatorOptions } from 'tabulator-tables'; +import { dataNameSelectedSuffix } from './common'; + +interface TabulatorInstance { + id: string; + spec: TabulatorSpec; + table: Tabulator; +} + +export interface TabulatorSpec { + dataSignalName: string; + options: TabulatorOptions; +} + +interface CustomWindow extends Window { + Tabulator: typeof Tabulator; +} + +export const tabulatorPlugin: Plugin = { + name: 'tabulator', + initializePlugin: (md) => definePlugin(md, 'tabulator'), + fence: (token, idx) => { + const tabulatorId = `tabulator-${idx}`; + return sanitizedHTML('div', { id: tabulatorId, class: 'tabulator', style: 'box-sizing: border-box;' }, token.content.trim()); + }, + hydrateComponent: async (renderer, errorHandler) => { + const tabulatorInstances: TabulatorInstance[] = []; + const containers = renderer.element.querySelectorAll('.tabulator'); + for (const [index, container] of containers.entries()) { + if (!container.textContent) continue; + if (!(window as unknown as CustomWindow).Tabulator) { + errorHandler(new Error('Tabulator not found'), 'tabulator', index, 'init', container); + continue; + } + + try { + const spec: TabulatorSpec = JSON.parse(container.textContent); + + let options: TabulatorOptions = { + autoColumns: true, + layout: 'fitColumns', + maxHeight: '200px', + }; + + //see if default options is an object with no properties + if (spec.options && Object.keys(spec.options).length > 0) { + options = spec.options; + } + + const table = new (window as unknown as CustomWindow).Tabulator(container as HTMLElement, options); + const tabulatorInstance: TabulatorInstance = { id: container.id, spec, table }; + tabulatorInstances.push(tabulatorInstance); + } catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'tabulator', index, 'parse', container); + continue; + } + } + const instances: IInstance[] = tabulatorInstances.map((tabulatorInstance, index) => { + const initialSignals = [{ + name: tabulatorInstance.spec.dataSignalName, + value: null, + priority: -1, + isData: true, + }]; + if (tabulatorInstance.spec.options.selectableRows) { + initialSignals.push({ + name: `${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`, + value: [], + priority: -1, + isData: true, + }); + } + return { + ...tabulatorInstance, + initialSignals, + recieveBatch: async (batch) => { + const newData = batch[tabulatorInstance.spec.dataSignalName].value as object[]; + if (newData) { + tabulatorInstance.table.setData(newData); + } + }, + beginListening(sharedSignals) { + if (tabulatorInstance.spec.options.selectableRows) { + for (const { isData, signalName } of sharedSignals) { + if (isData) { + const matchData = signalName === `${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`; + if (matchData) { + tabulatorInstance.table.on('rowSelectionChanged', (e, rows) => { + const selectedData = tabulatorInstance.table.getSelectedData(); + const batch: Batch = { + [`${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`]: { + value: selectedData, + isData: true, + }, + }; + renderer.signalBus.log(tabulatorInstance.id, 'sending batch', batch); + renderer.signalBus.broadcast(tabulatorInstance.id, batch); + }); + } + } + } + } + }, + getCurrentSignalValue() { + return tabulatorInstance.table.getSelectedData(); + }, + destroy: async () => { + tabulatorInstance.table.destroy(); + }, + }; + }); + return instances; + }, +}; diff --git a/packages/mdvega/src/plugins/util.ts b/packages/mdvega/src/plugins/util.ts new file mode 100644 index 000000000..a67ac2404 --- /dev/null +++ b/packages/mdvega/src/plugins/util.ts @@ -0,0 +1,8 @@ +export function urlParam(urlParamName: string, value: any) { + if (value === undefined || value === null) return ''; + if (Array.isArray(value)) { + return value.map(vn => `${urlParamName}[]=${encodeURIComponent(vn)}`).join('&'); + } else { + return `${urlParamName}=${encodeURIComponent(value)}`; + } +} diff --git a/packages/mdvega/src/plugins/vega-lite.ts b/packages/mdvega/src/plugins/vega-lite.ts new file mode 100644 index 000000000..51577f29c --- /dev/null +++ b/packages/mdvega/src/plugins/vega-lite.ts @@ -0,0 +1,16 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { definePlugin, Plugin } from '../factory'; +import { sanitizedHTML } from '../sanitize'; + +export const vegaLitePlugin: Plugin = { + name: 'vega-lite', + initializePlugin: (md) => definePlugin(md, 'vega-lite'), + fence: (token, idx) => { + const vegaLiteId = `vega-lite-${idx}`; + return sanitizedHTML('div', { id: vegaLiteId, class: 'vega-chart' }, token.content.trim()); + }, +}; diff --git a/packages/mdvega/src/plugins/vega.ts b/packages/mdvega/src/plugins/vega.ts new file mode 100644 index 000000000..69ec4a347 --- /dev/null +++ b/packages/mdvega/src/plugins/vega.ts @@ -0,0 +1,389 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { changeset, parse, View, expressionFunction, LoggerInterface } from 'vega'; +import { Batch, IInstance, Plugin, PrioritizedSignal, definePlugin } from '../factory'; +import { sanitizedHTML } from '../sanitize'; +import { BaseSignal, InitSignal, NewSignal, Runtime, Spec, ValuesData } from 'vega-typings'; +import { Resolver, resolveSpec } from '../resolver'; +import { ErrorHandler, Renderer } from '../renderer'; +import { LogLevel } from '../signalbus'; +import { dataNameSelectedSuffix } from './common'; +import { urlParam } from './util'; + +const ignoredSignals = ['width', 'height', 'padding', 'autosize', 'background', 'style', 'parent', 'datum', 'item', 'event', 'cursor']; + +interface SpecInit { + spec: Spec; + initialSignals: PrioritizedSignal[]; + container: Element; + index: number; +} + +interface VegaInstance extends SpecInit { + view: View; + id: string; + batch?: Batch; + dataSignals: string[]; + needToRun?: boolean; +} + +export const vegaPlugin: Plugin = { + name: 'vega', + initializePlugin: (md) => definePlugin(md, 'vega'), + fence: (token, idx) => { + const vegaId = `vega-${idx}`; + return sanitizedHTML('div', { id: vegaId, class: 'vega-chart' }, token.content.trim()); + }, + hydrateComponent: async (renderer, errorHandler) => { + const vegaInstances: VegaInstance[] = []; + const containers = renderer.element.querySelectorAll('.vega-chart'); + const specInits: SpecInit[] = []; + for (const [index, container] of containers.entries()) { + const specInit = await createSpecInit(container, index, renderer, errorHandler); + if (specInit) { + specInits.push(specInit); + } + } + prioritizeSignalValues(specInits); + for (const specInit of specInits) { + const vegaInstance = await createVegaInstance(specInit, renderer, errorHandler); + if (vegaInstance) { + vegaInstances.push(vegaInstance); + } + } + + //make a single array of all the initialSignals that are marked isData + const dataSignals = vegaInstances.map(vegaInstance => vegaInstance.initialSignals.filter(signal => signal.isData)).flat(); + + //spin through all instances to see if its spec has data that matches with dataSignals + for (const vegaInstance of vegaInstances) { + if (!vegaInstance.spec.data) continue; + for (const data of vegaInstance.spec.data) { + //find a matching data signal + const dataSignal = dataSignals.find(signal => + (signal.name === data.name) //exact match + || + (`${signal.name}${dataNameSelectedSuffix}` === data.name), //match a selection from Tabulator + ); + if (dataSignal) { + //if we find a match, add it to our initialSignals + vegaInstance.initialSignals.push({ + name: data.name, + value: (data as ValuesData).values, + priority: (data as ValuesData).values ? 1 : 0, + isData: true, + }); + } + } + } + + const instances: IInstance[] = vegaInstances.map((vegaInstance) => { + const { spec, view, initialSignals } = vegaInstance; + const startBatch = (from: string) => { + if (!vegaInstance.batch) { + renderer.signalBus.log(vegaInstance.id, 'starting batch', from); + vegaInstance.batch = {}; + view.runAfter(() => { + const { batch } = vegaInstance; + vegaInstance.batch = undefined; + renderer.signalBus.log(vegaInstance.id, 'sending batch', batch); + renderer.signalBus.broadcast(vegaInstance.id, batch); + }); + } + }; + return { + ...vegaInstance, + initialSignals, + recieveBatch: async (batch, from) => { + renderer.signalBus.log(vegaInstance.id, 'recieved batch', batch, from); + return new Promise(resolve => { + view.runAfter(async () => { + if (recieveBatch(batch, renderer, vegaInstance)) { + renderer.signalBus.log(vegaInstance.id, 'running after _pulse, changes from', from); + vegaInstance.needToRun = true; + } else { + renderer.signalBus.log(vegaInstance.id, 'no changes'); + } + renderer.signalBus.log(vegaInstance.id, 'running view after _pulse finished'); + resolve(); + }); + }); + }, + broadcastComplete: async () => { + renderer.signalBus.log(vegaInstance.id, 'broadcastComplete'); + if (vegaInstance.needToRun) { + view.runAfter(() => { + view.runAsync(); //do not await, since we are already in a runAfter + vegaInstance.needToRun = false; + renderer.signalBus.log(vegaInstance.id, 'running view after broadcastComplete'); + }); + } + }, + beginListening: (sharedSignals) => { + for (const { isData, signalName } of sharedSignals) { + if (ignoredSignals.includes(signalName)) return; + if (isData) { + const matchData = spec.data?.find(data => data.name === signalName); + if (matchData && vegaInstance.dataSignals.includes(matchData.name)) { + renderer.signalBus.log(vegaInstance.id, 'listening to data', signalName); + view.addDataListener(signalName, async (name, value) => { + startBatch(`data:${signalName}`); + vegaInstance.batch[name] = { value, isData }; + }); + } + } + const matchSignal = spec.signals?.find(signal => signal.name === signalName); + if (matchSignal) { + //only listen to signals that are change sources + const isChangeSource = + (matchSignal as BaseSignal).on || // event streams + (matchSignal as NewSignal).bind || // ui elements + (matchSignal as NewSignal).update // calculations + ; + if (isChangeSource) { + renderer.signalBus.log(vegaInstance.id, 'listening to signal', signalName); + view.addSignalListener(signalName, async (name, value) => { + startBatch(`signal:${signalName}`); + vegaInstance.batch[name] = { value, isData }; + }); + } else { + //renderer.signalBus.log(vegaInstance.id, 'not listening to signal, not a change source', signalName); + } + } else { + //renderer.signalBus.log(vegaInstance.id, 'not listening to signal, no match', signalName); + } + } + }, + getCurrentSignalValue: (signalName: string) => { + const matchSignal = spec.signals?.find(signal => signal.name === signalName); + if (matchSignal) { + return view.signal(signalName); + } else { + return undefined; + } + }, + destroy: async () => { + vegaInstance.view.finalize(); + }, + }; + }); + return instances; + }, +}; + +function recieveBatch(batch: Batch, renderer: Renderer, vegaInstance: VegaInstance) { + const { spec, view } = vegaInstance; + const doLog = renderer.signalBus.logLevel === LogLevel.all; + doLog && renderer.signalBus.log(vegaInstance.id, 'recieveBatch', batch); + let hasAnyChange = false; + for (const signalName in batch) { + const batchItem = batch[signalName]; + if (ignoredSignals.includes(signalName)) { + doLog && renderer.signalBus.log(vegaInstance.id, 'ignoring reverved signal name', signalName, batchItem.value); + continue; + } + if (batchItem.isData) { + let logReason: string; + if (!batchItem.value) { + logReason = 'not updating data, no value'; + } else { + const matchData = spec.data?.find(data => data.name === signalName); + if (!matchData) { + logReason = 'not updating data, no match'; + } else { + logReason = 'updating data'; + view.change(signalName, changeset().remove(() => true).insert(batchItem.value)); + hasAnyChange = true; + } + } + doLog && renderer.signalBus.log(vegaInstance.id, `(isData) ${logReason}`, signalName, batchItem.value); + } + let logReason = ''; + const matchSignal = spec.signals?.find(signal => signal.name === signalName); + if (!matchSignal) { + logReason = 'not updating signal, no match'; + } else { + if ((matchSignal as NewSignal).update) { + logReason = 'not updating signal, it is a calculation'; + } else { + if (isSignalDataBridge(matchSignal)) { + logReason = 'not updating signal, data bridge'; + } else { + const oldValue = view.signal(signalName); + if (oldValue === batchItem.value) { + logReason = 'not updating signal, same value'; + } else { + logReason = 'updating signal'; + view.signal(signalName, batchItem.value); + hasAnyChange = true; + } + } + } + } + doLog && renderer.signalBus.log(vegaInstance.id, logReason, signalName, batchItem.value); + } + return hasAnyChange; +} + +async function createSpecInit(container: Element, index: number, renderer: Renderer, errorHandler: ErrorHandler) { + if (!container.textContent) { + container.innerHTML = '
Expected a spec object or a url
'; + return; + } + + let result: Resolver; + try { + result = await resolveSpec(container.textContent); + } catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'vega', index, 'resolve', container); + return; + } + if (result.error) { + container.innerHTML = `
${result.error.toString()}
`; + errorHandler(result.error, 'vega', index, 'resolve', container); + return; + } + if (!result.spec) { + container.innerHTML = '
Expected a spec object
'; + return; + } + const { spec } = result; + const initialSignals: PrioritizedSignal[] = spec.signals?.map((signal: InitSignal | NewSignal) => { + if (ignoredSignals.includes(signal.name)) return; + let isData = isSignalDataBridge(signal as NewSignal); + //support legacy dataPrefix + if (signal.name.startsWith(renderer.options.dataSignalPrefix)) { + isData = true; + } + return { + name: signal.name, + value: signal.value, + priority: signal.bind ? 1 : 0, + isData, + }; + }).filter(Boolean) || []; + const specInit: SpecInit = { container, index, initialSignals, spec }; + return specInit; +} + +async function createVegaInstance(specInit: SpecInit, renderer: Renderer, errorHandler: ErrorHandler) { + const { container, index, initialSignals, spec } = specInit; + const id = `vega-${index}`; + + let runtime: Runtime; + let view: View; + + try { + runtime = parse(spec); + } catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'vega', index, 'parse', container); + return; + } + + try { + view = new View(runtime, { + container, + renderer: renderer.options.vegaRenderer, + logger: new VegaLogger(error => { + errorHandler(error, 'vega', index, 'view', container); + }), + }); + view.run(); + + //fix up initial signals + for (const signal of initialSignals) { + if (signal.isData) continue; //skip data signals + const currentValue = view.signal(signal.name); + if (currentValue !== signal.value) { + renderer.signalBus.log(id, 're-setting initial signal', signal.name, signal.value, currentValue); + signal.value = currentValue; + } + } + + } catch (e) { + container.innerHTML = `
${e.toString()}
`; + errorHandler(e, 'vega', index, 'view', container); + return; + } + + //make a dataSignals array that is made of all the signals that are marked as isData, where the name is in the spec data + const dataSignals = initialSignals.filter(signal => signal.isData && spec.data?.some(data => data.name === signal.name)).map(signal => signal.name); + + const instance: VegaInstance = { ...specInit, view, id, dataSignals }; + return instance; +} + +function isSignalDataBridge(signal: NewSignal) { + return signal.update === `data('${signal.name}')`; +} + +function prioritizeSignalValues(specInits: SpecInit[]) { + const highPrioritySignals = specInits.map(specInit => specInit.initialSignals.filter(signal => signal.priority > 0)).flat(); + for (const specInit of specInits) { + for (const prioritySignal of highPrioritySignals) { + const matchSignal = specInit.spec.signals?.find(signal => signal.name === prioritySignal.name); + if (matchSignal && (matchSignal as NewSignal).value !== undefined && (matchSignal as NewSignal).value !== prioritySignal.value) { + (matchSignal as NewSignal).value = prioritySignal.value; + } + } + } +} + +expressionFunction('urlParam', urlParam); + +class VegaLogger implements LoggerInterface { + private logLevel: number = 0; + + constructor(private errorHandler: (error: Error) => void) { + this.error = this.error.bind(this); + this.warn = this.warn.bind(this); + this.info = this.info.bind(this); + this.debug = this.debug.bind(this); + } + + level(level: number): this; + level(): number; + level(level?: number): this | number { + if (level === undefined) { + return this.logLevel; + } + this.logLevel = level; + return this; + } + + error(...args: readonly any[]) { + if (this.errorHandler) { + this.errorHandler(args[0]); + } + if (this.logLevel >= 1) { + console.error(...args); + } + return this; + } + + warn(...args: readonly any[]) { + if (this.logLevel >= 2) { + console.warn(...args); + } + return this; + } + + info(...args: readonly any[]) { + if (this.logLevel >= 3) { + console.info(...args); + } + return this; + } + + debug(...args: readonly any[]) { + if (this.logLevel >= 4) { + console.debug(...args); + } + return this; + } +} diff --git a/packages/mdvega/src/renderer.ts b/packages/mdvega/src/renderer.ts new file mode 100644 index 000000000..54400492c --- /dev/null +++ b/packages/mdvega/src/renderer.ts @@ -0,0 +1,104 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import MarkdownIt from 'markdown-it'; +import { Renderers } from 'vega-typings/types'; +import { create, IInstance, plugins } from './factory'; +import { SignalBus } from './signalbus'; + +export interface ErrorHandler { + (error: Error, pluginName: string, instanceIndex: number, phase: string, container: Element, detail?: string): void; +} + +export interface RendererOptions { + vegaRenderer?: Renderers; + dataSignalPrefix?: string; + signalBus?: SignalBus; + classList?: string[]; +} + +const defaultRendererOptions: RendererOptions = { + vegaRenderer: 'canvas', + dataSignalPrefix: 'data-signal:', + classList: ['markdown-block'], +}; + +interface Hydration { + pluginName: string; + instances: IInstance[]; +} + +export class Renderer { + + public md: MarkdownIt; + public instances: { [key: string]: IInstance[] }; + public signalBus: SignalBus; + public options: RendererOptions; + + constructor(public element: HTMLElement, options?: RendererOptions) { + this.options = { ...defaultRendererOptions, ...options }; + this.md = create({ classList: this.options.classList }); + this.signalBus = this.options.signalBus || new SignalBus(this.options.dataSignalPrefix); + this.instances = {}; + } + + async render(markdown: string, errorHandler?: ErrorHandler) { + if (!errorHandler) { + errorHandler = (error, pluginName, instanceIndex, phase) => { + console.error(`Error in plugin ${pluginName} instance ${instanceIndex} phase ${phase}`, error); + }; + } + + //loop through all the destroy handlers and call them. have the key there to help us debug + await this.destroy(); + + const parsedHTML = this.md.render(markdown); + this.element.innerHTML = parsedHTML; + + //loop through all the plugins and render them + this.signalBus.log('Renderer', 'rendering DOM'); + const hydrationPromises: Promise[] = []; + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.hydrateComponent) { + //make a new promise that returns IInstances but adds the plugin name + hydrationPromises.push(plugin.hydrateComponent(this, errorHandler).then(instances => { + return { + pluginName: plugin.name, + instances, + }; + })); + } + } + + try { + const pluginHydrations = await Promise.all(hydrationPromises); + for (const hydration of pluginHydrations) { + if (hydration && hydration.instances) { + this.instances[hydration.pluginName] = hydration.instances; + //registration phase + for (const instance of hydration.instances) { + this.signalBus.registerPeer(instance); + } + } + } + this.signalBus.beginListening(); + } catch (error) { + console.error('Error in rendering plugins', error); + } + } + + async destroy() { + this.signalBus.reset(); + for (const pluginName of Object.keys(this.instances)) { + const instances = this.instances[pluginName]; + for (const instance of instances) { + instance.destroy && await instance.destroy(); + } + } + this.instances = {}; + } + +} diff --git a/packages/mdvega/src/resolver.ts b/packages/mdvega/src/resolver.ts new file mode 100644 index 000000000..d98500a6f --- /dev/null +++ b/packages/mdvega/src/resolver.ts @@ -0,0 +1,57 @@ +import { Spec } from 'vega'; +import { compile } from 'vega-lite'; + +export interface Resolver { + spec?: Spec; + error?: Error; +} + +export async function resolveSpec(textContent: string): Promise { + try { + const either = JSON.parse(textContent); + if (typeof either === 'object') { + return resolveToVega(either); + } else { + return { error: new Error(`Spec must be either a JSON object or a string url, found type ${typeof either}`) }; + } + } catch (error) { + //see if it is a url, then await to load the json for a spec + if (textContent.startsWith('http://') || textContent.startsWith('https://') || textContent.startsWith('//')) { + try { + const response = await fetch(textContent); + const either = await response.json(); + if (typeof either === 'object') { + return resolveToVega(either); + } else { + return { error: new Error(`Expected a JSON object, found type ${typeof either}`) }; + } + } catch (error) { + return { error }; + } + } else { + return { error: new Error('Spec string must be a url') }; + } + } +} + +function resolveToVega(either: any): Resolver { + if ('$schema' in either && typeof either.$schema === 'string') { + if (either.$schema.includes('vega-lite')) { + //compile to vega + try { + const runtime = compile(either); + const { spec } = runtime; + return { spec }; + } catch (error) { + //did not compile + return { error }; + } + } else if (either.$schema.includes('vega')) { + return { spec: either as Spec }; + } else { + return { error: new Error('$schema property must be a string with vega or vega-lite version.') }; + } + } else { + return { error: new Error('Missing $schema property, must be a string with vega or vega-lite version.') }; + } +} diff --git a/packages/mdvega/src/sanitize.ts b/packages/mdvega/src/sanitize.ts new file mode 100644 index 000000000..6499a9269 --- /dev/null +++ b/packages/mdvega/src/sanitize.ts @@ -0,0 +1,21 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +export function sanitizedHTML(tagName: string, attributes: { [key: string]: string }, content: string) { + + // Create a temp element with the specified tag name + const element = document.createElement(tagName); + + // Iterate over the attribute list and set each attribute + Object.keys(attributes).forEach(key => { + element.setAttribute(key, attributes[key]); + }); + + // Set the textContent to automatically escape the content + element.textContent = content; + + // Return the outer HTML of the element + return element.outerHTML; +} diff --git a/packages/mdvega/src/signalbus.ts b/packages/mdvega/src/signalbus.ts new file mode 100644 index 000000000..6e4529a51 --- /dev/null +++ b/packages/mdvega/src/signalbus.ts @@ -0,0 +1,183 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { Batch, IInstance } from "./factory"; + +export enum LogLevel { + none = 0, + some = 1, + all = 2, +} + +// Signal Bus to manage shared signals +export class SignalBus { + public broadcastingStack: string[]; + public logLevel: LogLevel; + public logWatchIds: string[]; + public active: boolean; + public peers: IInstance[]; + public signalDeps: { [signalName: string]: { value: unknown; priority: number; initialPriorityId: string; isData: boolean; deps: IInstance[]; } }; + public peerDependencies: { [peerId: string]: string[] }; + + constructor(public dataSignalPrefix: string) { + this.logLevel = LogLevel.none; + this.logWatchIds = []; + this.reset(); + } + + public log(id: string, message: string, ...optionalParams: unknown[]) { + if (this.logLevel === LogLevel.none) return; + if (this.logWatchIds.length > 0 && !this.logWatchIds.includes(id)) return; + console.log(`[Signal Bus][${id}] ${message}`, ...optionalParams); + } + + async broadcast(originId: string, batch: Batch) { + //TODO handle multiple broadcasts + + //TODO handle circular dependencies + + if (this.broadcastingStack.includes(originId)) { + this.log(originId, 'Additional broadcast from', originId, this.broadcastingStack.join(' -> ')); + //return; + } + + this.log(originId, 'Broadcasting batch from', originId, batch); + this.broadcastingStack.push(originId); + for (const peerId of this.peerDependencies[originId]) { + const peer = this.peers.find(p => p.id === peerId); + + //create a new batch with the signals that the peer is interested in + const peerBatch: Batch = {}; + let hasBatch = false; + for (const signalName in batch) { + if ( + peer.initialSignals.some(s => s.name === signalName) + && batch[signalName].value !== this.signalDeps[signalName].value + ) { + peerBatch[signalName] = batch[signalName]; + hasBatch = true; + } + } + + if (!hasBatch) continue; + peer.recieveBatch && await peer.recieveBatch(peerBatch, originId); + } + this.broadcastingStack.pop(); + + //set current values + for (const signalName in batch) { + const signalDep = this.signalDeps[signalName]; + signalDep.value = batch[signalName].value; + } + + if (this.broadcastingStack.length === 0) { + //broadcast complete + for (const peer of this.peers) { + peer.broadcastComplete && await peer.broadcastComplete(); + } + } + } + + getPriorityPeer(signalName: string) { + const signalDep = this.signalDeps[signalName]; + if (!signalDep) return null; + return this.peers.find(p => p.id === signalDep.initialPriorityId); + } + + registerPeer(peer: IInstance) { + this.peers.push(peer); + for (const initialSignal of peer.initialSignals) { + if (!(initialSignal.name in this.signalDeps)) { + //first encounter with this signal + this.signalDeps[initialSignal.name] = { + deps: [peer], + priority: initialSignal.priority, + initialPriorityId: peer.id, + value: initialSignal.value, + isData: initialSignal.isData, + }; + } else { + //signal exists, add the peer to the deps and check to override the priority + const signalDep = this.signalDeps[initialSignal.name]; + if (!signalDep.deps.includes(peer)) { + signalDep.deps.push(peer); + } + if (initialSignal.priority > signalDep.priority) { + signalDep.priority = initialSignal.priority; + signalDep.initialPriorityId = peer.id; + signalDep.value = initialSignal.value; + signalDep.isData = initialSignal.isData; + } + } + } + } + + beginListening() { + //set the initial batch on each peer + this.log('beginListening', 'begin initial batch', this.signalDeps); + + for (const peer of this.peers) { + const batch: Batch = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + const { value, isData } = signalDep; + batch[signalName] = { value, isData }; + } + peer.recieveBatch && peer.recieveBatch(batch, 'initial'); + } + + this.log('beginListening', 'end initial batch'); + + //for all signalDeps, compile a list of signals for each peer depending on the signal + const peerSignals: { [peerId: string]: { signalName: string, isData: boolean }[] } = {}; + + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + if (signalDep.deps.length === 1) continue; // No need to share signals if only one peer depends on it + + for (const peer of signalDep.deps) { + if (!(peer.id in peerSignals)) { + peerSignals[peer.id] = []; + this.peerDependencies[peer.id] = []; + } + peerSignals[peer.id].push({ signalName, isData: signalDep.isData }); + + // Add other peers sharing this signal to the dependencies map + for (const otherPeer of signalDep.deps) { + if (otherPeer.id !== peer.id && !this.peerDependencies[peer.id].includes(otherPeer.id)) { + this.peerDependencies[peer.id].push(otherPeer.id); + } + } + } + } + + this.log('beginListening', '======= dependencies =========', peerSignals, this.peerDependencies); + + // Begin listening and logging the shared dependencies + for (const peer of this.peers) { + const sharedSignals = peerSignals[peer.id]; + if (sharedSignals) { + this.log(peer.id, 'Shared signals:', sharedSignals); + if (this.peerDependencies[peer.id]) { + this.log(peer.id, 'Shared dependencies:', this.peerDependencies[peer.id]); + } + peer.beginListening && peer.beginListening(sharedSignals); + } else { + this.log(peer.id, 'No shared signals'); + } + } + + this.active = true; + } + + reset() { + this.signalDeps = {}; + this.active = false; + this.peers = []; + this.broadcastingStack = []; + this.peerDependencies = {}; + } + +} diff --git a/packages/mdvega/test/index.html b/packages/mdvega/test/index.html new file mode 100644 index 000000000..ba8210dd8 --- /dev/null +++ b/packages/mdvega/test/index.html @@ -0,0 +1,361 @@ + + + + + + + MdVega test + + + + + + + + + + + + + +
+ + +
+
+ + + + + + \ No newline at end of file diff --git a/packages/mdvega/tsconfig.json b/packages/mdvega/tsconfig.json new file mode 100644 index 000000000..5eae73cbb --- /dev/null +++ b/packages/mdvega/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "moduleResolution": "node", + "outDir": "dist/es6", + "skipLibCheck": true, + "target": "es6", + "allowSyntheticDefaultImports": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/sanddance/src/types.ts b/packages/sanddance/src/types.ts index 44dbda2db..0204c6875 100644 --- a/packages/sanddance/src/types.ts +++ b/packages/sanddance/src/types.ts @@ -409,4 +409,3 @@ export interface InsightSetup { insight: Insight; setup?: Setup; } - diff --git a/scripts/vega-version.mjs b/scripts/vega-version.mjs index 62649f695..46a2ddcf8 100644 --- a/scripts/vega-version.mjs +++ b/scripts/vega-version.mjs @@ -44,6 +44,12 @@ const packages = { `vega@^${major}.${minor}`, ], }, + { + glob: 'packages/mdvega/test/index.html', + pattern: ({ major, minor }) => [ + `vega@^${major}.${minor}`, + ], + }, { glob: 'docs/tests/{data-inference,sanddance-specs}/v1/index.html', pattern: ({ major, minor }) => [