sakusei 0.3.0 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab634a886e4a04c4a309f672ff57317e54eab5d44e8fcac304a42c9463c5bb15
4
- data.tar.gz: 4c2e2a610935627d01c80dd0f0b7ad48f6a840dddf2b433a122f4655f3a27826
3
+ metadata.gz: 1845947d6ebe2f9b6c5732093eff5bbd90aa7bc2a60ad79a568033bd8c674e57
4
+ data.tar.gz: 595a6804e797c8b0c58804b7df122b5a738e5592b786f4a0c29454801c864ed9
5
5
  SHA512:
6
- metadata.gz: caf4eb2b35c3de565416c7001f5c5fb4e79837f3ddc5cb5817154b2ea4eb6e6125ad818b71520337b6381b90b9d0fd2ecbae392fe8b1d7645e41038df9d457fe
7
- data.tar.gz: 579ad95d4b5a86a3095f7c6138a57bc577863cfecef7244520404bfddcd84281b9d405fbbc37053dcaf7dea6b7f8d3b81461325e186b7ba806fe547b684f96ff
6
+ metadata.gz: 0c2d9f30c83d0f1de7c73f8c23dcfe590208c057e95cfae0a261a64a36f7a386f3e0e34d9ba1849b46407ce4e19bd86e6dc3544db861fa8ab2f92cf0ac868d6d
7
+ data.tar.gz: 29428e2b7e9dbbe86753c58a537b6596f55a7ff7c075efe9bf40c1b0a2ada7d6f9921273400cc8226cf5acb12613a8caffed8ce9efc55576fe9a7d9b7790ca71
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "defaultMode": "auto",
4
+ "allow": [
5
+ "Bash(git *)"
6
+ ]
7
+ }
8
+ }
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  .DS_Store
3
3
  *.pdf
