Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
fb3a215
Bump version to 0.3.5
albert-gee Sep 5, 2026
53640b3
Refresh README feature screenshots
albert-gee Sep 5, 2026
db6a0c9
Fix Writerside reuse and source resolution
albert-gee Sep 5, 2026
a1c85f8
Render Writerside semantic components
albert-gee Sep 5, 2026
4df82db
Complete Writerside editing and conformance
albert-gee Sep 5, 2026
8bbb894
Add native offline HTML export
albert-gee Sep 5, 2026
ae4469d
Defer monitor refresh during foreground loading
albert-gee Sep 5, 2026
620c2f9
Customize PDF and HTML exports
albert-gee Sep 5, 2026
710edc2
Update Flutter SDK constraints in pubspec files
albert-gee Sep 6, 2026
ba949dc
Fix floating toolbar color blending for disabled state
albert-gee Sep 7, 2026
7d96516
Add rich clipboard support for Linux and enhance WYSIWYG clipboard ha…
albert-gee Sep 7, 2026
fa724c0
Fixed the Editor right-click bug for document-wide selections
albert-gee Sep 7, 2026
370ae2e
Fixed a bug. Editor session restoration counted the top toolbar paddi…
albert-gee Sep 7, 2026
de18b6d
Add Copy as Markdown to the Editor
albert-gee Sep 8, 2026
4d74e16
Preserve formatting when pasting Markdown into Editor
albert-gee Sep 8, 2026
176e438
Defer Editor session updates during rebuilds
albert-gee Sep 8, 2026
42b1f95
Use rich clipboard for Editor Cut
albert-gee Sep 8, 2026
dcb46cb
Make standard Editor Copy plain text
albert-gee Sep 8, 2026
ff37af3
Restore standard rich Editor copy
albert-gee Sep 8, 2026
eaebdd2
Refactor copy/paste - distinguish between copying with formatting and…
albert-gee Sep 8, 2026
b88aa47
Refactor TOC view of left sidebar. Add `iconColor` support for native…
albert-gee Sep 8, 2026
e38e8d7
Add tests for TOC view in left sidebar.
albert-gee Sep 8, 2026
14e9b6d
Add support for excluding PDF outline in Markdown export
albert-gee Sep 8, 2026
90e194c
Make the instance dropdown and adjacent TOC Actions button 34 px high…
albert-gee Sep 8, 2026
b99e891
Fix translations - CLDR’s Russian/Ukrainian one category included val…
albert-gee Sep 8, 2026
3268fff
Extract `_SidebarRowSurface` as a reusable widget and update `_Sideba…
albert-gee Sep 8, 2026
31e664b
Fix translations (PT)
albert-gee Sep 8, 2026
3eafd84
Refactor table menu buttons in Editor view
albert-gee Sep 8, 2026
e18dee1
Implement traversal through a table in Editor view
albert-gee Sep 8, 2026
9b21113
Implemented table-size picker
albert-gee Sep 8, 2026
8b64e05
Pinned the current stable Flutter 3.47.2
albert-gee Sep 8, 2026
cd89790
Refactor readme, add new screenshots, remove demo.
albert-gee Sep 8, 2026
88b360e
Unify PDF and HTML export
albert-gee Sep 9, 2026
a324df2
Refactor tests and localization: update messages, improve fixture pat…
albert-gee Sep 9, 2026
bb250b8
Fix PDF export image bug
albert-gee Sep 9, 2026
a0c2e3f
fix export - missing image in html, missing quote in pdf
albert-gee Sep 9, 2026
c51432a
Refactor fixture paths in visualization script and documentation; upd…
albert-gee Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/flutter-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
bubblewrap \
clang \
cmake \
fonts-noto-core \
fonts-noto-mono \
ninja-build \
pkg-config \
libgtk-3-dev \
Expand Down Expand Up @@ -85,6 +87,9 @@ jobs:
- name: Generate localizations
run: flutter gen-l10n

- name: Audit localization catalogs
run: flutter test test/src/localization_audit_test.dart

- name: Verify generated files are committed
run: git diff --exit-code

Expand All @@ -109,7 +114,9 @@ jobs:
env WEBKIT_DISABLE_COMPOSITING_MODE=1 LIBGL_ALWAYS_SOFTWARE=1 \
/usr/bin/python3 -u tools/visualization_smoke.py \
--assets build/linux/x64/release/bundle/share/busymark/visualization \
--d2 build/linux/x64/release/bundle/libexec/busymark/d2
--d2 build/linux/x64/release/bundle/libexec/busymark/d2 \
--demo test/fixtures/markdown/basic.md \
--plantuml-corpus test/fixtures/visualization/plantuml-conformance.md

- name: Exercise the release binary visualization and PDF paths under X11
run: |
Expand Down Expand Up @@ -148,7 +155,9 @@ jobs:
LIBGL_ALWAYS_SOFTWARE=1 \
/usr/bin/python3 -u tools/visualization_smoke.py \
--assets build/linux/x64/release/bundle/share/busymark/visualization \
--d2 build/linux/x64/release/bundle/libexec/busymark/d2
--d2 build/linux/x64/release/bundle/libexec/busymark/d2 \
--demo test/fixtures/markdown/basic.md \
--plantuml-corpus test/fixtures/visualization/plantuml-conformance.md
report="${RUNNER_TEMP}/visualization-release-wayland.json"
XDG_RUNTIME_DIR="$runtime_dir" WAYLAND_DISPLAY=wayland-99 \
GDK_BACKEND=wayland BUSYMARK_RELEASE_SMOKE=1 \
Expand Down
365 changes: 106 additions & 259 deletions README.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions assets/export/html.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

196 changes: 131 additions & 65 deletions assets/export/markdown.typ
Original file line number Diff line number Diff line change
@@ -1,39 +1,55 @@
// BusyMark Markdown PDF renderer. All document data enters through JSON;
// Markdown text is never evaluated as Typst source.
#let data = json("document.json")
#let metadata = data.metadata
#let document-metadata = data.metadata
#let options = data.options

#set document(
title: metadata.title,
author: if metadata.author == "" { () } else { (metadata.author,) },
description: metadata.description,
keywords: metadata.keywords,
title: document-metadata.title,
author: if document-metadata.author == "" { () } else { (document-metadata.author,) },
description: document-metadata.description,
keywords: document-metadata.keywords,
)
#assert(data.schemaVersion == 2, message: "Unsupported export payload")
#let typography = options.typography
#let geometry = options.page
#let margins = geometry.marginsPt
#let content-height = (geometry.heightPt - margins.top - margins.bottom) * 1pt
#let running-visible() = options.showHeaderFooterOnFirstPage or counter(page).get().first() > 1
#let number-position = if options.pageNumbers == "bottomLeft" { left } else if options.pageNumbers == "bottomRight" { right } else { center }
#set page(
paper: options.paper,
flipped: options.landscape,
margin: (
x: options.marginHorizontalPt * 1pt,
y: options.marginVerticalPt * 1pt,
),
numbering: if options.pageNumbers { "1" } else { none },
number-align: center + bottom,
width: geometry.widthPt * 1pt,
height: geometry.heightPt * 1pt,
margin: (top: margins.top * 1pt, right: margins.right * 1pt, bottom: margins.bottom * 1pt, left: margins.left * 1pt),
// Logical page numbering also supplies native outline page references.
// First-page visibility belongs to the running footer, never the numbering
// function: an outline on that page must still show its destination numbers.
numbering: "1",
number-align: number-position + bottom,
header: if options.header == "none" { none } else { context { if running-visible() { text(document-metadata.title) } } },
footer: if options.footer == "none" and options.pageNumbers == "off" { none } else { context {
if running-visible() {
// Native page footer content combines title and counter without overlap.
if options.footer == "documentTitle" { align(center, text(document-metadata.title)) }
if options.pageNumbers != "off" { align(number-position, counter(page).display("1")) }
}
} },
)
#set text(
font: typography.bodyFont,
fallback: true,
size: 10.5pt,
lang: metadata.language,
size: typography.bodySizePt * 1pt,
lang: document-metadata.language.split("-").first(),
region: if document-metadata.language.split("-").len() == 2 and document-metadata.language.split("-").last().len() == 2 { document-metadata.language.split("-").last() } else { none },
)
#set par(leading: 0.68em)
#set heading(numbering: none)
#show heading.where(level: 1): set text(size: 22pt, weight: "bold")
#show heading.where(level: 2): set text(size: 17pt, weight: "bold")
#show heading.where(level: 3): set text(size: 13.5pt, weight: "bold")
#show heading.where(level: 4): set text(size: 11.5pt, weight: "bold")
#show heading.where(level: 5): set text(size: 10.5pt, weight: "bold")
#show heading.where(level: 6): set text(size: 10.5pt, weight: "bold")
#show link: set text(fill: rgb("2563a5"))
#set heading(numbering: if options.content.numberHeadings { "1.1" } else { none })
#show heading: it => {
set text(size: typography.headingSizesPt.at(calc.min(it.level, 6) - 1) * 1pt, weight: "bold")
it
}
#show raw: set text(font: typography.codeFont, size: typography.codeSizePt * 1pt)
#show link: set text(fill: rgb(options.accentColor))

#let value-or(item, key, default) = item.at(key, default: default)

Expand Down Expand Up @@ -102,6 +118,8 @@
emph(body)
} else if kind == "underline" {
underline(body)
let summary = value-or(item, "summary", "")
if summary != "" { footnote(text(summary)) }
} else if kind == "strikethrough" {
strike(body)
} else if kind == "code" {
Expand Down Expand Up @@ -135,34 +153,38 @@
}
}

#let render-fitted-image(asset, alt: "", maximum-height: 72% * content-height) = layout(size => {
let natural-size = measure(image(asset))
let scaled-height = natural-size.height * (size.width / natural-size.width)
if scaled-height > maximum-height {
image(asset, height: maximum-height, fit: "contain", alt: alt)
} else {
image(asset, width: 100%, fit: "contain", alt: alt)
}
})

#let render-display-image(item) = {
let asset = value-or(item, "asset", "")
let alt = value-or(item, "alt", "")
if asset == "" {
emph(text(if alt == "" { "[Image unavailable]" } else { "[Image: " + alt + "]" }))
} else {
layout(size => image(
asset,
width: 100%,
height: 72% * size.height,
fit: "contain",
alt: alt,
))
render-fitted-image(asset, alt: alt)
}
}

#let render-table(block-data) = {
#let render-table(block-data, render) = {
let rows = value-or(block-data, "children", ())
if rows.len() == 0 {
none
} else {
let column-count = calc.max(..rows.map(row => value-or(row, "children", ()).len()))
let column-count = value-or(block-data, "columnCount", calc.max(..rows.map(row => value-or(row, "children", ()).len())))
let cells = ()
for row in rows {
let is-header = value-or(row, "header", false)
let row-cells = ()
for cell in value-or(row, "children", ()) {
let cell-body = render-inlines(value-or(cell, "inlines", ()))
let cell-body = [#render-inlines(value-or(cell, "inlines", ()))#for child in value-or(cell, "children", ()) { render(child) }]
let alignment = value-or(cell, "align", "left")
let cell-align = if alignment == "center" {
center
Expand All @@ -173,23 +195,26 @@
}
row-cells.push(table.cell(
align: cell-align,
if is-header { strong(cell-body) } else { cell-body },
x: value-or(cell, "x", auto),
y: value-or(cell, "y", auto),
colspan: value-or(cell, "colspan", 1),
rowspan: value-or(cell, "rowspan", 1),
fill: if value-or(cell, "header", is-header) { rgb("f1f3f5") } else { none },
if value-or(cell, "header", is-header) { strong(cell-body) } else { cell-body },
))
}
for ignored in range(value-or(row, "children", ()).len(), column-count) {
row-cells.push(none)
}
if is-header {
cells.push(table.header(..row-cells))
} else {
for cell in row-cells { cells.push(cell) }
}
}
table(
columns: column-count,
columns: if value-or(block-data, "columnWidths", ()).len() == column-count {
value-or(block-data, "columnWidths", ()).map(width => if width != "" { float(width) * 0.75pt } else if value-or(block-data, "fixedColumns", false) { 1fr } else { auto })
} else { column-count },
inset: 5pt,
stroke: 0.5pt + rgb("c8cdd4"),
fill: (x, y) => if y == 0 { rgb("f1f3f5") } else { none },
..cells,
)
}
Expand Down Expand Up @@ -225,14 +250,36 @@
}
}

#let render-callout(body, fill: none, accent: black, title: "") = {
let content = {
if title != "" {
strong(title)
linebreak()
}
body
}
block(
width: 100%,
above: 0.7em,
below: 0.7em,
fill: fill,
stroke: (left: 2pt + accent),
inset: 9pt,
radius: 4pt,
content,
)
}

#let render-block(block-data) = {
let kind = block-data.kind
let inlines = value-or(block-data, "inlines", ())
let children = value-or(block-data, "children", ())
let block-anchor = value-or(block-data, "anchor", "")
if block-anchor != "" { [#metadata(block-anchor) #label(block-anchor)] }
if kind == "heading" {
let element = heading(
level: value-or(block-data, "level", 1),
outlined: true,
outlined: value-or(block-data, "outlined", true),
render-inlines(inlines),
)
let anchor = value-or(block-data, "id", "")
Expand All @@ -259,31 +306,47 @@
} else if kind == "admonition" {
let style = value-or(block-data, "style", "note")
let fill = if style == "warning" {
rgb("fff4d6")
rgb("fff6e9")
} else if style == "tip" {
rgb("e8f7ed")
rgb("f0faf4")
} else {
rgb("eaf2fb")
rgb("f1f7fd")
}
block(
width: 100%,
let accent = if style == "warning" {
rgb("a34200")
} else if style == "tip" {
rgb("247550")
} else {
rgb("2366aa")
}
let body = if children.len() > 0 {
for child in children { render-block(child) }
} else {
render-inlines(inlines)
}
let default-title = if style == "warning" {
"Warning"
} else if style == "tip" {
"Tip"
} else {
"Note"
}
render-callout(
body,
fill: fill,
inset: 9pt,
radius: 4pt,
if children.len() > 0 {
for child in children { render-block(child) }
} else {
render-inlines(inlines)
},
accent: accent,
title: value-or(block-data, "title", default-title),
)
} else if kind == "blockquote" {
quote(
block: true,
if children.len() > 0 {
for child in children { render-block(child) }
} else {
render-inlines(inlines)
},
let body = if children.len() > 0 {
for child in children { render-block(child) }
} else {
render-inlines(inlines)
}
render-callout(
body,
fill: rgb("f5f7fa"),
accent: rgb("4b5563"),
)
} else if kind == "thematicBreak" {
block(above: 0.8em, below: 0.8em, line(length: 100%, stroke: 0.6pt + rgb("aeb4bc")))
Expand Down Expand Up @@ -328,7 +391,7 @@
if source.starts-with("https://") { link(source, body) } else { body },
)
} else if kind == "table" {
render-table(block-data)
render-table(block-data, render-block)
} else if kind == "visualization" {
let asset = value-or(block-data, "asset", "")
let alt = value-or(block-data, "alt", "Diagram")
Expand All @@ -339,13 +402,11 @@
above: 0.8em,
below: 0.8em,
breakable: false,
align(center, layout(size => image(
align(center, render-fitted-image(
asset,
width: 100%,
height: 70% * size.height,
fit: "contain",
alt: alt,
))),
maximum-height: 70% * content-height,
)),
)
}
} else if kind == "openApiReference" {
Expand All @@ -369,4 +430,9 @@
}
}

#if options.content.includeToc {
outline(depth: options.content.tocDepth)
v(1em)
}

#for block-data in data.blocks { render-block(block-data) }
Loading
Loading