penframe
Version:
A lightweight DSL-based wireframe and UI structure visualization tool.
788 lines (704 loc) • 27.4 kB
JavaScript
function astToSvg(ast) {
if (!Array.isArray(ast) || ast.length === 0) {
throw new Error('AST must be a non-empty array');
}
const appNode = ast.find(n => n.type === 'config') || {};
const TITLE_HEIGHT = 60;
const appWidth = appNode.width || 1200;
const appHeight = appNode.height || 800;
const title = appNode.title || 'Penframe SVG';
const svgWidth = appWidth;
const svgHeight = appHeight + TITLE_HEIGHT;
const rootElements = ast.filter(n => n.type !== 'config');
const body = renderElements(rootElements, { width: appWidth, height: appHeight });
return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
<style>
.container { stroke: #333; stroke-width: 1; }
.app-area { fill: #ffffff; stroke: #cccccc; stroke-width: 1; }
.headline { font-family: sans-serif; dominant-baseline: middle; }
.paragraph { font-family: sans-serif; dominant-baseline: middle; }
.title { font-family: sans-serif; font-weight: bold; dominant-baseline: middle; }
.button { font-family: sans-serif; }
.form-label { font-family: sans-serif; font-size: 12px; }
.form-control { stroke: #333; stroke-width: 1; }
.placeholder-text { font-family: sans-serif; fill: #999; }
.list-item-text { font-family: sans-serif; font-size: 16px; }
.table-header { font-family: sans-serif; font-weight: bold; }
.table-cell { font-family: sans-serif; }
</style>
<text class="title" x="${svgWidth / 2}" y="${TITLE_HEIGHT / 2}" text-anchor="middle" font-size="20">${escapeXml(title)}</text>
<g class="app-group" transform="translate(0, ${TITLE_HEIGHT})">
<rect class="app-area" x="0" y="0" width="${appWidth}" height="${appHeight}" />
${body}
</g>
</svg>`;
}
function renderElements(elements, parent) {
if (!elements || elements.length === 0) {
return '';
}
let defaultY = 40;
const headlineYStep = 40;
const paragraphYStep = 20;
const buttonYStep = 40;
const formControlYStep = 60;
const listYStep = 80;
const imageYStep = 150;
const tableYStep = 200;
return elements.map(node => {
if (node.type === 'container') {
const y = node.y ?? defaultY;
const height = node.height || 100;
if (node.y === undefined) {
defaultY += height;
}
return renderContainer(node, parent, y);
}
if (node.type === 'headline') {
const x = node.x ?? parent.width / 2;
const y = node.y ?? defaultY;
if (node.y === undefined) {
defaultY += headlineYStep;
}
return renderHeadline(node, x, y);
}
if (node.type === 'paragraph') {
const y = node.y ?? defaultY;
let height = paragraphYStep;
const fontSize = 14;
if (node.width) {
const lines = getWrappedLines(node.value, node.width, fontSize);
height = lines.length * fontSize * 1.2;
}
if (node.y === undefined) {
defaultY += height;
}
return renderParagraph(node, parent, y);
}
if (node.type === 'button') {
const y = node.y ?? defaultY;
if (node.y === undefined) {
defaultY += buttonYStep;
}
return renderButton(node, parent, y);
}
if (node.type === 'formcontrol') {
const y = node.y ?? defaultY;
if (node.y === undefined) {
let controlHeight = 30; // Default height
const labelHeight = 18;
let step = labelHeight;
const items = node.items;
if (node.control === 'textbox' && node.rows > 1) {
controlHeight = (node.rows * 16) + 14;
} else if (node.control === 'select' && node.multiple === true && Array.isArray(items)) {
controlHeight = (items.length * 16) + 14;
} else if ((node.control === 'checkbox' || node.control === 'radio') && Array.isArray(items)) {
const direction = node.direction || 'column';
if (direction === 'column') {
controlHeight = items.length * 25;
} else { // row
controlHeight = 25;
}
}
step += controlHeight + 20; // Add control height and bottom margin
defaultY += step;
}
return renderFormControl(node, parent, y);
}
if (node.type === 'list') {
const y = node.y ?? defaultY;
if (node.y === undefined) {
let step = listYStep;
if (Array.isArray(node.items)) {
const direction = node.direction || 'column';
if (direction === 'column') {
step = (node.items.length * 20) + 20;
}
}
defaultY += step;
}
return renderList(node, parent, y);
}
if (node.type === 'image') {
const y = node.y ?? defaultY;
if (node.y === undefined) {
let step = node.height ? node.height + 20 : imageYStep;
defaultY += step;
}
return renderImage(node, parent, y);
}
if (node.type === 'table') {
const y = node.y ?? defaultY;
if (node.y === undefined) {
defaultY += tableYStep;
}
return renderTable(node, parent, y);
}
if (node.type === 'tabs') {
const y = node.y ?? defaultY;
if (node.y === undefined) {
const tabHeight = 40; // タブヘッダーの高さ
const tabMargin = 20; // 次のコンポーネントとのマージン
defaultY += tabHeight + tabMargin;
}
return renderTabs(node, parent, y);
}
if (node.type === 'badge') {
const y = node.y ?? defaultY;
if (node.y === undefined) {
const badgeHeight = 25; // バッジの高さ
const badgeMargin = 15; // 次のコンポーネントとのマージン
defaultY += badgeHeight + badgeMargin;
}
return renderBadge(node, parent, y);
}
if (node.type === 'hr') {
const y = node.y ?? defaultY;
if (node.y === undefined) {
defaultY += 20; // hrの高さ分を追加
}
return renderHR(node, parent, y);
}
if (node.type === 'spacer') {
const height = node.height || 16; // デフォルトの高さ
if (node.y === undefined) {
defaultY += height;
}
return renderSpacer(node, parent);
}
return '';
}).join('\\n');
}
function renderContainer(node, parent, y) {
const x = node.x || 0;
const width = node.width ?? (parent.width - x);
const height = node.height || 100;
const color = node.color || 'transparent';
const childrenSvg = renderElements(node.children, { width, height });
return `
<g transform="translate(${x}, ${y})">
<rect class="container" x="0" y="0" width="${width}" height="${height}" fill="${escapeXml(color)}" />
${childrenSvg}
</g>
`;
}
function renderHeadline(node, x, y) {
const level = node.level || 2;
const color = node.color || '#333333';
let fontSize, fontWeight;
switch (level) {
case 1: fontSize = 24; fontWeight = 'bold'; break;
case 2: fontSize = 18; fontWeight = 'bold'; break;
default: fontSize = 14; fontWeight = 'normal';
}
return `
<text class="headline" x="${x}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" fill="${escapeXml(color)}" text-anchor="middle">
${escapeXml(node.value)}
</text>
`;
}
function renderParagraph(node, parent, y) {
const align = node.align || 'left';
let x;
let textAnchor;
switch (align) {
case 'center':
x = parent.width / 2 + (node.x || 0);
textAnchor = 'middle';
break;
case 'right':
x = parent.width + (node.x || 0);
textAnchor = 'end';
break;
case 'left':
default:
x = 0 + (node.x || 0);
textAnchor = 'start';
break;
}
const color = node.color || '#333333';
const fontSize = 14;
const fontWeight = 'normal';
const width = node.width;
let textContent;
let textX = x;
if (width) {
textX = 0; // x is applied to tspan, not the text element
const lines = getWrappedLines(node.value, width, fontSize);
textContent = lines.map((line, index) => {
const dy = index === 0 ? 0 : '1.2em';
return `<tspan x="${x}" dy="${dy}">${escapeXml(line)}</tspan>`;
}).join('');
} else {
textContent = escapeXml(node.value);
}
return `
<text class="paragraph" x="${textX}" y="${y}" font-size="${fontSize}" font-weight="${fontWeight}" fill="${escapeXml(color)}" text-anchor="${textAnchor}">
${textContent}
</text>
`;
}
function renderButton(node, parent, y) {
const align = node.align || 'left';
const padding = 20;
const height = 30;
const fontSize = 14;
// Note: This is a simplified text width calculation
const textLength = node.value.length * fontSize * 0.6;
const width = node.width || textLength + padding * 2;
let rectX;
switch (align) {
case 'center':
rectX = (parent.width / 2) - (width / 2) + (node.x || 0);
break;
case 'right':
rectX = parent.width - width + (node.x || 0);
break;
case 'left':
default:
rectX = 0 + (node.x || 0);
break;
}
const color = node.color || '#cccccc';
const textColor = node.textColor || '#000000';
const textX = rectX + width / 2;
return `
<g transform="translate(0, ${y})">
<rect x="${rectX}" y="-${height/2}" width="${width}" height="${height}" fill="${escapeXml(color)}" rx="5" />
<text class="button" x="${textX}" y="0" font-size="${fontSize}" fill="${escapeXml(textColor)}" text-anchor="middle" dominant-baseline="middle">
${escapeXml(node.value)}
</text>
</g>
`;
}
function renderFormControl(node, parent, y) {
const x = node.x ?? 20;
const label = node.label || '';
const labelPosition = node.position || 'top';
const labelWidth = 120;
let controlX = x;
// --- Calculate control height first ---
let controlHeight = 30; // default
let items = node.items;
const direction = node.direction || 'column';
if (node.control === 'textbox' && node.rows > 1) {
controlHeight = (node.rows * 16) + 14;
} else if (node.control === 'select' && node.multiple === true && Array.isArray(items)) {
controlHeight = (items.length * 16) + 14;
} else if ((node.control === 'checkbox' || node.control === 'radio') && Array.isArray(items)) {
if (direction === 'column') {
controlHeight = items.length * 25;
} else {
controlHeight = 25; // height of a single row
}
}
if (label && labelPosition === 'left' && (node.control === 'textbox' || node.control === 'select' || node.control === 'calendar')) {
controlX = x + labelWidth;
}
const width = node.width ?? ((node.control === 'textbox' || node.control === 'select' || node.control === 'calendar') ? parent.width - controlX - 20 : null);
let labelSvg = '';
const labelHeight = 18; // font-size + margin
let controlTopY = y; // The top Y-coord for the control part
// --- Draw label at the top ---
if (label) {
if (labelPosition === 'left') {
const labelY = y + controlHeight / 2;
labelSvg = `<text class="form-label" x="${x}" y="${labelY}" dominant-baseline="middle">${escapeXml(label)}</text>`;
controlTopY = y;
} else { // top
const labelTextY = y + 12; // 12 is font-size
labelSvg = `<text class="form-label" x="${x}" y="${labelTextY}">${escapeXml(label)}</text>`;
controlTopY = y + labelHeight;
}
}
let controlSvg = '';
switch (node.control) {
case 'textbox': {
const rectY = label ? controlTopY : y - controlHeight / 2;
controlSvg = `<rect class="form-control" x="${controlX}" y="${rectY}" width="${width}" height="${controlHeight}" fill="#fff" />`;
if (node.placeholder) {
const placeholderY = rectY + controlHeight / 2;
controlSvg += `
<text class="placeholder-text" x="${controlX + 10}" y="${placeholderY}" dominant-baseline="middle">
${escapeXml(node.placeholder)}
</text>
`;
}
break;
}
case 'select':
if (node.multiple === true && Array.isArray(items)) {
controlSvg = `<rect class="form-control" x="${controlX}" y="${controlTopY}" width="${width}" height="${controlHeight}" fill="#fff" />`;
let listY = controlTopY + 18;
items.forEach(item => {
controlSvg += `<text class="form-label" x="${controlX + 10}" y="${listY}">${escapeXml(item)}</text>`;
listY += 16;
});
} else {
const rectY = label ? controlTopY : y - controlHeight / 2;
const controlCenterY = rectY + controlHeight / 2;
const arrowPath = `M ${controlX + width - 20} ${controlCenterY - 5} l 5 10 l 5 -10 z`;
controlSvg = `
<rect class="form-control" x="${controlX}" y="${rectY}" width="${width}" height="${controlHeight}" fill="#fff" />
<path d="${arrowPath}" fill="#333" />
`;
}
break;
case 'calendar': {
const rectY = label ? controlTopY : y - controlHeight / 2;
const placeholder = node.placeholder ?? 'YYYY-MM-DD';
const placeholderY = rectY + controlHeight / 2;
const iconWidth = 14;
const iconHeight = 14;
const iconX = controlX + width - iconWidth - 8;
const iconY = rectY + (controlHeight / 2) - (iconHeight / 2);
// Simple calendar icon
const iconPath = `
M ${iconX} ${iconY+2} h ${iconWidth} v ${iconHeight-2} h -${iconWidth} z
M ${iconX} ${iconY+5} h ${iconWidth}
M ${iconX+3} ${iconY} v 4
M ${iconX+iconWidth-3} ${iconY} v 4
`;
controlSvg = `
<rect class="form-control" x="${controlX}" y="${rectY}" width="${width}" height="${controlHeight}" fill="#fff" />
<path d="${iconPath}" stroke="#333" stroke-width="1.5" fill="none" />
<text class="placeholder-text" x="${controlX + 10}" y="${placeholderY}" dominant-baseline="middle">
${escapeXml(placeholder)}
</text>
`;
break;
}
case 'checkbox':
case 'radio':
if (Array.isArray(items)) {
const itemHeight = 25;
const itemWidth = 100;
const checkedValue = node.value;
items.forEach((item, index) => {
let itemX = controlX;
let itemCenterY = controlTopY + itemHeight / 2;
if (direction === 'column') {
itemCenterY = controlTopY + (index * itemHeight) + (itemHeight / 2);
} else { // row
itemX = controlX + (index * itemWidth);
}
const isChecked = Array.isArray(checkedValue) ? checkedValue.includes(item) : checkedValue === item;
if (node.control === 'checkbox') {
const checkWidth = 16;
const checkX = itemX;
const checkY = itemCenterY - checkWidth/2;
controlSvg += `<rect class="form-control" x="${checkX}" y="${checkY}" width="${checkWidth}" height="${checkWidth}" fill="#fff" />`;
if (isChecked) {
const checkMarkPath = `M ${checkX+4} ${checkY+8} l 3 3 l 6 -6`;
controlSvg += `<path d="${checkMarkPath}" stroke="#333" stroke-width="2" fill="none" />`;
}
} else {
const radioRadius = 8;
controlSvg += `<circle class="form-control" cx="${itemX + radioRadius}" cy="${itemCenterY}" r="${radioRadius}" fill="#fff" />`;
if (isChecked) {
controlSvg += `<circle cx="${itemX + radioRadius}" cy="${itemCenterY}" r="${radioRadius/2}" fill="#333" />`;
}
}
controlSvg += `
<text class="form-label" x="${itemX + 25}" y="${itemCenterY}" dominant-baseline="middle">
${escapeXml(item)}
</text>
`;
});
} else { // single checkbox/radio
const controlCenterY = y;
const controlX = parent.width / 2;
if (node.control === 'checkbox') {
const checkWidth = 16;
const checkX = controlX - (checkWidth / 2);
const checkY = controlCenterY - (checkWidth / 2);
controlSvg = `<rect class="form-control" x="${checkX}" y="${checkY}" width="${checkWidth}" height="${checkWidth}" fill="#fff" />`;
if (node.value === true) {
const checkMarkPath = `M ${checkX + 3} ${checkY + 8} l 4 4 l 8 -8`;
controlSvg += `<path d="${checkMarkPath}" stroke="#333" stroke-width="2" fill="none" />`;
}
} else {
const radioRadius = 10;
controlSvg = `<circle class="form-control" cx="${controlX}" cy="${controlCenterY}" r="${radioRadius}" fill="#fff" />`;
}
}
break;
}
return `
<g>
${labelSvg}
${controlSvg}
</g>
`;
}
function renderList(node, parent, y) {
const x = node.x ?? 20;
const items = node.items || [];
const direction = node.direction || 'column';
const listColor = node.listColor || '#333';
const textColor = node.color || '#333';
const itemHeight = 22; // Increased to accommodate larger font
const itemWidth = 120;
const bulletRadius = 3;
let listSvg = '';
items.forEach((item, index) => {
let itemX = x;
let itemY = y;
if (direction === 'column') {
itemY = y + (index * itemHeight);
} else { // row
itemX = x + (index * itemWidth);
}
listSvg += `
<circle cx="${itemX}" cy="${itemY}" r="${bulletRadius}" fill="${listColor}" />
<text class="list-item-text" x="${itemX + 10}" y="${itemY}" fill="${textColor}" dominant-baseline="middle">
${escapeXml(item)}
</text>
`;
});
return `<g>${listSvg}</g>`;
}
function renderImage(node, parent, y) {
const x = node.x ?? 20;
const width = node.width || 100;
const height = node.height || 100;
const color = node.color || '#ccc';
const altText = node.alt || '';
let textSvg = '';
if (altText) {
const textX = width / 2;
const textY = height / 2;
const textFontSize = 14;
// Estimate text width to create a background rect
const estimatedTextWidth = altText.length * textFontSize * 0.6;
const padding = 10;
const bgWidth = estimatedTextWidth + padding * 2;
const bgHeight = textFontSize + padding;
textSvg = `
<rect x="${textX - bgWidth / 2}" y="${textY - bgHeight / 2}" width="${bgWidth}" height="${bgHeight}" fill="#ffffff" opacity="0.8" />
<text x="${textX}" y="${textY}" text-anchor="middle" dominant-baseline="middle" font-size="${textFontSize}" fill="#333">
${escapeXml(altText)}
</text>
`;
}
return `
<g transform="translate(${x}, ${y})">
<rect x="0" y="0" width="${width}" height="${height}" fill="#f9f9f9" stroke="${color}" stroke-width="1" />
<line x1="0" y1="0" x2="${width}" y2="${height}" stroke="${color}" stroke-width="1" />
<line x1="0" y1="${height}" x2="${width}" y2="0" stroke="${color}" stroke-width="1" />
${textSvg}
</g>
`;
}
function renderTable(node, parent, y) {
const x = node.x ?? 20;
const tableWidth = node.width ?? parent.width - (x * 2);
const rows = node.rows || [];
const headers = node.headers || [];
const columnCount = headers.length || (rows[0] ? rows[0].length : 0);
if (columnCount === 0) return '';
const columnWidths = node.widths || Array(columnCount).fill(tableWidth / columnCount);
const rowHeight = 40;
let tableSvg = '';
let currentY = y;
// Draw Header
if (headers.length > 0) {
let currentX = x;
headers.forEach((header, i) => {
tableSvg += `
<rect x="${currentX}" y="${currentY}" width="${columnWidths[i]}" height="${rowHeight}" fill="#f0f0f0" stroke="#ccc" />
<text class="table-header" x="${currentX + columnWidths[i] / 2}" y="${currentY + rowHeight / 2}" text-anchor="middle" dominant-baseline="middle">
${escapeXml(header)}
</text>
`;
currentX += columnWidths[i];
});
currentY += rowHeight;
}
// Draw Rows
rows.forEach(row => {
let currentX = x;
row.forEach((cell, i) => {
const cellWidth = columnWidths[i];
const cellX = currentX;
const cellY = currentY;
let cellContentSvg = '';
// A cell is like a mini parent container for the element inside
const cellParent = { width: cellWidth, height: rowHeight };
// We need to translate the element rendering into the cell's coordinate system.
// The render functions return SVG content, so we wrap it in a transform group.
let elementSvg = '';
switch (cell.type) {
case 'button':
const btn = renderButton({ ...cell, align: cell.align || 'center' }, cellParent, rowHeight / 2);
elementSvg = btn;
break;
case 'checkbox':
const chk = renderFormControl({ control: 'checkbox', ...cell }, cellParent, rowHeight / 2);
elementSvg = chk;
break;
case 'textbox':
const txt = renderFormControl({ control: 'textbox', ...cell }, cellParent, rowHeight / 2);
elementSvg = txt;
break;
case 'text':
default: {
const textColor = cell.color || '#333';
const align = cell.align || 'left';
let textX;
let textAnchor;
const padding = 10;
switch(align) {
case 'center':
textX = columnWidths[i] / 2;
textAnchor = 'middle';
break;
case 'right':
textX = columnWidths[i] - padding;
textAnchor = 'end';
break;
case 'left':
default:
textX = padding;
textAnchor = 'start';
break;
}
elementSvg = `<text class="table-cell" x="${textX}" y="${rowHeight / 2}" dominant-baseline="middle" fill="${textColor}" text-anchor="${textAnchor}">${escapeXml(cell.value || '')}</text>`;
break;
}
}
tableSvg += `
<rect x="${cellX}" y="${cellY}" width="${cellWidth}" height="${rowHeight}" fill="#fff" stroke="#ccc" />
<g transform="translate(${cellX}, ${cellY})">
${elementSvg}
</g>
`;
currentX += cellWidth;
});
currentY += rowHeight;
});
return `<g>${tableSvg}</g>`;
}
function renderHR(node, parent, y) {
const x = node.x ?? 20;
const width = node.width ?? (parent.width - x * 2);
const color = node.color || '#cccccc';
const strokeWidth = node.strokeWidth || 1;
return `
<line x1="${x}" y1="${y}" x2="${x + width}" y2="${y}" stroke="${escapeXml(color)}" stroke-width="${strokeWidth}" />
`;
}
function renderSpacer(node, parent) {
// スペーサーは視覚的な要素を持たず、レイアウトスペースのみを提供する
return '';
}
function renderTabs(node, parent, y) {
const x = node.x ?? 20;
const width = node.width ?? (parent.width - x * 2);
const items = node.items || [];
const active = node.active ?? 0;
const tabHeight = 40;
const tabWidth = width / Math.max(items.length, 1);
let tabsSvg = '';
// タブヘッダーの描画
items.forEach((item, index) => {
const tabX = x + (index * tabWidth);
const isActive = index === active;
const bgColor = isActive ? '#ffffff' : '#f0f0f0';
const textColor = isActive ? '#333333' : '#666666';
const borderColor = '#cccccc';
tabsSvg += `
<rect x="${tabX}" y="${y}" width="${tabWidth}" height="${tabHeight}"
fill="${bgColor}" stroke="${borderColor}" stroke-width="1" />
<text x="${tabX + tabWidth / 2}" y="${y + tabHeight / 2}"
text-anchor="middle" dominant-baseline="middle"
font-family="sans-serif" font-size="14" fill="${textColor}">
${escapeXml(item)}
</text>
`;
});
// タブのボトムライン(アクティブタブの下は線を消す)
if (items.length > 0) {
const activeTabX = x + (active * tabWidth);
// アクティブタブの左側の線
if (activeTabX > x) {
tabsSvg += `
<line x1="${x}" y1="${y + tabHeight}" x2="${activeTabX}" y2="${y + tabHeight}"
stroke="#cccccc" stroke-width="1" />
`;
}
// アクティブタブの右側の線
const activeTabEndX = activeTabX + tabWidth;
if (activeTabEndX < x + width) {
tabsSvg += `
<line x1="${activeTabEndX}" y1="${y + tabHeight}" x2="${x + width}" y2="${y + tabHeight}"
stroke="#cccccc" stroke-width="1" />
`;
}
}
return `<g>${tabsSvg}</g>`;
}
function renderBadge(node, parent, y) {
const x = node.x ?? 20;
const text = node.value || '';
const bgColor = node.color || '#007bff';
const textColor = node.textColor || '#ffffff';
const borderRadius = node.borderRadius ?? 12;
const fontSize = 12;
const padding = 8;
// テキストの幅を推定(フォントサイズの0.6倍 × 文字数)
const estimatedTextWidth = text.length * fontSize * 0.6;
const width = node.width || (estimatedTextWidth + padding * 2);
const height = node.height || 20;
const centerX = x + width / 2;
const centerY = y + height / 2;
return `
<rect x="${x}" y="${y}" width="${width}" height="${height}"
fill="${escapeXml(bgColor)}" rx="${borderRadius}" ry="${borderRadius}" />
<text x="${centerX}" y="${centerY}"
text-anchor="middle" dominant-baseline="middle"
font-family="sans-serif" font-size="${fontSize}" font-weight="bold"
fill="${escapeXml(textColor)}">
${escapeXml(text)}
</text>
`;
}
function getWrappedLines(text, width, fontSize) {
// NOTE: This is a simplified text wrapping logic using an average character width.
// It may not be perfectly accurate for all fonts and characters.
const avgCharWidth = fontSize * 0.6;
const words = text.split(/\s+/);
const lines = [];
let currentLine = '';
for (const word of words) {
if (currentLine === '') {
currentLine = word;
} else {
let testLine = currentLine + ' ' + word;
if (testLine.length * avgCharWidth > width) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines;
}
function escapeXml(str) {
return String(str).replace(/[&<>"']/g, function (s) {
switch (s) {
case '&': return '&';
case '<': return '<';
case '>': return '>';
case '"': return '"';
case "'": return ''';
}
});
}
module.exports = astToSvg;