4
+ !examples/*.pdf
4
5
  node_modules/
5
6
  .worktrees/
data/CLAUDE.md ADDED
@@ -0,0 +1,86 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ Run all tests:
8
+
9
+ ```bash
10
+ bundle exec rake test
11
+ ```
12
+
13
+ Run a single test file:
14
+
15
+ ```bash
16
+ ruby -Ilib -Itest test/sakusei/test_builder_break_syntax.rb
17
+ ```
18
+
19
+ Build and install the gem locally:
20
+
21
+ ```bash
22
+ rake install
23
+ ```
24
+
25
+ Build a PDF from one of the examples:
26
+
27
+ ```bash
28
+ bundle exec sakusei build examples/getting-started.md
29
+ ```
30
+
31
+ Live-preview a markdown file in the browser:
32
+
33
+ ```bash
34
+ bundle exec sakusei-preview examples/getting-started.md
35
+ ```
36
+
37
+ ## Architecture
38
+
39
+ Sakusei ships two CLIs:
40
+
41
+ - `bin/sakusei` (Thor) — the build CLI that produces PDFs.
42
+ - `bin/sakusei-preview` — a separate live-render binary backed by `Sakusei::PreviewServer`. Watches the source file, its `@include` partials, the active style pack, and referenced images; re-renders to HTML on change via `npx md-to-pdf --as-html` and reloads the browser tab. Loads paged.js from a CDN so `@page`/`page-break-*` rules are visible as document pages. Use this for fast iteration; use `sakusei build` for the final PDF.
43
+
44
+ Both CLIs share the multi-stage build pipeline.
45
+
46
+ ### Build pipeline
47
+
48
+ `Builder#build` in `lib/sakusei/builder.rb` runs these steps in order:
49
+
50
+ 1. **StylePack.discover** — walks up the directory tree from the source file, looking for `.sakusei/style_packs/<name>/` at each level. Falls back to the built-in pack at `lib/templates/default_style_pack/`.
51
+ 2. **FileResolver** — resolves `<!-- @include ./file.md -->` directives and concatenates the content.
52
+ 3. **ErbProcessor** — evaluates ERB in the markdown. Available helpers: `today(format)`, `env(name, default)`, `sh(command)`, `include_file(path)`, `image_path(relative_path)`, `document_headings(path)`.
53
+ 4. **expand_break_syntax** — expands `::break::` shorthand to `<div class="page-break"></div>`.
54
+ 5. **VueProcessor** — finds `<vue-component name="Foo" prop="value" />` tags, renders them server-side via `lib/sakusei/vue_renderer.js` using Node.js + `@vue/server-renderer` in a single batched call.
55
+ 6. **HeadingWrapper** — wraps h2/h3 headings with their immediately following content block in keep-together divs to prevent orphaned headings.
56
+ 7. **MdToPdfConverter** / **HtmlConverter** — both extend `ConverterBase` (`lib/sakusei/converter_base.rb`) and assemble the `npx md-to-pdf` command with config, stylesheets, and header/footer from the style pack. `MdToPdfConverter` runs it in a temp dir and returns the PDF path; `HtmlConverter` adds `--as-html` and returns the HTML string (used by the live preview server).
57
+
58
+ `Builder#build_html` runs stages 1–6 and ends in `HtmlConverter`, sharing the pipeline with `Builder#build` so the preview matches the final PDF.
59
+
60
+ `MultiFileBuilder` handles glob patterns and multiple source files, delegating to `Builder` per file and concatenating results.
61
+
62
+ ### Style packs
63
+
64
+ A style pack is a directory containing:
65
+
66
+ - `config.js` — md-to-pdf configuration (Puppeteer/Chrome options)
67
+ - `style.css` — stylesheet applied after `lib/templates/base.css`
68
+ - `header.html`, `footer.html` — Puppeteer page chrome injected before the markdown content
69
+ - `components/*.vue` — Vue 3 SFCs, rendered server-side at build time (optional)
70
+ - `package.json` — if present, npm dependencies are auto-installed on first use
71
+
72
+ `lib/templates/base.css` is always applied first and provides keep-together rules for tables, code blocks, blockquotes, images, and common custom classes.
73
+
74
+ Style pack discovery walks up the directory tree from the source file; a named pack can live in any ancestor's `.sakusei/` directory. The default pack in `lib/templates/default_style_pack/` is the final fallback.
75
+
76
+ ### Vue component system
77
+
78
+ Components are referenced in markdown as `<vue-component name="MyComponent" prop="value" />`. `VueProcessor` finds all such tags in a first pass, replaces them with numbered placeholders, then sends a single JSON batch to `vue_renderer.js` via stdin. The JS renderer uses `@vue/server-renderer` to render each component and returns HTML + scoped CSS back as JSON. Scoped CSS is injected as a `<style>` block at the top of the document.
79
+
80
+ Component resolution order: local `./components/<Name>.vue` → style pack `components/<Name>.vue`.
81
+
82
+ Vue components support named slots via `<template #slotname>...</template>` inside the tag. Slot content is converted from Markdown to HTML before being passed to the renderer.
83
+
84
+ ### Tests
85
+
86
+ Tests use Minitest. Fixtures live in `test/fixtures/` (markdown samples and stub style packs). `test/test_vue_renderer.js` is a standalone Node.js test for the JS renderer. The base test class `Sakusei::TestCase` provides `fixtures_dir` and a `temp_dir` that auto-cleans on teardown.
data/Gemfile.lock ADDED
@@ -0,0 +1,82 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sakusei (0.5.8)
5
+ erb (~> 4.0)
6
+ listen (~> 3.8)
7
+ thor (~> 1.2)
8
+ webrick (~> 1.8)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ addressable (2.8.9)
14
+ public_suffix (>= 2.0.2, < 8.0)
15
+ ast (2.4.3)
16
+ bigdecimal (4.0.1)
17
+ cgi (0.5.1)
18
+ erb (4.0.3)
19
+ cgi (>= 0.3.3)
20
+ ffi (1.17.4)
21
+ ffi (1.17.4-arm64-darwin)
22
+ json (2.19.2)
23
+ json-schema (6.2.0)
24
+ addressable (~> 2.8)
25
+ bigdecimal (>= 3.1, < 5)
26
+ language_server-protocol (3.17.0.5)
27
+ lint_roller (1.1.0)
28
+ listen (3.10.0)
29
+ logger
30
+ rb-fsevent (~> 0.10, >= 0.10.3)
31
+ rb-inotify (~> 0.9, >= 0.9.10)
32
+ logger (1.7.0)
33
+ mcp (0.9.0)
34
+ json-schema (>= 4.1)
35
+ minitest (5.27.0)
36
+ parallel (1.27.0)
37
+ parser (3.3.10.2)
38
+ ast (~> 2.4.1)
39
+ racc
40
+ prism (1.9.0)
41
+ public_suffix (7.0.5)
42
+ racc (1.8.1)
43
+ rainbow (3.1.1)
44
+ rake (13.3.1)
45
+ rb-fsevent (0.11.2)
46
+ rb-inotify (0.11.1)
47
+ ffi (~> 1.0)
48
+ regexp_parser (2.11.3)
49
+ rubocop (1.85.1)
50
+ json (~> 2.3)
51
+ language_server-protocol (~> 3.17.0.2)
52
+ lint_roller (~> 1.1.0)
53
+ mcp (~> 0.6)
54
+ parallel (~> 1.10)
55
+ parser (>= 3.3.0.2)
56
+ rainbow (>= 2.2.2, < 4.0)
57
+ regexp_parser (>= 2.9.3, < 3.0)
58
+ rubocop-ast (>= 1.49.0, < 2.0)
59
+ ruby-progressbar (~> 1.7)
60
+ unicode-display_width (>= 2.4.0, < 4.0)
61
+ rubocop-ast (1.49.1)
62
+ parser (>= 3.3.7.2)
63
+ prism (~> 1.7)
64
+ ruby-progressbar (1.13.0)
65
+ thor (1.5.0)
66
+ unicode-display_width (3.2.0)
67
+ unicode-emoji (~> 4.1)
68
+ unicode-emoji (4.2.0)
69
+ webrick (1.9.2)
70
+
71
+ PLATFORMS
72
+ arm64-darwin-24
73
+ ruby
74
+
75
+ DEPENDENCIES
76
+ minitest (~> 5.0)
77
+ rake (~> 13.0)
78
+ rubocop (~> 1.0)
79
+ sakusei!
80
+
81
+ BUNDLED WITH
82
+ 2.7.2
data/README.md CHANGED
@@ -1,14 +1,5 @@
1
1
  # Sakusei
2
2
 
3
- **Sakusei** (作成) — from the Japanese words meaning "creation," "making," or "craft."
4
-
5
- Like a master artisan refining their craft, Sakusei transforms raw Markdown into beautifully crafted PDF documents. Every document is an act of creation — structured, styled, and brought to life with precision.
6
-
7
- The name embodies the philosophy behind this tool: documents aren't just generated, they're _crafted_.
8
-
9
-
10
- ## Overview
11
-
12
3
  Sakusei is a build system for creating PDF documents from Markdown source files. It supports:
13
4
 
14
5
  - **Markdown to PDF conversion** via `md-to-pdf`
@@ -17,16 +8,16 @@ Sakusei is a build system for creating PDF documents from Markdown source files.
17
8
  - **File inclusion** for multi-file documents
18
9
  - **PDF concatenation** for combining multiple documents
19
10
 
11
+ ---
20
12
 
21
- ## Installation
13
+ **Sakusei** (作成) — from the Japanese words meaning "creation," "making," or "craft."
22
14
 
23
- ### macOS (Homebrew)
15
+ Like a master artisan refining their craft, Sakusei transforms raw Markdown into beautifully crafted PDF documents. Every document is an act of creation — structured, styled, and brought to life with precision.
16
+
17
+ The name embodies the philosophy behind this tool: documents aren't just generated, they're _crafted_.
24
18
 
25
- ```bash
26
- # Add the tap and install
27
- brew tap keithrowell/sakusei https://github.com/keithrowell/sakusei/homebrew-tap
28
- brew install sakusei
29
- ```
19
+
20
+ ## Installation
30
21
 
31
22
  ### Ruby Gem
32
23
 
@@ -86,6 +77,142 @@ sakusei init my_company
86
77
  sakusei concat part1.pdf part2.pdf -o combined.pdf
87
78
  ```
88
79
 
80
+ ## Examples
81
+
82
+ ### Basic usage
83
+
84
+ Plain Markdown — headings, lists, a table, a blockquote — built with the default style pack:
85
+
86
+ ```markdown
87
+ # Project Kickoff Notes
88
+
89
+ **Date:** 14 April 2026
90
+ **Attendees:** Sarah Chen, Marcus Webb, Priya Nair
91
+
92
+ ---
93
+
94
+ ## Goals for This Sprint
95
+
96
+ The team aligned on three priorities for the next two weeks:
97
+
98
+ 1. Finalise the data model and get sign-off from the product team
99
+ 2. Stand up the staging environment so QA can begin testing early
100
+ 3. Resolve the outstanding API authentication issues before the client demo
101
+
102
+ ## Key Decisions
103
+
104
+ - **Database:** Sticking with PostgreSQL — the migration cost to Mongo isn't justified at this scale
105
+ - **Auth:** Moving to OAuth 2.0 with JWT refresh tokens; existing session-based auth deprecated end of month
106
+ - **Deployment:** Staging mirrors production (same instance type, same region) to avoid environment surprises
107
+
108
+ ## Action Items
109
+
110
+ | Owner | Task | Due |
111
+ |--------|-------------------------------------|--------|
112
+ | Marcus | Finalise schema and create migration | 17 Apr |
113
+ | Priya | Provision staging environment | 16 Apr |
114
+ | Sarah | Draft OAuth integration spec | 18 Apr |
115
+ | All | Review and comment on API auth RFC | 15 Apr |
116
+
117
+ > The client demo is locked in for the 25th — that date is immovable.
118
+ > Flag anything at risk by end of day Wednesday.
119
+ ```
120
+
121
+ Built with `sakusei build getting-started.md`:
122
+
123
+ [![Project Kickoff Notes PDF](examples/getting-started-screenshot.png)](examples/getting-started.pdf)
124
+
125
+ The full source is at [`examples/getting-started.md`](examples/getting-started.md).
126
+
127
+
128
+ ### Style packs and Vue components
129
+
130
+ The source for a one-page consulting proposal, using a custom style pack with `DocMeta`, `ProcessSteps`, and `DeliverableCards` components:
131
+
132
+ ```markdown
133
+ # Meridian Property Group — Operations Automation Assessment
134
+
135
+ <vue-component name="DocMeta"
136
+ client="Meridian Property Group"
137
+ preparedBy="KD Consulting"
138
+ date="<%= today('%d %B %Y') %>"
139
+ status="Proposal"
140
+ />
141
+
142
+
143
+ ## Opportunity
144
+
145
+ Meridian Property Group manages a portfolio of 340 residential and commercial tenancies
146
+ across Southeast Queensland. The business runs on strong fundamentals, but operational
147
+ bandwidth is a growing constraint: leasing coordinators spend the majority of their week
148
+ on routine correspondence, manual data entry, and chasing approvals through email chains.
149
+ This is work that AI and automation can largely eliminate.
150
+
151
+ KD Consulting proposes a focused **Operations Automation Assessment** — a two-week
152
+ engagement to map current workflows, identify the highest-impact automation opportunities,
153
+ and deliver a clear implementation roadmap.
154
+
155
+
156
+ ## How We Work
157
+
158
+ Our methodology is structured around rapid discovery and focused analysis. We do not run
159
+ open-ended workshops or produce lengthy process documentation for its own sake. Every
160
+ step is oriented toward the output: a prioritised set of recommendations the business
161
+ can act on.
162
+
163
+ <vue-component name="ProcessSteps" steps='[
164
+ {
165
+ "title": "Discovery",
166
+ "description": "Structured interviews with leasing coordinators, property managers,
167
+ and leadership. We map the full lifecycle of a tenancy — from enquiry through to
168
+ renewal or exit — and identify where time goes and where friction accumulates."
169
+ },
170
+ {
171
+ "title": "Analysis & Opportunity Mapping",
172
+ "description": "We analyse workflows against current AI and automation capabilities,
173
+ assessing each opportunity for feasibility, integration requirements, and business
174
+ impact."
175
+ },
176
+ {
177
+ "title": "Roadmap & Report",
178
+ "description": "Prioritised recommendations delivered as a practical implementation
179
+ roadmap — phased by effort and impact, with clear next steps for each initiative and
180
+ indicative investment requirements."
181
+ }
182
+ ]' />
183
+
184
+
185
+ ## Deliverables
186
+
187
+ <vue-component name="DeliverableCards" columns="3" items='[
188
+ {
189
+ "title": "Workflow Assessment",
190
+ "description": "A clear map of current operations across leasing, tenancy management,
191
+ and communications — documenting where time is spent and where the significant
192
+ friction points lie."
193
+ },
194
+ {
195
+ "title": "Automation Opportunity Register",
196
+ "description": "A structured register of identified automation opportunities, each
197
+ assessed for business impact, implementation effort, and integration requirements
198
+ with existing systems."
199
+ },
200
+ {
201
+ "title": "Implementation Roadmap",
202
+ "description": "A phased, prioritised action plan with recommended sequencing,
203
+ indicative timelines, and investment estimates — ready to brief a delivery team or
204
+ internal stakeholders."
205
+ }
206
+ ]' />
207
+ ```
208
+
209
+ Built with `sakusei build meridian-proposal.md -s kd-consulting`:
210
+
211
+ [![Meridian Property Group proposal PDF](examples/meridian-proposal-screenshot.png)](examples/meridian-proposal.pdf)
212
+
213
+ The full source is at [`examples/meridian-proposal.md`](examples/meridian-proposal.md).
214
+
215
+
89
216
  ## Style Packs
90
217
 
91
218
  Style packs are stored in `.sakusei/style_packs/` directories. Sakusei searches for style packs by walking up the directory tree from your source file.
@@ -126,20 +253,22 @@ Environment: <%= env('RAILS_ENV', 'development') %>
126
253
 
127
254
  ### Manual Page Breaks
128
255
 
129
- Insert page breaks in your markdown using HTML:
256
+ Insert a page break with the `::break::` shorthand:
130
257
 
131
258
  ```markdown
132
259
  # Chapter 1
133
260
 
134
261
  Content here...
135
262
 
136
- <div class="page-break"></div>
263
+ ::break::
137
264
 
138
265
  # Chapter 2
139
266
 
140
267
  More content...
141
268
  ```
142
269
 
270
+ This expands to `<div class="page-break"></div>` before rendering. You can also use that HTML directly if you prefer.
271
+
143
272
  Available classes:
144
273
  - `.page-break` or `.page-break-after` - Break after this element
145
274
  - `.page-break-before` - Break before this element
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'sakusei'
6
+ require 'sakusei/preview_server'
7
+
8
+ options = { open: true, paged: true }
9
+ parser = OptionParser.new do |opts|
10
+ opts.banner = 'Usage: sakusei-preview FILE [options]'
11
+
12
+ opts.on('-p', '--port PORT', Integer, "Port to bind (default #{Sakusei::PreviewServer::DEFAULT_PORT})") { |v| options[:port] = v }
13
+ opts.on('-s', '--style PACK', 'Style pack name to use') { |v| options[:style] = v }
14
+ opts.on('-c', '--config FILE', 'Path to md-to-pdf config file') { |v| options[:config] = v }
15
+ opts.on('--stylesheet FILE', 'Path to CSS stylesheet') { |v| options[:stylesheet] = v }
16
+ opts.on('--no-open', 'Do not open the browser automatically') { options[:open] = false }
17
+ opts.on('--no-paged', 'Disable paged.js pagination (show as scrolling HTML)') { options[:paged] = false }
18
+ opts.on('-h', '--help', 'Show this help') { puts opts; exit 0 }
19
+ end
20
+
21
+ args = parser.parse(ARGV)
22
+ if args.empty?
23
+ warn parser
24
+ exit 1
25
+ end
26
+
27
+ source_file = args.first
28
+ unless File.exist?(source_file)
29
+ warn "File not found: #{source_file}"
30
+ exit 1
31
+ end
32
+
33
+ begin
34
+ Sakusei::PreviewServer.new(source_file, options).run
35
+ rescue Interrupt
36
+ exit 0
37
+ rescue Sakusei::Error => e
38
+ warn "[sakusei-preview] #{e.message}"
39
+ exit 1
40
+ end
@@ -0,0 +1,31 @@
1
+ # Project Kickoff Notes
2
+
3
+ **Date:** 14 April 2026
4
+ **Attendees:** Sarah Chen, Marcus Webb, Priya Nair
5
+
6
+ ---
7
+
8
+ ## Goals for This Sprint
9
+
10
+ The team aligned on three priorities for the next two weeks:
11
+
12
+ 1. Finalise the data model and get sign-off from the product team
13
+ 2. Stand up the staging environment so QA can begin testing early
14
+ 3. Resolve the outstanding API authentication issues before the client demo
15
+
16
+ ## Key Decisions
17
+
18
+ - **Database:** Sticking with PostgreSQL — the migration cost to Mongo isn't justified at this scale
19
+ - **Auth:** Moving to OAuth 2.0 with JWT refresh tokens; existing session-based auth deprecated end of month
20
+ - **Deployment:** Staging mirrors production (same instance type, same region) to avoid environment surprises
21
+
22
+ ## Action Items
23
+
24
+ | Owner | Task | Due |
25
+ |--------|------------------------------------|--------|
26
+ | Marcus | Finalise schema and create migration | 17 Apr |
27
+ | Priya | Provision staging environment | 16 Apr |
28
+ | Sarah | Draft OAuth integration spec | 18 Apr |
29
+ | All | Review and comment on API auth RFC | 15 Apr |
30
+
31
+ > The client demo is locked in for the 25th — that date is immovable. Flag anything at risk by end of day Wednesday.
Binary file
@@ -0,0 +1,61 @@
1
+ # Meridian Property Group — Operations Automation Assessment
2
+
3
+ <vue-component name="DocMeta"
4
+ client="Meridian Property Group"
5
+ preparedBy="KD Consulting"
6
+ date="<%= today('%d %B %Y') %>"
7
+ status="Proposal"
8
+ />
9
+
10
+
11
+ Report Generated <%= today %>
12
+
13
+ ## Opportunity
14
+
15
+ Meridian Property Group manages a portfolio of 340 residential and commercial tenancies across Southeast Queensland. The business runs on strong fundamentals, but operational bandwidth is a growing constraint: leasing coordinators spend the majority of their week on routine correspondence, manual data entry, and chasing approvals through email chains. This is work that AI and automation can largely eliminate.
16
+
17
+ KD Consulting proposes a focused **Operations Automation Assessment** — a two-week engagement to map current workflows, identify the highest-impact automation opportunities, and deliver a clear implementation roadmap. The outcome is a practical action plan, not a theoretical report. Meridian's team will finish the engagement knowing exactly what to build, in what order, and what it will take to get there.
18
+
19
+
20
+ ## How We Work
21
+
22
+ Our methodology is structured around rapid discovery and focused analysis. We do not run open-ended workshops or produce lengthy process documentation for its own sake. Every step is oriented toward the output: a prioritised set of recommendations the business can act on.
23
+
24
+ <vue-component name="ProcessSteps" steps='[
25
+ {
26
+ "title": "Discovery",
27
+ "description": "Structured interviews with leasing coordinators, property managers, and leadership. We map the full lifecycle of a tenancy — from enquiry through to renewal or exit — and identify where time goes and where friction accumulates."
28
+ },
29
+ {
30
+ "title": "Analysis & Opportunity Mapping",
31
+ "description": "We analyse workflows against current AI and automation capabilities, assessing each opportunity for feasibility, integration requirements, and business impact. We distinguish quick wins from longer-horizon initiatives."
32
+ },
33
+ {
34
+ "title": "Roadmap & Report",
35
+ "description": "Prioritised recommendations delivered as a practical implementation roadmap — phased by effort and impact, with clear next steps for each initiative and indicative investment requirements."
36
+ }
37
+ ]' />
38
+
39
+
40
+ ## Deliverables
41
+
42
+ <vue-component name="DeliverableCards" columns="3" items='[
43
+ {
44
+ "title": "Workflow Assessment",
45
+ "description": "A clear map of current operations across leasing, tenancy management, and communications — documenting where time is spent and where the significant friction points lie."
46
+ },
47
+ {
48
+ "title": "Automation Opportunity Register",
49
+ "description": "A structured register of identified automation opportunities, each assessed for business impact, implementation effort, and integration requirements with existing systems."
50
+ },
51
+ {
52
+ "title": "Implementation Roadmap",
53
+ "description": "A phased, prioritised action plan with recommended sequencing, indicative timelines, and investment estimates — ready to brief a delivery team or internal stakeholders."
54
+ }
55
+ ]' />
56
+
57
+ ---
58
+
59
+ *This proposal is valid for 30 days from the date above. Prepared in confidence for Meridian Property Group.*
60
+
61
+ **KD Consulting**
Binary file
@@ -7,6 +7,7 @@ require_relative 'image_path_resolver'
7
7
  require_relative 'vue_processor'
8
8
  require_relative 'heading_wrapper'
9
9
  require_relative 'md_to_pdf_converter'
10
+ require_relative 'html_converter'
10
11
 
11
12
  module Sakusei
12
13
  class Builder
@@ -14,45 +15,64 @@ module Sakusei
14
15
  @source_file = File.expand_path(source_file)
15
16
  @options = options
16
17
  @source_dir = File.dirname(@source_file)
18
+ @file_resolver = nil
19
+ end
20
+
21
+ attr_reader :source_file, :source_dir
22
+
23
+ # Files that contributed to the most recent build (source + @include partials).
24
+ # Populated after #build, #build_html, or #build_processed_content runs.
25
+ def resolved_input_files
26
+ files = [@source_file]
27
+ files.concat(@file_resolver.resolved_files.to_a) if @file_resolver
28
+ files.uniq
17
29
  end
18
30
 
19
31
  def build
20
- # 1. Discover and load style pack
32
+ style_pack, processed_content = build_processed_content
33
+ $stderr.puts "[sakusei] converting to PDF..."
34
+ output_path = generate_output_path
35
+ convert_to_pdf(processed_content, output_path, style_pack)
36
+ $stderr.puts "[sakusei] written: #{output_path}"
37
+ output_path
38
+ end
39
+
40
+ # Run the full pipeline up to (but not including) PDF conversion, then
41
+ # render the processed markdown to a styled HTML string via md-to-pdf --as-html.
42
+ # Returns [html_string, style_pack].
43
+ def build_html
44
+ style_pack, processed_content = build_processed_content
45
+ html = HtmlConverter.new(processed_content, style_pack, @options.merge(source_dir: @source_dir)).convert
46
+ [html, style_pack]
47
+ end
48
+
49
+ private
50
+
51
+ def build_processed_content
21
52
  $stderr.puts "[sakusei] discovering style pack..."
22
53
  style_pack = discover_style_pack
23
54
  $stderr.puts "[sakusei] style pack: #{style_pack.name} (#{style_pack.path})"
24
55
 
25
- # 2. Resolve and concatenate file references
26
56
  $stderr.puts "[sakusei] resolving file includes..."
27
57
  resolved_content = resolve_files
28
58
 
29
- # 3. Process ERB templates
30
59
  $stderr.puts "[sakusei] processing ERB..."
31
60
  processed_content = process_erb(resolved_content)
32
61
 
33
- # 4. Process Vue components (if available)
62
+ processed_content = expand_break_syntax(processed_content)
34
63
  processed_content = process_vue(processed_content, style_pack)
35
-
36
- # 4.5 Wrap h2/h3 headings with their following block to prevent orphaned headings
37
64
  processed_content = wrap_headings(processed_content)
38
65
 
39
- # 5. Convert to PDF
40
- $stderr.puts "[sakusei] converting to PDF..."
41
- output_path = generate_output_path
42
- convert_to_pdf(processed_content, output_path, style_pack)
43
-
44
- $stderr.puts "[sakusei] written: #{output_path}"
45
- output_path
66
+ [style_pack, processed_content]
46
67
  end
47
68
 
48
- private
49
-
50
69
  def discover_style_pack
51
70
  StylePack.discover(@options[:source_dir] || @source_dir, @options[:style])
52
71
  end
53
72
 
54
73
  def resolve_files
55
- FileResolver.new(@source_file).resolve
74
+ @file_resolver = FileResolver.new(@source_file)
75
+ @file_resolver.resolve
56
76
  end
57
77
 
58
78
  def process_erb(content)
@@ -67,6 +87,10 @@ module Sakusei
67
87
  VueProcessor.new(content, @source_dir, style_pack: style_pack).process
68
88
  end
69
89
 
90
+ def expand_break_syntax(content)
91
+ content.gsub(/::break::/, '<div class="page-break"></div>')
92
+ end
93
+
70
94
  def wrap_headings(content)
71
95
  HeadingWrapper.new(content).wrap
72
96
  end