sakusei 0.1.0
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 +7 -0
- data/.gitignore +4 -0
- data/Gemfile +6 -0
- data/README.md +191 -0
- data/Rakefile +13 -0
- data/bin/sakusei +6 -0
- data/lib/sakusei/builder.rb +66 -0
- data/lib/sakusei/cli.rb +167 -0
- data/lib/sakusei/erb_processor.rb +56 -0
- data/lib/sakusei/file_resolver.rb +56 -0
- data/lib/sakusei/md_to_pdf_converter.rb +69 -0
- data/lib/sakusei/multi_file_builder.rb +77 -0
- data/lib/sakusei/pdf_concat.rb +69 -0
- data/lib/sakusei/style_pack.rb +146 -0
- data/lib/sakusei/style_preview.rb +148 -0
- data/lib/sakusei/version.rb +5 -0
- data/lib/sakusei/vue_processor.rb +141 -0
- data/lib/sakusei/vue_renderer.js +226 -0
- data/lib/sakusei.rb +26 -0
- data/lib/templates/base.css +243 -0
- data/lib/templates/default_style_pack/config.js +36 -0
- data/lib/templates/default_style_pack/footer.html +5 -0
- data/lib/templates/default_style_pack/header.html +5 -0
- data/lib/templates/default_style_pack/style.css +22 -0
- data/sakusei.gemspec +29 -0
- metadata +125 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Vue Server-Side Renderer for Sakusei (v2)
|
|
4
|
+
*
|
|
5
|
+
* Reads a JSON array of render jobs from stdin.
|
|
6
|
+
* Writes a JSON array of results to stdout.
|
|
7
|
+
*
|
|
8
|
+
* Each job: { id, componentFile, props, slotHtml }
|
|
9
|
+
* Each result: { id, html, css }
|
|
10
|
+
*
|
|
11
|
+
* Requires: npm install @vue/server-renderer @vue/compiler-sfc vue@3
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict'
|
|
15
|
+
const fs = require('fs')
|
|
16
|
+
const path = require('path')
|
|
17
|
+
const vm = require('vm')
|
|
18
|
+
const Module = require('module')
|
|
19
|
+
const crypto = require('crypto')
|
|
20
|
+
|
|
21
|
+
// Add CWD node_modules to module resolution (user's project dependencies)
|
|
22
|
+
const cwdNodeModules = path.join(process.cwd(), 'node_modules')
|
|
23
|
+
if (fs.existsSync(cwdNodeModules)) module.paths.unshift(cwdNodeModules)
|
|
24
|
+
|
|
25
|
+
const { parse, compileScript, compileTemplate, compileStyleAsync } = require('@vue/compiler-sfc')
|
|
26
|
+
const { renderToString } = require('@vue/server-renderer')
|
|
27
|
+
const { createSSRApp } = require('vue')
|
|
28
|
+
|
|
29
|
+
// Compiled code cache: filePath → { code, css }
|
|
30
|
+
const compiledCache = new Map()
|
|
31
|
+
|
|
32
|
+
// Transform ESM import/export syntax to CommonJS
|
|
33
|
+
function esmToCjs(code) {
|
|
34
|
+
// Named imports with optional aliases: import { a as b, c } from 'pkg'
|
|
35
|
+
code = code.replace(
|
|
36
|
+
/^import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']\s*;?$/gm,
|
|
37
|
+
(_, names, pkg) => {
|
|
38
|
+
const renamed = names.replace(/(\w+)\s+as\s+(\w+)/g, '$1: $2')
|
|
39
|
+
return `const {${renamed}} = require('${pkg}');`
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
// Default imports: import X from 'pkg'
|
|
43
|
+
code = code.replace(
|
|
44
|
+
/^import\s+(\w+)\s+from\s*["']([^"']+)["']\s*;?$/gm,
|
|
45
|
+
(_, name, pkg) => `const ${name} = require('${pkg}');`
|
|
46
|
+
)
|
|
47
|
+
// export function/const → remove export keyword
|
|
48
|
+
code = code.replace(/^export\s+((?:async\s+)?function|const|let|var)\b/gm, '$1')
|
|
49
|
+
// Remove bare named exports: export { ssrRender }
|
|
50
|
+
code = code.replace(/^export\s*\{[^}]*\}\s*;?$/gm, '')
|
|
51
|
+
return code
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Execute compiled CJS module code and return its exports
|
|
55
|
+
function executeModule(code, filePath) {
|
|
56
|
+
const patchedRequire = (id) => {
|
|
57
|
+
if (id.endsWith('.vue')) {
|
|
58
|
+
// Handle both relative and absolute .vue imports
|
|
59
|
+
const abs = path.isAbsolute(id) ? id : path.resolve(path.dirname(filePath), id)
|
|
60
|
+
const compiled = compiledCache.get(abs)
|
|
61
|
+
if (!compiled) throw new Error(`Component not pre-compiled: ${abs}`)
|
|
62
|
+
return executeModule(compiled.code, abs)
|
|
63
|
+
}
|
|
64
|
+
// Use the renderer's own require so CWD node_modules are on the search path
|
|
65
|
+
return require(id)
|
|
66
|
+
}
|
|
67
|
+
const mod = { exports: {} }
|
|
68
|
+
const wrapper = `(function(require, module, exports, __filename, __dirname) {\n${code}\n})`
|
|
69
|
+
const fn = vm.runInThisContext(wrapper, { filename: filePath })
|
|
70
|
+
fn(patchedRequire, mod, mod.exports, filePath, path.dirname(filePath))
|
|
71
|
+
return mod.exports
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Compile a .vue file (base form, no slot injection) and store in compiledCache
|
|
75
|
+
async function compileVueFile(filePath) {
|
|
76
|
+
if (compiledCache.has(filePath)) return compiledCache.get(filePath)
|
|
77
|
+
|
|
78
|
+
const source = fs.readFileSync(filePath, 'utf-8')
|
|
79
|
+
const { descriptor, errors: parseErrors } = parse(source, { filename: filePath })
|
|
80
|
+
if (parseErrors.length > 0) {
|
|
81
|
+
throw new Error(`Parse errors in ${path.basename(filePath)}: ${parseErrors.map(e => e.message).join(', ')}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const id = crypto.createHash('md5').update(filePath).digest('hex').slice(0, 8)
|
|
85
|
+
const hasScoped = descriptor.styles.some(s => s.scoped)
|
|
86
|
+
|
|
87
|
+
// Compile script section
|
|
88
|
+
let componentCode = 'const __component__ = {};'
|
|
89
|
+
let bindings
|
|
90
|
+
if (descriptor.scriptSetup || descriptor.script) {
|
|
91
|
+
const scriptResult = compileScript(descriptor, { id, genDefaultAs: '__component__' })
|
|
92
|
+
componentCode = esmToCjs(scriptResult.content)
|
|
93
|
+
bindings = scriptResult.bindings
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Compile template in SSR mode
|
|
97
|
+
const templateResult = compileTemplate({
|
|
98
|
+
source: descriptor.template ? descriptor.template.content : '<div></div>',
|
|
99
|
+
filename: filePath,
|
|
100
|
+
id,
|
|
101
|
+
scoped: hasScoped,
|
|
102
|
+
ssr: true,
|
|
103
|
+
compilerOptions: { bindingMetadata: bindings }
|
|
104
|
+
})
|
|
105
|
+
if (templateResult.errors.length > 0) {
|
|
106
|
+
throw new Error(`Template errors in ${path.basename(filePath)}: ${templateResult.errors.join(', ')}`)
|
|
107
|
+
}
|
|
108
|
+
const renderCode = esmToCjs(templateResult.code)
|
|
109
|
+
|
|
110
|
+
// Compile styles
|
|
111
|
+
let css = ''
|
|
112
|
+
for (const style of descriptor.styles) {
|
|
113
|
+
const result = await compileStyleAsync({
|
|
114
|
+
source: style.content,
|
|
115
|
+
filename: filePath,
|
|
116
|
+
id,
|
|
117
|
+
scoped: style.scoped || false
|
|
118
|
+
})
|
|
119
|
+
if (!result.errors.length) css += result.code + '\n'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const fullCode = `${componentCode}\n${renderCode}\n__component__.ssrRender = ssrRender;\nmodule.exports = __component__;`
|
|
123
|
+
const compiled = { code: fullCode, css }
|
|
124
|
+
compiledCache.set(filePath, compiled)
|
|
125
|
+
return compiled
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Scan source for .vue imports and pre-compile them recursively
|
|
129
|
+
async function preCompileImports(filePath, visited = new Set()) {
|
|
130
|
+
if (visited.has(filePath) || !fs.existsSync(filePath)) return
|
|
131
|
+
visited.add(filePath)
|
|
132
|
+
|
|
133
|
+
const source = fs.readFileSync(filePath, 'utf-8')
|
|
134
|
+
// Match both relative (./foo.vue) and absolute (/path/to/foo.vue) imports
|
|
135
|
+
const importMatches = [...source.matchAll(/import\s+\w+\s+from\s*['"]([^'"]+\.vue)['"]/g)]
|
|
136
|
+
|
|
137
|
+
for (const match of importMatches) {
|
|
138
|
+
const importPath = match[1]
|
|
139
|
+
const importedPath = path.isAbsolute(importPath)
|
|
140
|
+
? importPath
|
|
141
|
+
: path.resolve(path.dirname(filePath), importPath)
|
|
142
|
+
await preCompileImports(importedPath, visited)
|
|
143
|
+
await compileVueFile(importedPath)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Compile and render a single job, injecting slotHtml at the template level
|
|
148
|
+
async function renderJob(job) {
|
|
149
|
+
const { id, componentFile, props, slotHtml } = job
|
|
150
|
+
|
|
151
|
+
if (!componentFile || !fs.existsSync(componentFile)) {
|
|
152
|
+
return { id, html: `<!-- Vue component not found: ${path.basename(componentFile || 'unknown')} -->`, css: '' }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Pre-compile all transitively imported .vue files first
|
|
157
|
+
await preCompileImports(componentFile)
|
|
158
|
+
|
|
159
|
+
// Compile base form (for CSS and script)
|
|
160
|
+
const base = await compileVueFile(componentFile)
|
|
161
|
+
|
|
162
|
+
let finalCode = base.code
|
|
163
|
+
|
|
164
|
+
// If slot content provided, recompile template with slot injected
|
|
165
|
+
if (slotHtml) {
|
|
166
|
+
const source = fs.readFileSync(componentFile, 'utf-8')
|
|
167
|
+
const { descriptor } = parse(source, { filename: componentFile })
|
|
168
|
+
const idHash = crypto.createHash('md5').update(componentFile).digest('hex').slice(0, 8)
|
|
169
|
+
const hasScoped = descriptor.styles.some(s => s.scoped)
|
|
170
|
+
|
|
171
|
+
let scriptCode = 'const __component__ = {};'
|
|
172
|
+
let bindings
|
|
173
|
+
if (descriptor.scriptSetup || descriptor.script) {
|
|
174
|
+
const scriptResult = compileScript(descriptor, { id: idHash, genDefaultAs: '__component__' })
|
|
175
|
+
scriptCode = esmToCjs(scriptResult.content)
|
|
176
|
+
bindings = scriptResult.bindings
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const templateSource = (descriptor.template ? descriptor.template.content : '<div></div>')
|
|
180
|
+
.replace(/<slot\s*\/>/g, slotHtml)
|
|
181
|
+
.replace(/<slot>\s*<\/slot>/g, slotHtml)
|
|
182
|
+
|
|
183
|
+
const templateResult = compileTemplate({
|
|
184
|
+
source: templateSource,
|
|
185
|
+
filename: componentFile,
|
|
186
|
+
id: idHash,
|
|
187
|
+
scoped: hasScoped,
|
|
188
|
+
ssr: true,
|
|
189
|
+
compilerOptions: { bindingMetadata: bindings }
|
|
190
|
+
})
|
|
191
|
+
const renderCode = esmToCjs(templateResult.code)
|
|
192
|
+
finalCode = `${scriptCode}\n${renderCode}\n__component__.ssrRender = ssrRender;\nmodule.exports = __component__;`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const component = executeModule(finalCode, componentFile)
|
|
196
|
+
const app = createSSRApp(component, props)
|
|
197
|
+
const html = await renderToString(app)
|
|
198
|
+
|
|
199
|
+
return { id, html, css: base.css || '' }
|
|
200
|
+
} catch (err) {
|
|
201
|
+
process.stderr.write(`Vue render error (${path.basename(componentFile)}): ${err.message}\n`)
|
|
202
|
+
return { id, html: `<!-- Vue component error: ${err.message} -->`, css: '' }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function main() {
|
|
207
|
+
let input = ''
|
|
208
|
+
process.stdin.setEncoding('utf-8')
|
|
209
|
+
for await (const chunk of process.stdin) input += chunk
|
|
210
|
+
|
|
211
|
+
let jobs
|
|
212
|
+
try {
|
|
213
|
+
jobs = JSON.parse(input)
|
|
214
|
+
} catch (e) {
|
|
215
|
+
process.stderr.write(`Failed to parse input JSON: ${e.message}\n`)
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const results = await Promise.all(jobs.map(renderJob))
|
|
220
|
+
process.stdout.write(JSON.stringify(results) + '\n')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
main().catch(e => {
|
|
224
|
+
process.stderr.write(`Fatal error: ${e.message}\n`)
|
|
225
|
+
process.exit(1)
|
|
226
|
+
})
|
data/lib/sakusei.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'set'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
require_relative 'sakusei/version'
|
|
8
|
+
require_relative 'sakusei/cli'
|
|
9
|
+
require_relative 'sakusei/builder'
|
|
10
|
+
require_relative 'sakusei/style_pack'
|
|
11
|
+
require_relative 'sakusei/file_resolver'
|
|
12
|
+
require_relative 'sakusei/erb_processor'
|
|
13
|
+
require_relative 'sakusei/md_to_pdf_converter'
|
|
14
|
+
require_relative 'sakusei/pdf_concat'
|
|
15
|
+
require_relative 'sakusei/multi_file_builder'
|
|
16
|
+
require_relative 'sakusei/style_preview'
|
|
17
|
+
require_relative 'sakusei/vue_processor'
|
|
18
|
+
|
|
19
|
+
module Sakusei
|
|
20
|
+
class Error < StandardError; end
|
|
21
|
+
|
|
22
|
+
# Main entry point for building PDFs
|
|
23
|
+
def self.build(source_file, options = {})
|
|
24
|
+
Builder.new(source_file, options).build
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/* Sakusei Base Styles - Applied before style pack CSS */
|
|
2
|
+
|
|
3
|
+
/* CSS Reset and Base Typography */
|
|
4
|
+
* {
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
html {
|
|
9
|
+
font-size: 100%;
|
|
10
|
+
-webkit-print-color-adjust: exact;
|
|
11
|
+
print-color-adjust: exact;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family:
|
|
16
|
+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
|
17
|
+
sans-serif;
|
|
18
|
+
font-size: 11pt;
|
|
19
|
+
line-height: 1.6;
|
|
20
|
+
color: #333;
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Headings */
|
|
26
|
+
h1,
|
|
27
|
+
h2,
|
|
28
|
+
h3,
|
|
29
|
+
h4,
|
|
30
|
+
h5,
|
|
31
|
+
h6 {
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
line-height: 1.3;
|
|
34
|
+
margin-top: 1.5em;
|
|
35
|
+
margin-bottom: 0.5em;
|
|
36
|
+
color: #222;
|
|
37
|
+
page-break-after: avoid;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
h1 {
|
|
41
|
+
font-size: 2em;
|
|
42
|
+
}
|
|
43
|
+
h2 {
|
|
44
|
+
font-size: 1.5em;
|
|
45
|
+
}
|
|
46
|
+
h3 {
|
|
47
|
+
font-size: 1.25em;
|
|
48
|
+
}
|
|
49
|
+
h4 {
|
|
50
|
+
font-size: 1.1em;
|
|
51
|
+
}
|
|
52
|
+
h5 {
|
|
53
|
+
font-size: 1em;
|
|
54
|
+
}
|
|
55
|
+
h6 {
|
|
56
|
+
font-size: 0.9em;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Paragraphs */
|
|
60
|
+
p {
|
|
61
|
+
margin-top: 0;
|
|
62
|
+
margin-bottom: 1em;
|
|
63
|
+
orphans: 3;
|
|
64
|
+
widows: 3;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Links */
|
|
68
|
+
a {
|
|
69
|
+
color: #0366d6;
|
|
70
|
+
text-decoration: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
a:hover {
|
|
74
|
+
text-decoration: underline;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Code */
|
|
78
|
+
code,
|
|
79
|
+
kbd,
|
|
80
|
+
pre,
|
|
81
|
+
samp {
|
|
82
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
code {
|
|
86
|
+
background-color: rgba(175, 184, 193, 0.2);
|
|
87
|
+
padding: 0.2em 0.4em;
|
|
88
|
+
border-radius: 3px;
|
|
89
|
+
font-size: 0.9em;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pre {
|
|
93
|
+
background-color: #f6f8fa;
|
|
94
|
+
padding: 1em;
|
|
95
|
+
border-radius: 6px;
|
|
96
|
+
overflow-x: auto;
|
|
97
|
+
page-break-inside: avoid;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pre code {
|
|
101
|
+
background: none;
|
|
102
|
+
padding: 0;
|
|
103
|
+
font-size: 1em;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Blockquotes */
|
|
107
|
+
blockquote {
|
|
108
|
+
border-left: 4px solid #ddd;
|
|
109
|
+
padding-left: 1em;
|
|
110
|
+
margin-left: 0;
|
|
111
|
+
margin-right: 0;
|
|
112
|
+
color: #666;
|
|
113
|
+
page-break-inside: avoid;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Lists */
|
|
117
|
+
ul,
|
|
118
|
+
ol {
|
|
119
|
+
padding-left: 2em;
|
|
120
|
+
margin-top: 0;
|
|
121
|
+
margin-bottom: 1em;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
li {
|
|
125
|
+
margin-bottom: 0.25em;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
li > ul,
|
|
129
|
+
li > ol {
|
|
130
|
+
margin-top: 0.25em;
|
|
131
|
+
margin-bottom: 0.25em;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Tables */
|
|
135
|
+
table {
|
|
136
|
+
border-collapse: collapse;
|
|
137
|
+
width: 100%;
|
|
138
|
+
margin-bottom: 1em;
|
|
139
|
+
page-break-inside: avoid;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
th,
|
|
143
|
+
td {
|
|
144
|
+
border: 1px solid #d0d7de;
|
|
145
|
+
padding: 0.5em;
|
|
146
|
+
text-align: left;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
th {
|
|
150
|
+
background-color: #f6f8fa;
|
|
151
|
+
font-weight: 600;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
tr {
|
|
155
|
+
page-break-inside: avoid;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Images */
|
|
159
|
+
img {
|
|
160
|
+
max-width: 100%;
|
|
161
|
+
height: auto;
|
|
162
|
+
page-break-inside: avoid;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Horizontal rules */
|
|
166
|
+
hr {
|
|
167
|
+
border: none;
|
|
168
|
+
border-top: 1px solid #ddd;
|
|
169
|
+
margin: 1.5em 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Page breaks */
|
|
173
|
+
.page-break,
|
|
174
|
+
.page-break-after {
|
|
175
|
+
page-break-after: always;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.page-break-before {
|
|
179
|
+
page-break-before: always;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Keep together - prevent page breaks inside these elements */
|
|
183
|
+
.keep-together,
|
|
184
|
+
figure,
|
|
185
|
+
figcaption,
|
|
186
|
+
dl,
|
|
187
|
+
dt,
|
|
188
|
+
dd,
|
|
189
|
+
details,
|
|
190
|
+
summary,
|
|
191
|
+
.admonition,
|
|
192
|
+
.callout,
|
|
193
|
+
.note,
|
|
194
|
+
.warning,
|
|
195
|
+
.tip,
|
|
196
|
+
.info,
|
|
197
|
+
.card,
|
|
198
|
+
.box,
|
|
199
|
+
.math,
|
|
200
|
+
.katex,
|
|
201
|
+
.katex-display {
|
|
202
|
+
page-break-inside: avoid;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Additional protection for table elements */
|
|
206
|
+
thead,
|
|
207
|
+
tbody,
|
|
208
|
+
tfoot {
|
|
209
|
+
page-break-inside: avoid;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Print-specific */
|
|
213
|
+
@media print {
|
|
214
|
+
body {
|
|
215
|
+
font-size: 10pt;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
h1,
|
|
219
|
+
h2,
|
|
220
|
+
h3,
|
|
221
|
+
h4,
|
|
222
|
+
h5,
|
|
223
|
+
h6 {
|
|
224
|
+
page-break-after: avoid;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
tr,
|
|
228
|
+
img,
|
|
229
|
+
pre,
|
|
230
|
+
blockquote {
|
|
231
|
+
page-break-inside: avoid;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
a {
|
|
235
|
+
color: #333;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
a[href]:after {
|
|
239
|
+
content: " (" attr(href) ")";
|
|
240
|
+
font-size: 0.8em;
|
|
241
|
+
color: #666;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
// PDF options
|
|
3
|
+
pdf_options: {
|
|
4
|
+
format: 'A4',
|
|
5
|
+
printBackground: true,
|
|
6
|
+
margin: {
|
|
7
|
+
top: '2cm',
|
|
8
|
+
right: '2cm',
|
|
9
|
+
bottom: '2cm',
|
|
10
|
+
left: '2cm'
|
|
11
|
+
},
|
|
12
|
+
displayHeaderFooter: true,
|
|
13
|
+
headerTemplate: `
|
|
14
|
+
<div style="font-size: 9px; width: 100%; text-align: center; color: #666;">
|
|
15
|
+
<span class="title"></span>
|
|
16
|
+
</div>
|
|
17
|
+
`,
|
|
18
|
+
footerTemplate: `
|
|
19
|
+
<div style="font-size: 9px; width: 100%; text-align: center; color: #666;">
|
|
20
|
+
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
|
|
21
|
+
</div>
|
|
22
|
+
`
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Markdown-it options
|
|
26
|
+
md_options: {
|
|
27
|
+
html: true,
|
|
28
|
+
linkify: true,
|
|
29
|
+
typographer: true
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Launch options for Puppeteer
|
|
33
|
+
launch_options: {
|
|
34
|
+
headless: true
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- Custom footer template - used when configured in config.js -->
|
|
2
|
+
<!-- Available classes: date, title, url, pageNumber, totalPages -->
|
|
3
|
+
<div style="font-size: 9px; width: 100%; text-align: center; color: #666;">
|
|
4
|
+
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
|
|
5
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* Sakusei Default Style Pack - Override styles applied on top of base */
|
|
2
|
+
|
|
3
|
+
/* Heading enhancements */
|
|
4
|
+
h1 {
|
|
5
|
+
border-bottom: 2px solid #eee;
|
|
6
|
+
padding-bottom: 0.3em;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
h2 {
|
|
10
|
+
border-bottom: 1px solid #eee;
|
|
11
|
+
padding-bottom: 0.3em;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Code block styling */
|
|
15
|
+
code {
|
|
16
|
+
background-color: #f6f8fa;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Link hover enhancement */
|
|
20
|
+
a:hover {
|
|
21
|
+
text-decoration: underline;
|
|
22
|
+
}
|
data/sakusei.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require_relative 'lib/sakusei/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = 'sakusei'
|
|
5
|
+
spec.version = Sakusei::VERSION
|
|
6
|
+
spec.authors = ['Keith Rowell']
|
|
7
|
+
spec.email = ['keith@example.com']
|
|
8
|
+
|
|
9
|
+
spec.summary = 'A PDF building system using Markdown, ERB, and CSS templating'
|
|
10
|
+
spec.description = 'Sakusei is a build system for creating PDF documents from Markdown sources with support for ERB templates, VueJS components, and hierarchical styling.'
|
|
11
|
+
spec.homepage = 'https://github.com/keithrowell/sakusei'
|
|
12
|
+
spec.license = 'MIT'
|
|
13
|
+
|
|
14
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
15
|
+
|
|
16
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
17
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
18
|
+
end
|
|
19
|
+
spec.bindir = 'bin'
|
|
20
|
+
spec.executables = ['sakusei']
|
|
21
|
+
spec.require_paths = ['lib']
|
|
22
|
+
|
|
23
|
+
# Dependencies
|
|
24
|
+
spec.add_dependency 'thor', '~> 1.2'
|
|
25
|
+
spec.add_dependency 'erb', '~> 4.0'
|
|
26
|
+
|
|
27
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
|
28
|
+
spec.add_development_dependency 'rubocop', '~> 1.0'
|
|
29
|
+
end
|