UNPKG

markmap-lib

Version:

Visualize your Markdown as mindmaps with Markmap

373 lines (332 loc) 12.3 kB
"use strict"; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); exports.__esModule = true; exports.markmap = markmap; exports.loadPlugins = loadPlugins; exports.plugins = exports.Markmap = void 0; var d3 = _interopRequireWildcard(require("d3")); var _d3Flextree = require("d3-flextree"); var _util = require("./util"); var plugins = _interopRequireWildcard(require("./plugins")); exports.plugins = plugins; var _hook = require("./util/hook"); function linkWidth(nodeData) { const data = nodeData.data; return Math.max(6 - 2 * data.d, 1.5); } function adjustSpacing(tree, spacing) { (0, _util.walkTree)(tree, (d, next) => { d.ySizeInner = d.ySize - spacing; d.y += spacing; next(); }, 'children'); } class Markmap { constructor(svg, opts) { this.options = void 0; this.state = void 0; this.svg = void 0; this.styleNode = void 0; this.g = void 0; this.zoom = void 0; ['handleZoom', 'handleClick'].forEach(key => { this[key] = this[key].bind(this); }); this.svg = svg.datum ? svg : d3.select(svg); this.styleNode = this.svg.append('style'); this.zoom = d3.zoom().on('zoom', this.handleZoom); this.options = Object.assign({ duration: 500, nodeFont: '300 16px/20px sans-serif', nodeMinHeight: 16, spacingVertical: 5, spacingHorizontal: 80, autoFit: false, fitRatio: 0.95, color: (colorFn => node => colorFn(node.p.i))(d3.scaleOrdinal(d3.schemeCategory10)), paddingX: 8 }, opts); this.state = { id: this.options.id || (0, _util.getId)() }; this.g = this.svg.append('g').attr('class', `${this.state.id}-g`); this.updateStyle(); this.svg.call(this.zoom); } getStyleContent() { const { style, nodeFont } = this.options; const { id } = this.state; const extraStyle = typeof style === 'function' ? style(id) : ''; const styleText = `\ .${id} a { color: #0097e6; } .${id} a:hover { color: #00a8ff; } .${id}-g > path { fill: none; } .${id}-fo > div { font: ${nodeFont}; white-space: nowrap; } .${id}-fo code { padding: .2em .4em; font-size: calc(1em - 2px); color: #555; background-color: #f0f0f0; border-radius: 2px; } .${id}-fo del { text-decoration: line-through; } .${id}-fo em { font-style: italic; } .${id}-fo strong { font-weight: 500; } .${id}-fo pre { margin: 0; } .${id}-fo pre[class*=language-] { padding: 0; } .${id}-g > g { cursor: pointer; } ${extraStyle} `; return styleText; } updateStyle() { this.svg.attr('class', (0, _util.addClass)(this.svg.attr('class'), this.state.id)); this.styleNode.text(this.getStyleContent()); } handleZoom() { const { transform } = d3.event; this.g.attr('transform', transform); } handleClick(d) { var _data$p; const { data } = d; data.p = Object.assign(Object.assign({}, data.p), {}, { f: !((_data$p = data.p) == null ? void 0 : _data$p.f) }); this.renderData(d.data); } initializeData(node) { let i = 0; const { nodeFont, color, nodeMinHeight } = this.options; const { id } = this.state; const container = document.createElement('div'); const containerClass = `${id}-container`; container.className = (0, _util.addClass)(container.className, `${id}-fo`, containerClass); const style = document.createElement('style'); style.textContent = ` ${this.getStyleContent()} .${containerClass} { position: absolute; width: 0; height: 0; top: -100px; left: -100px; overflow: hidden; font: ${nodeFont}; } .${containerClass} > div { display: inline-block; } `; document.body.append(style, container); (0, _util.walkTree)(node, (item, next) => { var _item$c; item.c = (_item$c = item.c) == null ? void 0 : _item$c.map(child => Object.assign({}, child)); i += 1; const el = document.createElement('div'); el.innerHTML = item.v; container.append(el); item.p = Object.assign(Object.assign({}, item.p), {}, { // unique ID i, el }); color(item); // preload colors next(); }); const nodes = (0, _util.arrayFrom)(container.childNodes); this.constructor.transformHtml.call(this, nodes); (0, _util.walkTree)(node, (item, next, parent) => { var _parent$p; const rect = item.p.el.getBoundingClientRect(); item.v = item.p.el.innerHTML; item.p.s = [Math.ceil(rect.width), Math.max(Math.ceil(rect.height), nodeMinHeight)]; // TODO keep keys for unchanged objects // unique key, should be based on content item.p.k = `${(parent == null ? void 0 : (_parent$p = parent.p) == null ? void 0 : _parent$p.i) || ''}.${item.p.i}:${item.v}`; next(); }); container.remove(); style.remove(); } setOptions(opts) { Object.assign(this.options, opts); } setData(data, opts) { if (!data) data = Object.assign({}, this.state.data); this.state.data = data; this.initializeData(data); if (opts) this.setOptions(opts); this.renderData(); } renderData(originData) { var _origin$data$p$x, _origin$data$p$y; if (!this.state.data) return; const { spacingHorizontal, paddingX, spacingVertical, autoFit, color } = this.options; const { id } = this.state; const layout = (0, _d3Flextree.flextree)().children(d => { var _d$p; return !((_d$p = d.p) == null ? void 0 : _d$p.f) && d.c; }).nodeSize(d => { const [width, height] = d.data.p.s; return [height, width + (width ? paddingX * 2 : 0) + spacingHorizontal]; }).spacing((a, b) => { return a.parent === b.parent ? spacingVertical : spacingVertical * 2; }); const tree = layout.hierarchy(this.state.data); layout(tree); adjustSpacing(tree, spacingHorizontal); const descendants = tree.descendants().reverse(); const links = tree.links(); const linkShape = d3.linkHorizontal(); const minX = d3.min(descendants, d => d.x - d.xSize / 2); const maxX = d3.max(descendants, d => d.x + d.xSize / 2); const minY = d3.min(descendants, d => d.y); const maxY = d3.max(descendants, d => d.y + d.ySizeInner); Object.assign(this.state, { minX, maxX, minY, maxY }); if (autoFit) this.fit(); const origin = originData && descendants.find(item => item.data === originData) || tree; const x0 = (_origin$data$p$x = origin.data.p.x0) != null ? _origin$data$p$x : origin.x; const y0 = (_origin$data$p$y = origin.data.p.y0) != null ? _origin$data$p$y : origin.y; // Update the nodes const node = this.g.selectAll((0, _util.childSelector)('g')).data(descendants, d => d.data.p.k); const nodeEnter = node.enter().append('g').attr('transform', d => `translate(${y0 + origin.ySizeInner - d.ySizeInner},${x0 + origin.xSize / 2 - d.xSize})`).on('click', this.handleClick); const nodeExit = this.transition(node.exit()); nodeExit.select('rect').attr('width', 0).attr('x', d => d.ySizeInner); nodeExit.select('foreignObject').style('opacity', 0); nodeExit.attr('transform', d => `translate(${origin.y + origin.ySizeInner - d.ySizeInner},${origin.x + origin.xSize / 2 - d.xSize})`).remove(); const nodeMerge = node.merge(nodeEnter); this.transition(nodeMerge).attr('transform', d => `translate(${d.y},${d.x - d.xSize / 2})`); const rect = nodeMerge.selectAll((0, _util.childSelector)('rect')).data(d => [d], d => d.data.p.k).join(enter => { return enter.append('rect').attr('x', d => d.ySizeInner).attr('y', d => d.xSize - linkWidth(d) / 2).attr('width', 0).attr('height', linkWidth); }, update => update, exit => exit.remove()); this.transition(rect).attr('x', -1).attr('width', d => d.ySizeInner + 2).attr('fill', d => color(d.data)); const circle = nodeMerge.selectAll((0, _util.childSelector)('circle')).data(d => d.data.c ? [d] : [], d => d.data.p.k).join(enter => { return enter.append('circle').attr('stroke-width', '1.5').attr('cx', d => d.ySizeInner).attr('cy', d => d.xSize).attr('r', 0); }, update => update, exit => exit.remove()); this.transition(circle).attr('r', 6).attr('stroke', d => color(d.data)).attr('fill', d => { var _d$data$p; return ((_d$data$p = d.data.p) == null ? void 0 : _d$data$p.f) ? color(d.data) : '#fff'; }); const foreignObject = nodeMerge.selectAll((0, _util.childSelector)('foreignObject')).data(d => [d], d => d.data.p.k).join(enter => { const fo = enter.append('foreignObject').attr('class', `${id}-fo`).attr('x', paddingX).attr('y', 0).style('opacity', 0).attr('height', d => d.xSize); fo.append('xhtml:div').select(function (d) { const node = d.data.p.el.cloneNode(true); this.replaceWith(node); return node; }).attr('xmlns', 'http://www.w3.org/1999/xhtml'); return fo; }, update => update, exit => exit.remove()).attr('width', d => Math.max(0, d.ySizeInner - paddingX * 2)); this.transition(foreignObject).style('opacity', 1); // Update the links const path = this.g.selectAll((0, _util.childSelector)('path')).data(links, d => d.target.data.p.k).join(enter => { const source = [y0 + origin.ySizeInner, x0 + origin.xSize / 2]; return enter.insert('path', 'g').attr('d', linkShape({ source, target: source })); }, update => update, exit => { const source = [origin.y + origin.ySizeInner, origin.x + origin.xSize / 2]; return this.transition(exit).attr('d', linkShape({ source, target: source })).remove(); }); this.transition(path).attr('stroke', d => color(d.target.data)).attr('stroke-width', d => linkWidth(d.target)).attr('d', d => { const source = [d.source.y + d.source.ySizeInner, d.source.x + d.source.xSize / 2]; const target = [d.target.y, d.target.x + d.target.xSize / 2]; return linkShape({ source, target }); }); descendants.forEach(d => { d.data.p.x0 = d.x; d.data.p.y0 = d.y; }); } transition(sel) { const { duration } = this.options; return sel.transition().duration(duration); } fit() { const svgNode = this.svg.node(); const { width: offsetWidth, height: offsetHeight } = svgNode.getBoundingClientRect(); const { fitRatio } = this.options; const { minX, maxX, minY, maxY } = this.state; const naturalWidth = maxY - minY; const naturalHeight = maxX - minX; const scale = Math.min(offsetWidth / naturalWidth * fitRatio, offsetHeight / naturalHeight * fitRatio, 2); const initialZoom = d3.zoomIdentity.translate((offsetWidth - naturalWidth * scale) / 2 - minY * scale, (offsetHeight - naturalHeight * scale) / 2 - minX * scale).scale(scale); return this.transition(this.svg).call(this.zoom.transform, initialZoom).end(); } rescale(scale) { const svgNode = this.svg.node(); const { width: offsetWidth, height: offsetHeight } = svgNode.getBoundingClientRect(); const halfWidth = offsetWidth / 2; const halfHeight = offsetHeight / 2; const transform = d3.zoomTransform(svgNode); const newTransform = transform.translate((halfWidth - transform.x) * (1 - scale) / transform.k, (halfHeight - transform.y) * (1 - scale) / transform.k).scale(scale); return this.transition(this.svg).call(this.zoom.transform, newTransform).end(); } static create(svg, opts, data) { const mm = new Markmap(svg, opts); if (data) { mm.setData(data); mm.fit(); // always fit for the first render } return mm; } } exports.Markmap = Markmap; Markmap.transformHtml = new _hook.Hook(); function markmap(svg, data, opts) { return Markmap.create(svg, opts, data); } async function loadPlugins(items, options) { items = items.map(item => { if (typeof item === 'string') { const name = item; item = plugins[name]; if (!item) { console.warn(`[markmap] Unknown plugin: ${name}`); } } return item; }).filter(Boolean); return (0, _util.initializePlugins)(Markmap, items, options); }