Export diagrams as interactive HTML from the app and an agent tool
This commit is contained in:
@@ -44,7 +44,50 @@ func (m *Manager) renderDiagramTool() mcp.Tool {
|
||||
}
|
||||
}
|
||||
|
||||
func renderDiagram(ctx context.Context, source string) ([]byte, error) {
|
||||
func (m *Manager) exportDiagramTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "export_diagram",
|
||||
Description: "Export a diagram as a self-contained HTML page, drawn exactly as byre's app lays it out and as interactive as its full-size view: clicking a box highlights it, its arrows and its neighbors, each highlight line is a button, and it zooms and pans. " +
|
||||
"Pass the body of a ```diagram block and the file to write; show it with show_page.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {"type": "string", "description": "The block's body, without the fence lines."},
|
||||
"path": {"type": "string", "description": "The HTML file to write, relative to your worktree, absolute, or starting with ~/."}
|
||||
},
|
||||
"required": ["source", "path"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
Source string `json:"source"`
|
||||
Path string `json:"path"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed export_diagram call: %w", err)
|
||||
}
|
||||
full, err := l.resolve(args.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
html, err := renderDiagram(ctx, args.Source, "--html")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(full), 0o755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = os.WriteFile(full, html, 0o644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Wrote the diagram to %s.", args.Path), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func renderDiagram(ctx context.Context, source string, flags ...string) ([]byte, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -52,7 +95,7 @@ func renderDiagram(ctx context.Context, source string) ([]byte, error) {
|
||||
app := filepath.Join(filepath.Dir(exe), "..", "MacOS", "Byre")
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, app, "render-diagram")
|
||||
cmd := exec.CommandContext(ctx, app, append([]string{"render-diagram"}, flags...)...)
|
||||
cmd.Stdin = strings.NewReader(source)
|
||||
stderr := bytes.Buffer{}
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
@@ -33,3 +33,5 @@ highlight "Event loop" store server coord
|
||||
Keep each box to a title and one or two short detail lines, and draw only the arrows the reader needs; a picture with every dependency in it reads worse than prose.
|
||||
|
||||
Check a diagram before you send it: `render_diagram` takes the block's body and returns a PNG of what the app draws, or the reason it can't. Look for crossing or overlapping arrows and labels sitting on top of each other, and adjust the rows, `aside` or arrows until it reads cleanly.
|
||||
|
||||
To hand a diagram over as a file, `export_diagram` writes it as one self-contained HTML page with the app's exact layout and its full-size view's interactivity (click to highlight, highlight buttons, zoom and pan, light and dark mode); show it with `show_page`. The full-size view in the app has the same export as its Export HTML button.
|
||||
|
||||
@@ -867,7 +867,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool(), m.renderDiagramTool()}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
|
||||
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool(), m.renderDiagramTool(), m.exportDiagramTool(l)}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
|
||||
lead := true
|
||||
if sess.Role == api.Implementer {
|
||||
g += longTitleGuidance(task)
|
||||
@@ -937,7 +937,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
|
||||
return nil, err
|
||||
}
|
||||
l.notes = "\n\n" + notes
|
||||
tools = append(append(m.leadTools(l), m.readKnowledgeTool(), m.renderDiagramTool()), m.clientShellTools()...)
|
||||
tools = append(append(m.leadTools(l), m.readKnowledgeTool(), m.renderDiagramTool(), m.exportDiagramTool(l)), m.clientShellTools()...)
|
||||
}
|
||||
env := git.IdentityEnv(l.gitName, l.gitEmail)
|
||||
servers := []*mcp.Server{{Name: "byre", Version: "0.0.0", Tools: tools}}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import CoreText
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
enum BoxTint: String {
|
||||
case plain, blue, green, amber, red
|
||||
@@ -270,7 +271,7 @@ struct BoxFocus {
|
||||
}
|
||||
}
|
||||
|
||||
private let pad: CGFloat = 12
|
||||
let pad: CGFloat = 12
|
||||
private let childGap: CGFloat = 14
|
||||
private let boxGap: CGFloat = 20
|
||||
private let rowGap: CGFloat = 48
|
||||
@@ -278,17 +279,17 @@ private let columnGap: CGFloat = 80
|
||||
private let margin: CGFloat = 20
|
||||
|
||||
@MainActor
|
||||
private var titleFont: CTFont {
|
||||
var titleFont: CTFont {
|
||||
Fonts.native(13, weight: 600) as CTFont
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private var detailFont: CTFont {
|
||||
var detailFont: CTFont {
|
||||
Fonts.native(12) as CTFont
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private var edgeLabelFont: CTFont {
|
||||
var edgeLabelFont: CTFont {
|
||||
Fonts.native(11) as CTFont
|
||||
}
|
||||
|
||||
@@ -788,7 +789,7 @@ func cachedBoxLayout(_ source: String) -> Result<BoxLayout, DiagramError> {
|
||||
return result
|
||||
}
|
||||
|
||||
private func fill(_ b: PlacedBox) -> Color {
|
||||
func fill(_ b: PlacedBox) -> Color {
|
||||
switch b.node.tint {
|
||||
case .blue: Palette.accentTint
|
||||
case .green: Palette.okTint
|
||||
@@ -905,6 +906,7 @@ struct DiagramViewer: View {
|
||||
let layout: BoxLayout
|
||||
@ViewState private var picked: String?
|
||||
@ViewState private var preset: Int?
|
||||
@ViewState private var exporting: DiagramHTMLFile?
|
||||
|
||||
private var focus: BoxFocus? {
|
||||
if let picked {
|
||||
@@ -918,16 +920,23 @@ struct DiagramViewer: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if !layout.presets.isEmpty {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(Array(layout.presets.enumerated()), id: \.offset) { i, p in
|
||||
Button(p.name) {
|
||||
picked = nil
|
||||
preset = preset == i ? nil : i
|
||||
}
|
||||
.buttonStyle(WebButtonStyle(primary: preset == i))
|
||||
HStack(spacing: 6) {
|
||||
ForEach(Array(layout.presets.enumerated()), id: \.offset) { i, p in
|
||||
Button(p.name) {
|
||||
picked = nil
|
||||
preset = preset == i ? nil : i
|
||||
}
|
||||
.buttonStyle(WebButtonStyle(primary: preset == i))
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
Button("Export HTML") {
|
||||
exporting = DiagramHTMLFile(html: diagramHTML(layout))
|
||||
}
|
||||
.buttonStyle(WebButtonStyle())
|
||||
.help("Save the diagram as a web page that zooms and highlights like this view")
|
||||
}
|
||||
.fileExporter(isPresented: Binding { exporting != nil } set: { if !$0 { exporting = nil } }, document: exporting, contentType: .html, defaultFilename: "diagram.html") { _ in
|
||||
exporting = nil
|
||||
}
|
||||
ZoomingCanvas(layout: layout, focus: focus) { id in
|
||||
preset = nil
|
||||
|
||||
@@ -57,7 +57,10 @@ enum Main {
|
||||
static func main() {
|
||||
let args = CommandLine.arguments
|
||||
if args.count == 2 && args[1] == "render-diagram" {
|
||||
exit(renderDiagram())
|
||||
exit(renderDiagram(html: false))
|
||||
}
|
||||
if args.count == 3 && args[1] == "render-diagram" && args[2] == "--html" {
|
||||
exit(renderDiagram(html: true))
|
||||
}
|
||||
ByreApp.main()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import CoreText
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
private func hex(_ c: Color, _ scheme: ColorScheme) -> String {
|
||||
var env = EnvironmentValues()
|
||||
env.colorScheme = scheme
|
||||
let comps = c.resolve(in: env).cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil)!.components!
|
||||
return "#" + comps.prefix(3).map { String(Int(($0 * 255).rounded()) + 0x100, radix: 16).dropFirst() }.joined()
|
||||
}
|
||||
|
||||
private func num(_ v: CGFloat) -> String {
|
||||
"\((v * 10).rounded() / 10)"
|
||||
}
|
||||
|
||||
private func esc(_ s: String) -> String {
|
||||
s.replacingOccurrences(of: "&", with: "&").replacingOccurrences(of: "<", with: "<").replacingOccurrences(of: ">", with: ">").replacingOccurrences(of: "\"", with: """)
|
||||
}
|
||||
|
||||
private func json(_ v: Any) -> String {
|
||||
String(decoding: try! JSONSerialization.data(withJSONObject: v), as: UTF8.self).replacingOccurrences(of: "</", with: "<\\/")
|
||||
}
|
||||
|
||||
private func fontFace(_ subset: String) -> String {
|
||||
let url = Bundle.main.resourceURL!.appending(path: "fonts/atkinson-hyperlegible-next-\(subset)-wght-normal.woff2")
|
||||
let data = try! Data(contentsOf: url)
|
||||
return "@font-face{font-family:Atkinson;font-weight:200 800;src:url(data:font/woff2;base64,\(data.base64EncodedString())) format(\"woff2\")}"
|
||||
}
|
||||
|
||||
private func edgePath(_ pts: [CGPoint]) -> String {
|
||||
guard let first = pts.first else {
|
||||
return ""
|
||||
}
|
||||
var d = "M\(num(first.x)) \(num(first.y))"
|
||||
if pts.count > 2 {
|
||||
for i in 1..<(pts.count - 1) {
|
||||
let a = pts[i - 1]
|
||||
let b = pts[i]
|
||||
let c = pts[i + 1]
|
||||
let lin = hypot(b.x - a.x, b.y - a.y)
|
||||
let lout = hypot(c.x - b.x, c.y - b.y)
|
||||
let r = min(6, lin / 2, lout / 2)
|
||||
let p = CGPoint(x: b.x - (b.x - a.x) / lin * r, y: b.y - (b.y - a.y) / lin * r)
|
||||
let q = CGPoint(x: b.x + (c.x - b.x) / lout * r, y: b.y + (c.y - b.y) / lout * r)
|
||||
d += "L\(num(p.x)) \(num(p.y))Q\(num(b.x)) \(num(b.y)) \(num(q.x)) \(num(q.y))"
|
||||
}
|
||||
}
|
||||
let last = pts[pts.count - 1]
|
||||
return d + "L\(num(last.x)) \(num(last.y))"
|
||||
}
|
||||
|
||||
private func arrowHead(_ from: CGPoint, _ to: CGPoint) -> String {
|
||||
let angle = atan2(to.y - from.y, to.x - from.x)
|
||||
let size: CGFloat = 8
|
||||
let a = CGPoint(x: to.x - size * cos(angle - .pi / 7), y: to.y - size * sin(angle - .pi / 7))
|
||||
let b = CGPoint(x: to.x - size * cos(angle + .pi / 7), y: to.y - size * sin(angle + .pi / 7))
|
||||
return "\(num(to.x)),\(num(to.y)) \(num(a.x)),\(num(a.y)) \(num(b.x)),\(num(b.y))"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func diagramHTML(_ layout: BoxLayout) -> String {
|
||||
let colors: [(String, Color)] = [("bg", Palette.bg), ("border", Palette.border), ("text", Palette.text), ("muted", Palette.muted), ("accent", Palette.accent)]
|
||||
let light = colors.map { "--\($0.0):\(hex($0.1, .light))" }.joined(separator: ";")
|
||||
let dark = colors.map { "--\($0.0):\(hex($0.1, .dark))" }.joined(separator: ";")
|
||||
let titleLine = textSize("X", titleFont).height
|
||||
let detailLine = textSize("X", detailFont).height
|
||||
var svg = ""
|
||||
for b in layout.boxes {
|
||||
let r = b.rect
|
||||
let f = fill(b)
|
||||
svg += "<g class=\"box\" data-id=\"\(esc(b.node.id))\" style=\"--l:\(hex(f, .light));--d:\(hex(f, .dark))\">"
|
||||
svg += "<rect x=\"\(num(r.minX))\" y=\"\(num(r.minY))\" width=\"\(num(r.width))\" height=\"\(num(r.height))\" rx=\"\(b.depth == 0 ? 8 : 6)\"/>"
|
||||
var y = r.minY + pad
|
||||
svg += "<text class=\"title\" x=\"\(num(r.minX + pad))\" y=\"\(num(y + CTFontGetAscent(titleFont)))\">\(esc(b.node.title))</text>"
|
||||
y += titleLine + 2
|
||||
for d in b.node.details {
|
||||
svg += "<text class=\"detail\" x=\"\(num(r.minX + pad))\" y=\"\(num(y + CTFontGetAscent(detailFont)))\">\(esc(d))</text>"
|
||||
y += detailLine
|
||||
}
|
||||
svg += "</g>"
|
||||
}
|
||||
for (i, e) in layout.edges.enumerated() {
|
||||
svg += "<g class=\"edge\" data-i=\"\(i)\">"
|
||||
svg += "<path d=\"\(edgePath(e.points))\"\(e.edge.dashed ? " stroke-dasharray=\"5 4\"" : "")/>"
|
||||
if e.points.count >= 2 {
|
||||
svg += "<polygon points=\"\(arrowHead(e.points[e.points.count - 2], e.points[e.points.count - 1]))\"/>"
|
||||
}
|
||||
if let label = e.edge.label, let rect = e.labelRect {
|
||||
svg += "<text x=\"\(num(rect.minX))\" y=\"\(num(rect.minY + CTFontGetAscent(edgeLabelFont)))\">\(esc(label))</text>"
|
||||
}
|
||||
svg += "</g>"
|
||||
}
|
||||
let data: [String: Any] = [
|
||||
"boxes": layout.boxes.map { ["id": $0.node.id, "parent": $0.parent.map { $0 as Any } ?? NSNull()] },
|
||||
"edges": layout.edges.map { ["from": $0.edge.from, "to": $0.edge.to] },
|
||||
"presets": layout.presets.map { ["name": $0.name, "ids": Array($0.ids)] },
|
||||
"width": layout.size.width,
|
||||
"height": layout.size.height,
|
||||
]
|
||||
let buttons = layout.presets.enumerated().map { "<button data-p=\"\($0.offset)\">\(esc($0.element.name))</button>" }.joined()
|
||||
return """
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Diagram</title>
|
||||
<style>
|
||||
\(fontFace("latin-ext"))
|
||||
\(fontFace("latin"))
|
||||
:root{\(light)}
|
||||
@media (prefers-color-scheme:dark){:root{\(dark)}.box{--l:var(--d)!important}}
|
||||
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font-family:Atkinson,-apple-system,sans-serif}
|
||||
body{display:flex;flex-direction:column}
|
||||
nav{display:flex;flex-wrap:wrap;gap:6px;padding:12px 12px 0}
|
||||
nav:empty{display:none}
|
||||
button{font:600 13px Atkinson,-apple-system,sans-serif;color:var(--text);background:transparent;border:1px solid var(--border);border-radius:6px;padding:4px 10px;cursor:pointer}
|
||||
button.on{color:var(--bg);background:var(--accent);border-color:var(--accent)}
|
||||
svg{flex:1;width:100%;min-height:0;touch-action:none;cursor:grab;user-select:none;-webkit-user-select:none}
|
||||
.box rect{fill:var(--l);stroke:var(--border);stroke-width:1}
|
||||
.box.lit rect{stroke:var(--accent);stroke-width:1.5}
|
||||
.box.dim{opacity:.3}
|
||||
.box{cursor:pointer}
|
||||
.title{font-size:\(num(CTFontGetSize(titleFont)))px;font-weight:600;fill:var(--text)}
|
||||
.detail{font-size:\(num(CTFontGetSize(detailFont)))px;fill:var(--muted)}
|
||||
.edge{pointer-events:none}
|
||||
.edge path{fill:none;stroke:var(--muted);stroke-width:1.3;stroke-linecap:round;stroke-linejoin:round}
|
||||
.edge polygon{fill:var(--muted)}
|
||||
.edge text{font-size:\(num(CTFontGetSize(edgeLabelFont)))px;fill:var(--muted)}
|
||||
.edge.lit path{stroke:var(--accent);stroke-width:2}
|
||||
.edge.lit polygon,.edge.lit text{fill:var(--accent)}
|
||||
.edge.faint{opacity:.2}
|
||||
</style></head><body>
|
||||
<nav>\(buttons)</nav>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"><g id="world">\(svg)</g></svg>
|
||||
<script>
|
||||
const D=\(json(data));
|
||||
const svg=document.querySelector("svg"),world=document.getElementById("world");
|
||||
const parent={};D.boxes.forEach(b=>parent[b.id]=b.parent);
|
||||
const anc=id=>{const o=[];for(let p=parent[id];p!=null;p=parent[p])o.push(p);return o};
|
||||
let picked=null,preset=null;
|
||||
function render(){
|
||||
let boxes=null,edges=new Set();
|
||||
if(picked!=null){boxes=new Set([picked]);D.edges.forEach((e,i)=>{if(e.from==picked||e.to==picked){edges.add(i);boxes.add(e.from);boxes.add(e.to)}})}
|
||||
else if(preset!=null){const ids=D.presets[preset].ids;boxes=new Set(ids);D.edges.forEach((e,i)=>{if(boxes.has(e.from)&&boxes.has(e.to))edges.add(i)})}
|
||||
document.querySelectorAll(".box").forEach(g=>{const id=g.dataset.id;
|
||||
g.classList.toggle("lit",!!boxes&&boxes.has(id));
|
||||
const dim=!!boxes&&!boxes.has(id)&&!anc(id).some(a=>boxes.has(a))&&![...boxes].some(b=>anc(b).includes(id));
|
||||
g.classList.toggle("dim",dim)});
|
||||
document.querySelectorAll(".edge").forEach(g=>{const i=+g.dataset.i;g.classList.toggle("lit",edges.has(i));g.classList.toggle("faint",!!boxes&&!edges.has(i))});
|
||||
document.querySelectorAll("nav button").forEach(b=>b.classList.toggle("on",+b.dataset.p===preset));
|
||||
}
|
||||
document.querySelectorAll("nav button").forEach(b=>b.onclick=()=>{const p=+b.dataset.p;picked=null;preset=preset===p?null:p;render()});
|
||||
let s=1,tx=0,ty=0;
|
||||
const apply=()=>world.setAttribute("transform",`translate(${tx} ${ty}) scale(${s})`);
|
||||
function fit(){const r=svg.getBoundingClientRect();s=Math.min(2,r.width/D.width,r.height/D.height);tx=(r.width-D.width*s)/2;ty=(r.height-D.height*s)/2;apply()}
|
||||
function zoom(f,cx,cy){const n=Math.min(6,Math.max(.25,s*f));tx=cx-(cx-tx)*n/s;ty=cy-(cy-ty)*n/s;s=n;apply()}
|
||||
const local=e=>{const r=svg.getBoundingClientRect();return[e.clientX-r.left,e.clientY-r.top]};
|
||||
svg.addEventListener("wheel",e=>{e.preventDefault();if(e.ctrlKey){const[x,y]=local(e);zoom(Math.exp(-e.deltaY*.01),x,y)}else{tx-=e.deltaX;ty-=e.deltaY;apply()}},{passive:false});
|
||||
const ptrs=new Map();let moved=0,pinch=0;
|
||||
svg.addEventListener("pointerdown",e=>{svg.setPointerCapture(e.pointerId);ptrs.set(e.pointerId,local(e));if(ptrs.size==1)moved=0;pinch=0});
|
||||
svg.addEventListener("pointermove",e=>{if(!ptrs.has(e.pointerId))return;const p=local(e),o=ptrs.get(e.pointerId);
|
||||
if(ptrs.size==1){tx+=p[0]-o[0];ty+=p[1]-o[1];moved+=Math.abs(p[0]-o[0])+Math.abs(p[1]-o[1]);apply();ptrs.set(e.pointerId,p);return}
|
||||
ptrs.set(e.pointerId,p);moved=99;const[a,b]=[...ptrs.values()];const d=Math.hypot(a[0]-b[0],a[1]-b[1]);
|
||||
if(pinch)zoom(d/pinch,(a[0]+b[0])/2,(a[1]+b[1])/2);pinch=d});
|
||||
const up=e=>{ptrs.delete(e.pointerId);pinch=0};
|
||||
svg.addEventListener("pointerup",e=>{const tap=ptrs.size==1&&moved<4;up(e);if(!tap)return;
|
||||
const g=document.elementsFromPoint(e.clientX,e.clientY).find(el=>el.closest&&el.closest(".box"));
|
||||
const id=g?g.closest(".box").dataset.id:null;preset=null;picked=id==picked?null:id;render()});
|
||||
svg.addEventListener("pointercancel",up);
|
||||
addEventListener("resize",fit);
|
||||
fit();
|
||||
</script></body></html>
|
||||
"""
|
||||
}
|
||||
|
||||
struct DiagramHTMLFile: FileDocument {
|
||||
static let readableContentTypes: [UTType] = [.html]
|
||||
let html: String
|
||||
|
||||
init(html: String) {
|
||||
self.html = html
|
||||
}
|
||||
|
||||
init(configuration: ReadConfiguration) throws {
|
||||
throw CocoaError(.fileReadUnsupportedScheme)
|
||||
}
|
||||
|
||||
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
|
||||
FileWrapper(regularFileWithContents: Data(html.utf8))
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
func renderDiagram() -> Int32 {
|
||||
func renderDiagram(html: Bool) -> Int32 {
|
||||
FontSize.stored = false
|
||||
let source = String(decoding: FileHandle.standardInput.readDataToEndOfFile(), as: UTF8.self)
|
||||
let layout: BoxLayout
|
||||
@@ -13,6 +13,10 @@ func renderDiagram() -> Int32 {
|
||||
FileHandle.standardError.write(Data(error.reason.utf8))
|
||||
return 1
|
||||
}
|
||||
if html {
|
||||
FileHandle.standardOutput.write(Data(diagramHTML(layout).utf8))
|
||||
return 0
|
||||
}
|
||||
let content = BoxCanvas(layout: layout)
|
||||
.frame(width: layout.size.width, height: layout.size.height)
|
||||
.padding(16)
|
||||
|
||||
Reference in New Issue
Block a user