
Book Writing Workspace
- 179 installs
- 23 repo stars
- Updated August 4, 2026
- aktsmm/agent-skills
Organize long-form book projects with chapters, outlines, research notes, and revision passes so agents and authors co-write structured manuscripts without losing continuity or version context.
About
book-writing-workspace from aktsmm/agent-skills sets up an agent-friendly book production workspace with outlines, chapter files, research notes, and revision loops so authors and coding agents collaborate on complete manuscripts efficiently.
- Chapter and outline scaffolding
- Research note organization
- Iterative drafting workflows
- Revision and continuity checks
- Long-form co-writing workspace
Book Writing Workspace by the numbers
- 179 all-time installs (skills.sh)
- Ranked #535 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aktsmm/agent-skills --skill book-writing-workspaceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 179 |
|---|---|
| repo stars | ★ 23 |
| Last updated | August 4, 2026 |
| Repository | aktsmm/agent-skills ↗ |
What it does
Organize long-form book projects with chapters, outlines, research notes, and revision passes so agents and authors co-write structured manuscripts without losing continuity or version context.
Files
Book Writing Workspace
Create and maintain a reusable manuscript workspace with folders, writing agents, instructions, review rules, and optional Markdown -> Re:VIEW -> PDF support.
When to use
- Book writing, technical writing, 執筆プロジェクト, Re:VIEW
- Assessing or standardizing an existing book manuscript repository
- Creating a new technical writing project from templates when needed
- Setting up Markdown → Re:VIEW → PDF workflow
- Upgrading an existing manuscript workspace so it can generate Re:VIEW output and printable PDFs
- Standardizing chapter/section file layout, writing rules, review workflow, and page allocation
- Adding optional Re:VIEW/PDF support without turning the writing workspace into a general Git or publishing operations toolkit
Quick Start
Start by assessing the manuscript workspace, even when creating a new project:
1. Confirm the main manuscript lives in sections/ and uses one file per section. 2. Confirm outlines or key points live separately from final manuscript files. 3. Confirm chapter intro files use kebab-case naming (e.g. 00-introduction/00-introduction.md). 4. Confirm writing, heading, notation, page allocation, and review rules are available. 5. Decide whether Re:VIEW/PDF output is needed for this project; keep it optional unless the workflow requires it.
Operating Workflow
When the workspace already exists, do not stop at setup-oriented advice. This skill should also support:
1. Normalizing manuscript folders and section naming. 2. Keeping outlines, drafts, final manuscript, and images aligned by chapter. 3. Running focused writing and review loops until P1/P2 issues are resolved. 4. Checking word count targets and source confidence before finalizing text. 5. Enabling Re:VIEW/PDF support only when the project needs reproducible output.
Bootstrap Workflow
Use the setup script only when creating a new workspace or adding missing structure deliberately.
python scripts/setup_workspace.py `
--name "project-name" `
--title "Book Title" `
--path "D:\target\path" `
--chapters 8
# Include Re:VIEW/PDF scaffolding only when needed.
python scripts/setup_workspace.py `
--name "project-name" `
--title "Book Title" `
--path "D:\target\path" `
--chapters 8 `
--with-review1. Gather info: Project name, title, location, chapter count 2. Run script: scripts/setup_workspace.py 3. Review output: Confirm README, agents, instructions, and docs were created 4. Customize: Edit docs/page-allocation.md, docs/schedule.md, and .github/copilot-instructions.md. If --with-review is used, also customize config/review-metadata/project.yml.
Git workflows are project-specific. Do not add generic commit/push prompts here; follow the repository's existing version-control conventions.
Metadata, migration, converter verification, and sync-back rules live in references. Keep the main SKILL focused on manuscript structure and writing workflow.
Generated Workspace
- Manuscript folders under
keypoints/,sections/, andimages/ - AI workflow files under
.github/agents/and.github/instructions/ - Project docs such as
README.md,docs/page-allocation.md, anddocs/schedule.md - Helper scripts such as
scripts/count_chars.py - Optional Re:VIEW scripts and metadata when
--with-reviewis used
Recommended Writing Unit
- Use 1 file = 1 section as the default manuscript unit.
- Keep chapter intro in
{NN}-{slug}/{NN}-{slug}.mdand section files alongside it. - For PDF/Re:VIEW output, heading levels define hierarchy, while file split mainly improves authoring and review workflow.
Agents Overview
| Agent | Role | Default |
|---|---|---|
@writing | Write and edit manuscripts | Yes |
@writing-reviewer | Review manuscripts (P1/P2/P3) | Yes |
@converter | Convert Markdown to Re:VIEW | Only with --with-review |
Dependencies
| Tool | Purpose | Required |
|---|---|---|
| Python 3.8+ | Scripts | Yes |
| Git | Version control | Yes |
| Docker | Re:VIEW PDF build | Optional, only with --with-review |
Reference Map
| Topic | Reference |
|---|---|
| Folder structure | references/folder-structure.md |
| Setup workflow | references/setup-workflow.md |
| Customization points | references/customization-points.md |
| Re:VIEW / PDF tips | references/review-pdf-tips.md |
Done Criteria
- [ ] Workspace folder structure created
- [ ] Writing and review agents deployed to
.github/agents/ - [ ]
docs/page-allocation.mdconfigured - [ ]
README.mdanddocs/schedule.mdcustomized - [ ] Manuscript files follow the chapter/section naming convention
- [ ]
scripts/count_chars.pyworks for target manuscript files - [ ] Re:VIEW/PDF output is either explicitly out of scope or enabled and verified
# Book Writing Workspace
# Ignore patterns for git
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv/
venv/
ENV/
# Node.js (if using Re:VIEW with npm)
node_modules/
.npm-cache/
# IDE
.vscode/settings.json
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
Desktop.ini
# Build outputs
pdf/*.pdf
*.log
# Temporary files
*.tmp
*.temp
*.bak
# Sensitive files
*.env
.env.local
secrets/
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
## English
Copyright (c) 2025-2026 yamapan (aktsmm)
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
International License.
You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the
license, and indicate if changes were made. You may do so in any reasonable manner,
but not in any way that suggests the licensor endorses you or your use.
- **NonCommercial** — You may not use the material for commercial purposes.
*(Please contact the author if you wish to use this material for commercial purposes.)*
- **ShareAlike** — If you remix, transform, or build upon the material, you must
distribute your contributions under the same license as the original.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
**AI/ML Training Restriction** — Use of this content for AI/ML training, data
mining, or other analytical purposes is prohibited without explicit permission.
Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
---
## 日本語
Copyright (c) 2025-2026 yamapan (aktsmm)
この作品はクリエイティブ・コモンズ 表示-非営利-継承 4.0 国際ライセンスの下に提供されています。
あなたは以下の条件に従う限り、自由に:
- **共有** — どのようなメディアやフォーマットでも資料を複製・再配布できます
- **翻案** — 資料をリミックス、変形、および加工することができます
以下の条件に従ってください:
- **表示** — あなたは適切なクレジットを表示し、ライセンスへのリンクを提供し、
変更があったらその旨を示さなければなりません。これらは合理的であればどのような方法で
行っても構いませんが、許諾者があなたやあなたの利用行為を支持していると示唆するような
方法は除きます。
- **非営利** — あなたは営利目的でこの資料を利用してはなりません。
(※商用利用をご希望の場合は、別途ご連絡ください。)
- **継承** — もしあなたがこの資料をリミックス、変形、または加工した場合、
あなたはあなたの貢献部分を元の作品と同じライセンスの下で配布しなければなりません。
追加的な制約は課せません — あなたは、このライセンスが他の者に許諾することを法的に
制限するような法的条項や技術的手段を適用してはなりません。
**AI/MLトレーニング制限** — 本コンテンツをAI/MLモデルのトレーニング、データマイニング、
その他の解析目的での使用は明示的な許可なく禁止されています。
ライセンス全文: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ja
---
## Special Permission for Microsoft Employees / Microsoft 社員向け特別許諾
### English
Microsoft Corporation employees are granted permission to use, copy, modify, and
distribute this material for any purpose within the scope of their employment
duties at Microsoft, including internal business use and customer-facing
activities, without the NonCommercial restriction of this license.
This special permission applies only to work performed as part of official
Microsoft business activities.
### 日本語
Microsoft Corporation の社員は、Microsoft での業務の範疇において、本資料を社内業務
および顧客対応を含むあらゆる目的で使用、複製、改変、配布することが許諾されます。
この場合、本ライセンスの「非営利」制限は適用されません。
この特別許諾は、Microsoft の公式な業務活動の一環として行われる作業にのみ適用されます。
---
## Disclaimer / 免責事項
### English
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### 日本語
本ソフトウェアは「現状のまま」で提供され、明示または黙示を問わず、商品性、
特定目的への適合性、および権利非侵害についての保証を含むがこれに限定されない、
いかなる種類の保証も伴いません。作者または著作権者は、契約行為、不法行為、
またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの
使用またはその他の扱いによって生じる一切の請求、損害、その他の責任について
責任を負いません。
Customization Points
These files are expected to be edited immediately after workspace creation.
Project Metadata
| File | Why edit it |
|---|---|
README.md | Describe the book scope, workflow, and repository usage |
.github/copilot-instructions.md | Define readers, goals, and project-specific constraints |
config/review-metadata/common.yml | Tune stable shared defaults such as cover palette and colophon defaults |
config/review-metadata/project.yml | Set project-specific metadata such as author list, publisher, title, subtitle, and cover badge |
Writing Management
| File | Why edit it |
|---|---|
docs/page-allocation.md | Set chapter and file-level character targets |
docs/schedule.md | Replace placeholder milestones and track progress |
docs/naming-conventions.md | Adjust file and image naming if the team uses a different scheme |
.github/instructions/writing/writing.instructions.md | Tune table-first writing, figure usage, and section structure rules |
.github/instructions/writing/writing-notation.instructions.md | Tune language, notation, and diagram label rules for the manuscript |
Outline vs Manuscript Presentation Rules
If the workspace uses both outline files and final manuscript files, define early which ideas should be planned as diagrams and which should be rendered as tables in the full text.
Recommended Rule
- Put diagram candidates in the outline or key-points side before drafting the full prose
- For each diagram candidate, capture the reader goal, not just the topic name
- Use tables in the final manuscript when comparing 3 or more terms, roles, plans, tools, or options
- Use diagrams for flow, direction, sequence, hierarchy, or source/target relationships
- Do not leave comparison-heavy sections as long bullet lists if a table would let the reader re-scan faster
Why This Matters
Diagram needs are often discovered too late if they are deferred until section drafting. Likewise, comparison-heavy sections become harder to read when they stay as prose or bullets instead of being normalized into tables.
Verification and Diagram Language Rules
Before large-scale drafting begins, decide two policy points explicitly.
Recommended Rule
- Keep unverified items in outline or key-points material as explicit follow-up notes
- Do not let unresolved facts drift into final prose as plausible-sounding explanation
- Decide whether diagrams should default to the manuscript language, and document the limited cases where original-language labels stay visible
- Keep operation names or official feature names in their original language only when readers are expected to recognize them that way
Why This Matters
Speculative prose becomes expensive to detect once it has spread across multiple chapters. Likewise, diagrams often drift into all-English labeling even when the manuscript is written for another language audience, which creates avoidable reading friction.
Automation
| File | Why edit it |
|---|---|
.github/agents/*.agent.md | Tune writing/review permissions for your team workflow |
scripts/count_chars.py | Check manuscript length against page allocation targets |
scripts/convert_md_to_review.py | Optional: extend conversion rules when Re:VIEW is enabled |
scripts/build_review_pdf.py | Optional: build the final PDF and regenerate cover assets |
scripts/review_metadata.py | Optional: merge metadata defaults and generate config/cover files |
Metadata Layer
If the workspace uses a shared metadata layer for Re:VIEW/PDF output, separate stable defaults from project-specific fields.
Recommended Rule
- Put only stable shared defaults in
config/review-metadata/common.yml - Keep author lists, publisher, title, subtitle, and other project-specific fields in
config/review-metadata/project.yml - Do not place contributor-specific names in the common metadata file
- Keep cover author typography settings such as suffix, font size, and position in metadata/helper defaults
- If the repo contains a series, compare title patterns across books before finalizing one book's
booktitleorsubtitle - Regenerate
config.ymland cover assets from the metadata layer instead of editing generated files by hand
Why This Matters
People, publisher details, title wording, and cover readability vary per project. If they are stored in a common metadata file or fixed only in exported images, they leak across projects and are easy to forget during setup or retrofits.
LaTeX Style Injection Point
Build scripts that use vvakame/review Docker images typically copy the gem's default review-jsbook files into sty/ at the start of each build, overwriting any local edits.
Rule
- Never edit `sty/review-jsbook.cls` or `sty/review-base.sty` directly — changes will be lost
- Place all LaTeX customizations in the custom sty content that the build script injects as
sty/review-custom.sty - In PowerShell build scripts, this is typically a heredoc variable (e.g.
$customStyContent) written to a file before the Docker run - In shell-based workflows, append to
sty/review-style.styafter the gem copy step
Why This Matters
If you add \usepackage{xurl} or other fixes directly to sty/review-jsbook.cls, the next build silently reverts those changes. The fix appears to work in manual testing but fails in CI or clean builds.
Heading Rename Safety
When changing chapter titles that are also reflected in folder names, file names, or planning docs, do not stop at the first rename.
Recommended Rule
- Rename the manuscript heading and every synchronized path together
- Check that the old folder or file no longer exists physically
- Grep the old chapter title after the rename to catch leftovers in docs, agents, or instructions
- Rebuild Re:VIEW/PDF output and confirm the old path is not still being converted
Why This Matters
If the old chapter path remains on disk, build scripts may pick up both old and new files. That can produce duplicated chapters or stale generated output that is hard to diagnose from PDF alone.
Sync-Back from Final Manuscripts to Author Drafts
If your workflow keeps both final manuscripts and author draft folders, define a clear sync-back rule before copying text from the final manuscript side back into drafts.
Recommended Rule
- Sync the prose that should stay aligned between final manuscript and draft
- Preserve draft-local relative asset paths if the draft folder uses a different image layout
- Do not blindly copy generated output back into draft folders
- Do not update unrelated sibling draft files just because they share the same author folder
- Keep keypoints, notes, and draft-only planning files as separate sources unless the task explicitly says to synchronize them
- After sync-back, verify matched manuscript pairs with
git diff --no-index, hashes, or an equivalent exact comparison - Treat chapter intro files such as
ch*-00_*.mdas mandatory sync-check targets because they drift easily
Typical Example
If the final manuscript uses:
but the author draft keeps the image next to the draft file:
then sync the prose but preserve the draft-local image path instead of copying the final path verbatim.
Why This Matters
Draft folders often serve as working sources for individual authors. They may have:
- different relative paths
- rough notes not intended for final manuscript
- additional sibling files such as keypoints or partial section drafts
Treat sync-back as a selective editorial sync, not as a raw mirror operation.
Workspace Folder Structure
Generated projects use the following top-level layout.
Required Folders
| Folder | Purpose |
|---|---|
.github/agents/ | Writing and review agents |
.github/instructions/ | Writing instructions used by the agents |
keypoints/ | Outline and section key points |
sections/ | Draft and final manuscript files |
images/ | Figures and image assets by chapter |
docs/ | Page allocation, schedule, naming rules, workflow docs |
scripts/ | Helper scripts for manuscript checks |
Optional Folders
| Folder | Created When |
|---|---|
re-view-output/ | Created when --with-review is used |
materials/ | Default reference materials folder |
Chapter Layout
Each chapter gets a matching subtree in the manuscript folders.
keypoints/
00-introduction/
sections/
00-introduction/
images/
00-introduction/This symmetry keeps outlines, drafts, and images aligned by chapter.
Re:VIEW PDF Build Tips
Practical tips for customizing PDF output with Re:VIEW 5.x and the vvakame/review Docker image.
Font Selection
Add jafont=<preset> to texdocumentclass options in config.yml:
texdocumentclass: ["review-jsbook", "media=ebook,paper=b5,...,jafont=noto-otf"]Available Presets (vvakame/review:5.9)
| Preset | Mincho (Body) | Gothic (Headings) | Notes |
|---|---|---|---|
| (default) | IPAex Mincho | IPAex Gothic | Classic, slightly dated |
haranoaji | Harano Aji Mincho | Harano Aji Gothic | TeX Live default, sharp |
noto-otf | Noto Serif CJK JP | Noto Sans CJK JP | Google/Adobe, rich weights |
noto-otf provides ExtraLight through Black weights and is pre-installed in the Docker image.TOC Depth Control
Problem
config.yml's tocdepth setting maps to \def\review@tocdepth{N} in the generated .tex. However, Re:VIEW's \reviewtableofcontents macro applies this value after document begin, so neither direct \setcounter{tocdepth} nor \AtBeginDocument in review-style.sty can override it.
Solution
Redefine \review@tocdepth itself in review-style.sty:
\makeatletter
\def\review@tocdepth{1}
\makeatotherDepth Values
| Value | Shows in TOC | Markdown equivalent |
|---|---|---|
| 0 | Chapter (#) only | # |
| 1 | + Section (##) | #, ## |
| 2 | + Subsection (###) | #, ##, ### |
Build Script Integration
When using a build script that injects styles via printf >> sty/review-style.sty:
printf '%s\n' \
'% TOC depth: show only chapter and section' \
'\makeatletter' \
'\def\review@tocdepth{1}' \
'\makeatother' \
>> /work/sty/review-style.styHeader/Footer Customization
Chapter Number Duplication and Partial Build Renumbering
Problem 1: Chapter Number Appears Twice
If the manuscript Markdown H1 already contains a literal chapter prefix such as 第7章 ..., Re:VIEW may still add its own chapter number in the PDF or EPUB output. That produces headings like 第4章 第7章 ... when chapter numbering is also generated by the builder.
Recommended Rule
- Treat chapter numbering as a build concern, not as a hard-coded H1 prefix in generated output
- If author-facing Markdown keeps
第N章for readability, strip that prefix in the Markdown-to-Re:VIEW converter for level-1 headings - Do not patch generated
.refiles by hand; fix the converter or source side instead
Why This Matters
The duplication is easy to miss in raw Markdown because it only appears after Re:VIEW numbering is applied. Fixing it in the converter prevents the same problem from reappearing across PDF, EPUB, and TOC output.
Problem 2: Partial Builds Renumber Chapters
If you regenerate only a subset of chapters and also regenerate catalog.yml for that subset, Re:VIEW will number those chapters from 1 again. This is correct for the temporary partial catalog, but misleading for final numbering review.
Recommended Rule
- Use partial conversion/builds for fast iteration on layout, wording, and local rendering issues
- Do not treat chapter numbers or TOC numbering from a partial catalog as the final source of truth
- For chapter-number review, regenerate the full catalog and rebuild the full book range
Why This Matters
Partial build output is useful for speed, but it can make a later chapter look like Chapter 1 simply because the catalog was narrowed for debugging. Without an explicit rule, reviewers often misdiagnose the problem as a heading bug instead of a catalog-range effect.
Custom headers and footers can be injected into review-style.sty using fancyhdr:
\fancyhead{}
\fancyhead[LE]{\gtfamily\sffamily\bfseries\upshape \leftmark}
\fancyhead[RO]{\gtfamily\sffamily\bfseries\upshape \rightmark}
\fancypagestyle{plain}{%
\fancyhead{}
\fancyfoot{}
\fancyfoot[LE,RO]{\thepage}
\renewcommand{\headrulewidth}{0pt}
\renewcommand{\footrulewidth}{0pt}
}URL Line Breaking
Problem
Long URLs (especially reference links) overflow the page width in PDF output. The default review-jsbook.cls defines \UrlBreaks but does not cover all characters, and lacks aggressive line-break permissions.
Solution
Two changes are required — LaTeX side and Ruby side:
1. LaTeX: Add xurl + emergencystretch
Add xurl (a TeX Live standard package that extends url with permissive break points) and \emergencystretch to the custom sty:
\usepackage{xurl}
\emergencystretch=3em2. Ruby: Override inline_href in review-ext.rb
Re:VIEW converts @<href>{url, text} to \href{url}{\texttt{text}}. xurl only affects \url{} commands, so it does not break URLs inside \href{\texttt{}}.
Override inline_href in review-ext.rb to emit \url{} when the display text equals the URL:
def inline_href(str)
url, label = str.strip.split(/,\s*/, 2)
if label.nil? || label.strip == url.strip
"\\url{#{escape_url(url)}}"
else
"\\href{#{escape_url(url)}}{#{escape(label.strip)}}"
end
endWarning: Adding xurl alone will appear to fix the build (no compile errors),but the PDF will still have overflowing URLs. Both changes are required.
Alternative: Converter-side label stripping
If you control the Markdown-to-Re:VIEW converter, a cleaner alternative to the review-ext.rb override is to strip the label when it equals the URL, so Re:VIEW emits a bare @<href>{url}:
- Re:VIEW 5.x
compile_href:@<href>{url, label}->\href{url}{label}(not breakable),
while bare @<href>{url} -> \url{url} (breakable once xurl is loaded).
- In the converter, when a Markdown
[label](url)haslabel == url, emit@<href>{url}
(drop the label). Keep @<href>{url, label} for human-readable labels (e.g. table cells).
- With this approach, only the LaTeX-side
xurlis needed; the Rubyinline_hrefoverride
becomes optional.
Gotcha: \reviewhref does not exist in Re:VIEW 5.9.0. Do not try to\renewcommand{\reviewhref} in the custom sty — the build fails with an undefinedcontrol sequence. Fix link breaking viaxurl+ bare\url{}(converter orreview-ext.rb),
not by redefining a Re:VIEW href macro.
Where to Add
Place these lines in review-custom.sty or the build script's custom sty injection point. Do not edit sty/review-jsbook.cls directly — the build script overwrites sty/ from the gem defaults on every run.
Notes
xurlis included in TeX Live and does not require separate installation in the Docker image\emergencystretch=3emgives LaTeX extra flexibility when it cannot find good break pointsxurlalone is not enough — theinline_hrefoverride is essential because Re:VIEW uses\href{\texttt{}}not\url{}
Debugging
- Add
--debugtoreview-pdfmakerto keep the build directory for inspection - Check
sty/review-style.styinside the build directory to verify injected settings - Use
grep -rn 'tocdepth' sty/ *.texto trace where values are set - Use
pdftotext output.pdf -to verify TOC content without opening a viewer
Font Size Tuning
Changing font size requires adjusting multiple parameters together. Add all values to texdocumentclass in config.yml:
| Setting | 10pt (default) | 9pt (compact) |
|---|---|---|
fontsize | 10pt | 9pt |
baselineskip | 15.4pt | 13.5pt |
line_length | 40zw | 43zw |
number_of_lines | 35 | 38 |
| Chars/page (est.) | ~585 | ~740 |
Example (9pt):
texdocumentclass:
[
"review-jsbook",
"media=ebook,paper=b5,serial_pagination=true,openright,fontsize=9pt,baselineskip=13.5pt,line_length=43zw,number_of_lines=38,head_space=30mm,headsep=10mm,headheight=5mm,footskip=10mm,jafont=noto-otf",
]Chapter Opening on Right Page
Use openright instead of openany in texdocumentclass options. This ensures every # (chapter) starts on an odd (right-side) page. If the previous chapter ends on an odd page, a blank even page is inserted automatically.
Chapter Title Page (Full Page)
Override \@makechapterhead in review-custom.sty to make the chapter title occupy a full page with centered layout and decorative rules:
\makeatletter
\renewcommand{\@makechapterhead}[1]{%
\vspace*{3cm}%
\begin{center}%
{\Large\headfont \@chapapp\thechapter\@chappos}%
\par\vskip 12pt%
{\rule{0.6\textwidth}{0.5pt}}%
\par\vskip 16pt%
{\Huge\headfont #1}%
\par\vskip 12pt%
{\rule{0.6\textwidth}{0.5pt}}%
\end{center}%
\clearpage%
}
\makeatotherThe \clearpage at the end forces the first ## (section) to start on the next page.
Pitfall: Do not use \renewcommand{\reviewchapterhead} — this macro does not existin Re:VIEW 5.x. Use \@makechapterhead (standard jsbook) instead.Also avoid\\(line break) in\@makechapterhead; use\par\vskipfor spacing.
Code Block Auto-Wrapping with review-ext.rb
Long code lines that exceed the page width can be automatically wrapped at build time using a review-ext.rb extension with the unicode-display_width gem.
Setup
1. Place review-ext.rb in the Re:VIEW project root (same directory as config.yml) 2. Install the gem at build time:
gem install unicode-display_width --no-document3. Adjust WRAP_WIDTH constants for your font size and line length:
| Font Size | line_length | Recommended WRAP_WIDTH |
|---|---|---|
| 10pt | 40zw | 76 |
| 9pt | 43zw | 82 |
How It Works
The extension overrides code_line and code_line_num in LATEXBuilder. When a line exceeds WRAP_WIDTH display columns, it should split at spaces or delimiters such as /, -, _, ., : when possible, and only fall back to character-level wrapping when no safe break point exists.
Do not inject visible continuation markers such as ↵ into the PDF output. They tend to look like mojibake or converter garbage in review screenshots.
See templates/review-ext.rb for a ready-to-use template.
Practical Rules
- Prefer delimiter-aware wrapping over raw character-count splitting
- Do not add visible wrap markers to wrapped code lines
- Keep a last-resort hard wrap for single long tokens that exceed line width
Code Fence Captions
Problem
If a Markdown code fence has a language but no explicit caption, some converters accidentally promote the language name itself (text, json, markdown) into a visible list caption. This looks like converter noise in the final PDF.
Recommended Rule
- Treat the first fence token as the syntax/language only
- Only emit a visible Re:VIEW list caption when the fence info contains an explicit caption
- When the caption is omitted, still emit the required Re:VIEW block arguments, but keep the caption empty
Example:
```markdown ``json API response example { "ok": true }
should produce a visible caption, while:
````markdown
{ "ok": true }````
should not show json as the caption.
Reference URLs Inside Bullet Lists
Problem
In Markdown manuscripts, authors often write references as a bullet title followed by a URL on the next indented line. During Markdown -> Re:VIEW -> LaTeX conversion, that newline may collapse back into a space inside list items, causing long URLs to run across the page and get clipped.
Recommended Rule
- Write manuscript references in two lines inside the same list item
- Put the title on the first line
- Put the URL itself as a link on the next indented line
- In the converter, emit an explicit line break before link-only continuation lines
Example manuscript form:
- GitHub Docs: GitHub Copilot policies
[https://docs.github.com/en/copilot/concepts/policies](https://docs.github.com/en/copilot/concepts/policies)This keeps the title readable and prevents long URLs from relying on implicit line wrapping alone.
Footnote ID Collisions Across Combined Markdown Files
Problem
Some book workflows concatenate multiple Markdown section files into one chapter-level .re file. If each source file uses local footnote labels such as [^1], [^2], the same labels can repeat across files. If the converter normalizes those labels directly into a shared namespace, later definitions may overwrite earlier ones or references may point to the wrong footnote body.
Another trap is treating any [^id]: substring as a footnote definition. Inline prose such as See note[^4]: or list items ending with [^1]: still contain a reference followed by punctuation, not a footnote definition.
Recommended Rule
- Namespace Markdown footnote labels per source file before converting them to final Re:VIEW footnote IDs
- Detect footnote definitions only at the start of a line
- Keep reference replacement active even when a colon immediately follows the closing bracket in prose or list items
Example Strategy
If both section-a.md and section-b.md contain [^1], rename them to source-scoped labels such as:
section_a_1section_b_1
Do this before any final ID sanitization for Re:VIEW output.
Verification
- Inspect a representative
.resnippet and confirm that each@<fn>{...}points to the intended//footnote[...] - Rebuild the final PDF after converter changes; checking only
.reoutput is not enough when references can silently resolve to the wrong note body
Table Notes and Footnotes
Problem
In a Markdown -> Re:VIEW -> LaTeX -> PDF workflow, a note may semantically belong to a whole table, but footnote markers placed inside table cells or even in the table header do not reliably become page-bottom footnotes in the final PDF. Depending on the converter and LaTeX table environment, the marker may disappear, remain inline, or fail to render as a normal bottom note.
Recommended Rule
- If a note applies to the whole table, do not place the footnote marker inside a table cell
- Do not rely on table-header footnote markers for bottom-of-page notes
- Add one short prose sentence immediately before or after the table, and attach the footnote marker there
- Write the footnote body so it explicitly says the note applies to the table
Example Strategy
Prefer this:
#### Representative built-in classifiers
Note that, in the table below, Agreements, Resume, and Financial Statement are English-only as of May 2026[^1].
| Classifier | Description |
| ---------- | ----------- |
| Threat | ... |
| Agreements | ... |
[^1]: In the table below, Agreements, Resume, and Financial Statement are English-only as of May 2026.Avoid this:
| Classifier[^1] | Description |
| -------------- | ----------- |
| Agreements | ... |Why This Works
- The note still reads as table-specific to the human reader
- The footnote marker stays in normal prose, which Re:VIEW and LaTeX handle more reliably
- The final PDF is less likely to lose the note or render it in an unexpected place
Odd/Even Running Headers and Side Markers
Problem
When designing chapter/section markers for the top margin or the left/right side margins, it is easy to focus on fancyhdr slot configuration (LE, RO, etc.) and miss that the LaTeX document class is still running in oneside mode. In that case, odd/even-specific placements do not behave as expected, and layout experiments can appear to "do nothing".
Another trap is trying to derive chapter, section, and subsection labels from only one mark stream. If multiple hierarchy levels need to be displayed at the same time, overloading a single mark variable tends to produce blanks or stale labels.
Recommended Rule
- Use
twosideintexdocumentclasswhenever odd/even page layout should differ - Treat chapter and lower-level markers as separate responsibilities
- Prefer
leftmarkfor stable chapter-level display - Use
rightmarkfor the currently active lower-level heading when appropriate - If the design needs multiple hierarchy levels at once, keep separate stored values instead of trying to infer everything from one mark
Practical Notes
onesidecan make odd/evenfancyhdrsettings appear broken even when the style file is correct- Re:VIEW/jsbook-based stacks often behave more predictably when chapter-level display uses
leftmark - Verify the actual PDF result, not just the style file text
Fastest Way to Isolate Header Problems
If an odd/even running header still does not show the expected chapter or section title, first verify the slot itself before changing mark logic again.
- Temporarily replace the intended header content with a fixed literal such as
CHAPTER-TEST - Rebuild the PDF and check whether that literal appears in the target slot
- If the literal appears, the page-style slot is working and the remaining bug is in mark propagation or title capture
- If the literal does not appear, the issue is page style selection,
twosideconfiguration, or viewer caching, not the mark text itself
This avoids repeated changes to \chaptermark, \sectionmark, or custom state variables when the real problem is elsewhere.
Margin Labels: Prefer fancyhdr Before Page Overlay Packages
For vertical chapter/section labels placed in the side margins, prefer a fancyhdr-based approach first.
- Use
rotateboxplus horizontal offset inside the appropriateLE/ROheader slots - Treat page-overlay packages such as
eso-picas a fallback, not the first choice
In Re:VIEW/jsbook-based stacks, eso-pic can conflict with other page-layout helpers such as pxesopic / gentombow, causing LaTeX build failures even when the layout idea itself is valid.
If a margin-label experiment unexpectedly breaks the build, remove the overlay package first and return to a plain fancyhdr implementation before debugging other parts of the style.
PDF Viewer Cache During Layout Verification
Problem
Running-header, footnote, or spacing changes are often verified by repeatedly rebuilding the same PDF output filename. Some PDF viewers continue to show a cached version of the file even after the build succeeded, which makes it look as though the latest style change had no effect.
Recommended Rule
- Do not trust a same-name PDF tab alone when verifying layout changes
- Check the updated file timestamp after each build
- If the workflow archives previous builds into timestamped directories, compare against the newly archived copy as well
- When a result seems unchanged, verify the generated
.reor.texsnippet before assuming the style edit failed
Verification
- Confirm the output PDF modification time changed
- Open the latest archived/timestamped PDF when available
- Compare one representative
.resnippet to the PDF page before continuing with further style changes
Explicit Blank Lines Between Paragraphs
Problem
In a Markdown -> Re:VIEW -> LaTeX -> PDF workflow, plain blank lines usually only mark paragraph boundaries. They do not reliably create a visibly larger vertical gap in the final PDF. Likewise, forcing line breaks with @<br>{} may end up as paragraph-internal \\ and still fail to produce the intended blank line.
Recommended Rule
- If you need a visibly larger gap in the PDF, do not rely on Markdown blank lines alone
- Use a hidden manuscript marker such as
<!-- review:br --> - Convert that marker in the Markdown-to-Re:VIEW script into a raw LaTeX vertical-space command
Recommended conversion target:
//raw[|latex|\\par\\vspace{1em}]This keeps the Markdown source readable while producing an explicit blank gap in PDF output.
Why This Works
- Markdown blank lines: paragraph split only
@<br>{}: may become\\, which is just a line break\par\vspace{1em}: ends the paragraph and inserts actual vertical space
Example
Manuscript source:
本文の締めです。
<!-- review:br -->
<!-- review:br -->
次の視点へ切り替えます。Converter output:
本文の締めです。
//raw[|latex|\\par\\vspace{1em}]
//raw[|latex|\\par\\vspace{1em}]
次の視点へ切り替えます。Practical Notes
- Keep the marker name stable across the workspace so authors can reuse it consistently
- Tune
1emto1.5emor2emonly after checking the actual PDF result - Prefer this technique only when visual separation matters; for normal prose, paragraph breaks are usually enough
Markdown Backslash Escapes in PDF Output
Problem
Markdown allows backslash escapes such as \_, \*, ` \ `, \\ to prevent special characters from being interpreted as formatting. When converting Markdown to Re:VIEW (.re), these escapes may pass through literally, producing visible backslashes in the PDF output (e.g. yuyanz\_ instead of yuyanz_`).
Solution
Add an unescape step in the Markdown-to-Re:VIEW conversion script's inline processing, after protecting code spans but before applying bold/italic transformations:
# Unescape Markdown backslash escapes (e.g. \_ -> _)
text = re.sub(r"\\([_*`\\])", r"\1", text)This must run before bold/italic regex matching so that \_ is reduced to _ before the *...* patterns are evaluated.
Affected Elements
- Headings (
## Yuya(yuyanz\_)→ shows backslash in section title) - Body text (e.g.
file\_name→ shows backslash in prose) - Any inline context processed by
replace_inline()
Markdown Image Caption vs Layout Metadata
Problem
When Markdown images are converted to Re:VIEW, the image alt text and optional title often get mixed together. If authors put manual figure numbering such as Image: ... or 図 01 into alt text, the final PDF can end up with duplicated numbering because Re:VIEW already manages figure numbering.
Another common issue is using the title field as if it were a second caption. For example,  should treat scale=0.80 as layout metadata, not as visible caption text.
Recommended Rule
- Use alt text for the visible caption only
- Do not put manual numbering such as
図 01,Figure 1, orImage:into alt text - Use the Markdown image title only for layout metadata such as
scale=0.80 - If no caption is provided, let the converter fall back to the file stem or another deterministic rule
Markdown Tables Need //tsize for PDF Wrapping
Problem
When Markdown pipe tables are converted to plain Re:VIEW //table{} blocks without a matching //tsize, LaTeX PDF output often falls back to fixed-width-free columns such as |l|l|l|. In that mode, long Japanese prose inside cells does not wrap correctly and can run past the page edge.
This tends to surface in tables where the last column contains explanatory sentences, for example:
- tool comparison tables
- role / usage / notes matrices
- long scenario descriptions in 3-column tables
Solution
Emit a LaTeX-specific //tsize immediately before the generated //table{} block:
//tsize[|latex||L{27mm}|L{36mm}|L{63mm}|]
//table{
項目 説明 利用シーン
------------------------------------------------------------
...
//}Practical Rule
- Use
L{...mm}columns for any table column that may need wrapping - Reserve the widest column for prose-heavy cells, usually the last column
- Generate widths automatically in the Markdown-to-Re:VIEW converter when possible
- Keep the total width aligned to the actual PDF text area used by the project
Why This Matters
Even if the manuscript source looks fine in Markdown, the PDF builder only wraps table cells when the LaTeX column spec supports wrapping. If a project relies on auto-generated Re:VIEW, table layout must be handled in the converter, not left to authors to patch by hand after every build.
Recommended Pattern
This should become a Re:VIEW image block where:
- caption =
Agent モードの選択 - metric =
scale=0.80
Anti-Patterns


These patterns make later conversion and numbering brittle.
Chapter Illustration on Blank Pages
When using openright, a blank even page is inserted before each chapter that starts on an odd page. This blank page can be used to display an illustration by overriding \cleardoublepage in review-custom.sty.
Implementation
\makeatletter
% Track mainmatter state — illustration only in mainmatter
\newif\ifreview@inmainmatter
\review@inmainmatterfalse
\g@addto@macro\reviewmainmatterhook{%
\review@inmainmattertrue
}
\let\review@origcleardoublepage\cleardoublepage
\renewcommand{\cleardoublepage}{%
\clearpage
\if@twoside
\ifodd\c@page\else
\thispagestyle{empty}%
\ifreview@inmainmatter
\edef\review@nextch{\the\numexpr\value{chapter}+1\relax}%
\ifnum\review@nextch>0
\ifnum\review@nextch<8 % adjust upper bound to your chapter count
\null\vfill
\begin{center}%
\includegraphics[width=0.65\textwidth,height=0.65\textheight,keepaspectratio]{images/chapter-illustrations/ch\review@nextch}%
\end{center}%
\vfill
\fi
\fi
\fi
\newpage
\if@twocolumn\hbox{}\newpage\fi
\fi
\fi
}
\makeatotherFile Naming
Place images as images/chapter-illustrations/ch1.png, ch2.png, etc. Do not zero-pad (ch01.png) because \thechapter outputs 1, not 01.
Build Prerequisites
Run extractbb on all illustration PNGs before review-pdfmaker:
cd /work && for f in images/chapter-illustrations/*.png; do extractbb "$f"; doneThis generates .xbb bounding box files that dvipdfmx needs.
Image Handling Pitfalls (dvipdfmx)
\IfFileExists Does Not Find Image Files
In dvipdfmx environments, \IfFileExists{images/foo.png} always returns false because .png is not in the kpsewhich search path. Use \ifnum conditions on chapter numbers or \openin file-read tests instead.
extractbb Paranoid Mode in Docker
TeX Live's default openout_any = p (paranoid) blocks writes to absolute paths. extractbb /work/images/foo.png fails with openout_any = p error. Always use relative paths: cd /work && extractbb images/foo.png.
\cleardoublepage Fires in Frontmatter
The \cleardoublepage hook runs for all page transitions, including title page → TOC. Without a mainmatter guard, images meant for ch1 can appear on the blank page after the title page (where chapter counter = 0, so nextch = 1). Always gate illustration insertion with a \ifreview@inmainmatter flag set in \reviewmainmatterhook.
Footnote Placement in Generated .re Files
Problem
When a Markdown-to-Re:VIEW converter concatenates multiple Markdown files into one chapter .re file, footnote definitions (//footnote[id][text]) are often appended at the end of the chapter. Re:VIEW's LaTeX builder outputs \footnotetext[N]{...} at the position where //footnote appears in the .re file. If all definitions are at the chapter end, every footnote in the chapter clusters on the last few pages of the PDF instead of appearing at the bottom of the page where it is referenced.
Additional Trap: Nested Blocks
Moving //footnote to right after the reference line is not always sufficient. If the reference is inside a block such as //table{...//} that is itself nested inside a column block (===[column]...===[/column]), placing the footnote after the inner //} still leaves it inside the column. Re:VIEW renders columns with tcolorbox, and \footnotetext inside tcolorbox triggers a Counter too large LaTeX error that aborts the build entirely.
Recommended Rule
1. After generating all .re content, redistribute //footnote definitions from the chapter end to immediately after their reference locations 2. If the reference is inside any block, place the footnote after the outermost enclosing block (where depth returns to 0), not just the innermost //} 3. Track block depth for both brace-delimited blocks (//xxx{...//}) and column blocks (===[column]...===[/column]) 4. Footnotes with no matching reference (unused) stay at chapter end
Verification
- Check that
//footnotelines appear near their@<fn>{}references in the.refile, not all at the end - Confirm no
//footnoteis inside a//table{...//}or===[column]...===[/column]block - Build the full PDF and check that footnotes appear at the bottom of the referencing page, not clustered at chapter end
- Watch for
Counter too largeerrors — they indicate a footnote is still inside a tcolorbox-rendered block
Template Threshold for Skipping Files
When the converter skips files below a character-count threshold to exclude templates, keep the threshold low enough that short but real content (e.g., a heading + one paragraph + an image reference) is not silently dropped from the PDF. A threshold around 200 characters for normal section files is a practical balance; 300 characters can silently skip valid content.
Setup Workflow
Use the setup script to create a new manuscript workspace from the bundled templates.
Command
python scripts/setup_workspace.py `
--name "project-name" `
--title "Book Title" `
--path "D:\target\path" `
--chapters 8
# Add Re:VIEW/PDF support only when the project needs it.
python scripts/setup_workspace.py `
--name "project-name" `
--title "Book Title" `
--path "D:\target\path" `
--chapters 8 `
--with-reviewMain Options
| Option | Meaning |
|---|---|
--name | Folder name for the new workspace |
--title | Human-readable book title |
--path | Parent directory where the workspace will be created |
--chapters | Number of generated chapter folders |
--chapter-titles | Explicit chapter names instead of defaults |
--with-review | Add optional Re:VIEW/PDF scaffolding |
--with-materials | Add materials/references/ |
--no-materials | Skip materials/ |
What Gets Generated
1. Folder structure for outlines, drafts, images, docs, and optional output 2. Writing/review agents and writing instructions under .github/ 3. Project docs such as README.md, docs/page-allocation.md, and docs/schedule.md 4. Helper scripts such as scripts/count_chars.py 5. Initial chapter intro files in both outline and manuscript folders 6. Optional Re:VIEW/PDF scripts and metadata when --with-review is used
Post-Setup Checks
1. Open the generated README.md and confirm project metadata 2. Edit docs/page-allocation.md to fit the book length 3. Edit docs/schedule.md to replace placeholder dates 4. Adjust .github/copilot-instructions.md for the target audience and goals
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Book Writing Workspace Setup Script
Creates a complete book writing workspace with:
- Directory structure for manuscripts, images, materials
- AI agent configurations
- Writing instructions and guidelines
- Utility scripts
Usage:
python setup_workspace.py --name "my-book" --title "My Book Title" --path "D:\\projects"
"""
import argparse
import shutil
from pathlib import Path
from typing import List
# Script location (for finding templates)
SCRIPT_DIR = Path(__file__).parent
SKILL_DIR = SCRIPT_DIR.parent
TEMPLATES_DIR = SKILL_DIR / "templates"
ASSETS_DIR = SKILL_DIR / "assets"
def create_directory_structure(
base_path: Path,
chapters: List[str],
include_review: bool = False,
include_materials: bool = True
) -> None:
"""Create the complete directory structure."""
# Main content directories
dirs = [
".github/agents",
".github/instructions/writing",
"docs",
"docs/templates",
"scripts",
]
# Chapter-based directories
for i, chapter in enumerate(chapters):
chapter_num = str(i).zfill(2)
chapter_slug = chapter.lower().replace(' ', '-')
dirs.append(f"keypoints/{chapter_num}-{chapter_slug}")
dirs.append(f"sections/{chapter_num}-{chapter_slug}")
dirs.append(f"images/{chapter_num}-{chapter_slug}")
# Optional: Re:VIEW output
if include_review:
dirs.extend([
"config/review-metadata",
"re-view-output/images",
"re-view-output/sty",
])
# Optional: Materials
if include_materials:
dirs.extend([
"materials/references",
])
# Create all directories
for dir_path in dirs:
(base_path / dir_path).mkdir(parents=True, exist_ok=True)
print(f" ✅ Created: {dir_path}")
def copy_template_files(base_path: Path, book_title: str, include_review: bool = False) -> None:
"""Copy and customize template files."""
# Copy from references directory
templates = {
"agents/writing.agent.md": ".github/agents/writing.agent.md",
"agents/writing-reviewer.agent.md": ".github/agents/writing-reviewer.agent.md",
"instructions/writing.instructions.md": ".github/instructions/writing/writing.instructions.md",
"instructions/writing-heading.instructions.md": ".github/instructions/writing/writing-heading.instructions.md",
"instructions/writing-notation.instructions.md": ".github/instructions/writing/writing-notation.instructions.md",
"docs/writing-guide.md": "docs/writing-guide.md",
"docs/naming-conventions.md": "docs/naming-conventions.md",
"docs/page-allocation.md": "docs/page-allocation.md",
"docs/schedule.md": "docs/schedule.md",
"docs/templates/template-chapter-intro.md": "docs/templates/template-chapter-intro.md",
"docs/templates/template-section.md": "docs/templates/template-section.md",
"copilot-instructions.md": ".github/copilot-instructions.md",
"AGENTS.md": "AGENTS.md",
"README.md": "README.md",
}
for src, dst in templates.items():
src_path = TEMPLATES_DIR / src
dst_path = base_path / dst
if src_path.exists():
dst_path.parent.mkdir(parents=True, exist_ok=True)
# Read, customize, and write
content = src_path.read_text(encoding="utf-8")
content = content.replace("{{BOOK_TITLE}}", book_title)
content = content.replace("{{PROJECT_PATH}}", str(base_path))
dst_path.write_text(content, encoding="utf-8")
print(f" ✅ Created: {dst}")
else:
print(f" ⚠️ Template not found: {src}")
review_templates = {}
if include_review:
review_templates = {
"agents/converter.agent.md": ".github/agents/converter.agent.md",
"custom-titlepage.tex": "re-view-output/custom-titlepage.tex",
"review-ext.rb": "re-view-output/review-ext.rb",
"sty/review-custom.sty": "re-view-output/sty/review-custom.sty",
"sty/review-style.sty": "re-view-output/sty/review-style.sty",
"review-metadata/common.yml": "config/review-metadata/common.yml",
"review-metadata/project.yml": "config/review-metadata/project.yml",
}
for src, dst in review_templates.items():
src_path = TEMPLATES_DIR / src
dst_path = base_path / dst
if src_path.exists():
dst_path.parent.mkdir(parents=True, exist_ok=True)
content = src_path.read_text(encoding="utf-8")
content = content.replace("{{BOOK_TITLE}}", book_title)
dst_path.write_text(content, encoding="utf-8")
print(f" ✅ Created: {dst}")
else:
print(f" ⚠️ Template not found: {src}")
def copy_scripts(base_path: Path, include_review: bool = False) -> None:
"""Copy utility scripts."""
scripts = [
"count_chars.py",
]
if include_review:
scripts.extend([
"convert_md_to_review.py",
"build_review_pdf.py",
"inspect_pdf.py",
"review_metadata.py",
])
for script in scripts:
src_path = TEMPLATES_DIR / "scripts" / script
dst_path = base_path / "scripts" / script
if src_path.exists():
shutil.copy2(src_path, dst_path)
print(f" ✅ Copied: scripts/{script}")
else:
print(f" ⚠️ Script not found: {script}")
def copy_assets(base_path: Path) -> None:
"""Copy static asset files."""
assets = [
(".gitignore", ".gitignore"),
]
for src, dst in assets:
src_path = ASSETS_DIR / src
dst_path = base_path / dst
if src_path.exists():
shutil.copy2(src_path, dst_path)
print(f" ✅ Copied: {dst}")
else:
print(f" ⚠️ Asset not found: {src}")
def create_chapter_intro_files(
base_path: Path,
chapters: List[str],
) -> None:
"""Create initial chapter introduction files."""
for i, chapter in enumerate(chapters):
chapter_num = str(i).zfill(2)
chapter_slug = chapter.lower().replace(' ', '-')
chapter_folder = f"{chapter_num}-{chapter_slug}"
# Create intro file in keypoints
keypoints_file = base_path / f"keypoints/{chapter_folder}/{chapter_folder}.md"
keypoints_content = f"""# {chapter}
## Overview
(Write the overview of this chapter here)
## Key Points
- Point 1
- Point 2
- Point 3
## Sections
- a. Section A Title
- b. Section B Title
"""
keypoints_file.write_text(keypoints_content, encoding="utf-8")
# Create intro file in contents
contents_file = base_path / f"sections/{chapter_folder}/{chapter_folder}.md"
contents_content = f"""# {chapter}
(Write the introduction paragraph here)
"""
contents_file.write_text(contents_content, encoding="utf-8")
print(f" ✅ Created chapter intro files for {len(chapters)} chapters")
def main():
parser = argparse.ArgumentParser(
description="Set up a book writing workspace"
)
parser.add_argument(
"--name",
required=True,
help="Project folder name (e.g., 'my-book-project')"
)
parser.add_argument(
"--title",
required=True,
help="Book title (e.g., 'Introduction to Cloud Security')"
)
parser.add_argument(
"--path",
required=True,
help="Parent directory path (e.g., 'D:\\projects')"
)
parser.add_argument(
"--chapters",
type=int,
default=8,
help="Number of chapters (default: 8)"
)
parser.add_argument(
"--chapter-titles",
nargs="*",
help="Custom chapter titles (space-separated)"
)
parser.add_argument(
"--with-review",
"--include-review",
dest="include_review",
action="store_true",
help="Include optional Re:VIEW/PDF scaffolding"
)
parser.add_argument(
"--no-review",
dest="include_review",
action="store_false",
help="Exclude Re:VIEW/PDF scaffolding"
)
parser.set_defaults(include_review=False)
parser.add_argument(
"--with-materials",
"--include-materials",
dest="include_materials",
action="store_true",
help="Include a reference materials directory"
)
parser.add_argument(
"--no-materials",
dest="include_materials",
action="store_false",
help="Exclude reference materials directory"
)
parser.set_defaults(include_materials=True)
args = parser.parse_args()
# Determine chapter titles
if args.chapter_titles:
chapters = args.chapter_titles
else:
# Default chapter structure
chapters = [
"Introduction",
]
for i in range(1, args.chapters - 1):
chapters.append(f"Chapter {i}")
chapters.append("Conclusion")
# Resolve options
include_review = args.include_review
include_materials = args.include_materials
# Create base path
base_path = Path(args.path) / args.name
print(f"\n📚 Setting up Book Writing Workspace")
print(f" Project: {args.name}")
print(f" Title: {args.title}")
print(f" Location: {base_path}")
print(f" Chapters: {len(chapters)}")
print(f" Re:VIEW: {'Yes' if include_review else 'No'}")
print(f" Materials: {'Yes' if include_materials else 'No'}")
print()
# Check if directory exists
if base_path.exists():
print(f"❌ Error: Directory already exists: {base_path}")
return 1
# Create workspace
print("📁 Creating directory structure...")
create_directory_structure(base_path, chapters, include_review, include_materials)
print("\n📄 Creating template files...")
copy_template_files(base_path, args.title, include_review)
print("\n📜 Copying utility scripts...")
copy_scripts(base_path, include_review)
print("\n📦 Copying asset files...")
copy_assets(base_path)
print("\n✏️ Creating chapter intro files...")
create_chapter_intro_files(base_path, chapters)
print(f"\n✅ Workspace created successfully at: {base_path}")
print("\n📋 Next steps:")
print(f" 1. cd \"{base_path}\"")
print(f" 2. code \"{base_path}\"")
print(" 3. Edit docs/page-allocation.md to set word count targets")
print(" 4. Start writing in keypoints/")
print(" 5. Follow your repository's existing Git workflow when saving changes")
return 0
if __name__ == "__main__":
exit(main())
Installed Skills
The following agents are available in this workspace.
| Agent | Role |
|---|---|
| @writing | Manuscript writing and editing |
| @writing-reviewer | Quality review (P1/P2/P3) |
If Re:VIEW/PDF support is enabled, add a converter agent for Markdown to Re:VIEW conversion.
Folder Permissions
| Folder | Writing | Reviewer | Converter |
|---|---|---|---|
keypoints/ | Read | Read | Read |
sections/ | Edit | Read | Read |
re-view-output/ | - | - | Optional |
images/ | Read | Read | Read |
Workflow
Key Points -> Draft -> Review -> Fix -> Optional Convert/PDF
↓ ↓ ↓ ↓ ↓
keypoints sections Loop until re-view-output
P1=0, P2=0 when enabledData Verification
Before completing a chapter:
1. Run python scripts/count_chars.py to check word counts 2. Verify against docs/page-allocation.md targets 3. Ensure folder structure matches keypoints/
Converter Agent
Convert Markdown manuscripts to Re:VIEW format for PDF generation.
Role
Convert files in sections/ to Re:VIEW format in re-view-output/.
Goals
- Convert Markdown to Re:VIEW syntax accurately
- Maintain formatting and structure
- Generate PDF-ready output
Permissions
- Allowed: Read
sections/, editre-view-output/, run terminal commands - Forbidden: Edit
sections/,git push
Workflow
1. Read Markdown files from sections/ 2. Convert to Re:VIEW format using scripts/convert_md_to_review.py 3. Output to re-view-output/ 4. Build PDF with Docker (if available)
Re:VIEW Conversion Rules
Headings
| Markdown | Re:VIEW |
|---|---|
# H1 | = H1 |
## H2 | == H2 |
### H3 | === H3 |
Inline Formatting
| Markdown | Re:VIEW |
|---|---|
**bold** | @<b>{bold} |
*italic* | @<i>{italic} |
` code ` | @<code>{code} |
[text](url) | @<href>{url, text} |
Lists
- Item 1
- Item 2 * Item 1
* Item 2Code Blocks
````markdown
print("hello")````
//listnum[code1][python]{
print("hello")
//}PDF Build Command
docker run --rm -v "${PWD}:/work" vvakame/review rake pdfOutput Structure
re-view-output/
├── *.re
├── images/
├── catalog.yml
└── config.ymlError Handling
| Issue | Solution |
|---|---|
| Conversion error | Check Markdown syntax, fix source |
| Build failure | Check Docker, review logs |
| Image not found | Verify image paths in images/ |
Writing Reviewer Agent
Review manuscripts and provide structured feedback.
Role
Review Markdown files in sections/ and provide prioritized improvement suggestions.
Goals
- Identify issues using P1/P2/P3 priority system
- Ensure consistency with writing guidelines
- Improve readability and technical accuracy
Permissions
- Allowed: Read all files
- Forbidden: Edit files,
gitcommands
Priority System
| Priority | Level | Description | Action Required |
|---|---|---|---|
| P1 | Critical | Must fix before publishing | Immediate fix |
| P2 | Important | Should fix for quality | Fix before final |
| P3 | Suggestion | Nice to have improvement | Optional |
P1 Examples
- Incorrect technical information
- Heading level errors (using
#in section files) - Folder structure mismatch
- Word count out of range
- Missing required sections
P2 Examples
- Inconsistent terminology
- Unclear explanations
- Missing sources/references
- Style guide violations
P3 Examples
- Minor wording improvements
- Alternative expressions
- Additional examples
Output Format
## Review Result: {filename}
### Summary
- P1: {count} issues
- P2: {count} issues
- P3: {count} suggestions
### P1 (Critical)
#### P1-1: {issue title}
- **Location**: Line XX
- **Issue**: {description}
- **Fix**: {suggested fix}
### P2 (Important)
#### P2-1: {issue title}
- **Location**: Line XX
- **Issue**: {description}
- **Suggestion**: {improvement}
### P3 (Suggestions)
- {suggestion 1}
- {suggestion 2}Checklist
Structure
- [ ] Folder hierarchy matches
keypoints/ - [ ] Heading levels are correct (no
#in section files) - [ ] File naming follows conventions
Content
- [ ] Technical accuracy verified
- [ ] Terms explained on first use
- [ ] Sources cited where needed
Style
- [ ] Uses polite/desu-masu style
- [ ] Sentences under 500 characters
- [ ] Natural Japanese (not translation-style)
Word Count
- [ ] Within target range per file type
- [ ] Chapter total within bounds
Reference Files
| File | Content |
|---|---|
.github/instructions/writing/writing.instructions.md | Style guide |
docs/page-allocation.md | Word count targets |
docs/naming-conventions.md | File naming |
Writing Agent
Write and edit manuscripts for the book project.
Role
Edit Markdown files in sections/ to create high-quality manuscripts.
Goals
- Write manuscripts based on key points in
keypoints/ - Balance technical accuracy with readability
- Achieve P1=0, P2=0 after review
Permissions
- Allowed: Read files, edit
sections/, request review, run terminal commands - Forbidden:
git push, delete files, editkeypoints/
I/O Contract
| Item | Description |
|---|---|
| Input | File path or chapter/section specification |
| Output | Edited Markdown file, review response status |
Instructions
Follow the writing style guidelines in .github/instructions/writing/writing.instructions.md.
Key Rules
1. Use polite/desu-masu style (です・ます調) 2. Keep sentences under 500 characters 3. Explain technical terms on first use 4. Include sources and references
Done Criteria
- [ ] Target file editing complete
- [ ] Folder structure matches
keypoints/ - [ ] Word count within range per
docs/page-allocation.md - [ ] P1 (critical) issues = 0
- [ ] P2 (important) issues = 0
Workflow
1. Check structure in keypoints/ 2. Determine file type and target word count 3. Write manuscript in sections/ 4. Verify word count with python scripts/count_chars.py 5. Report completion to Orchestrator
Error Handling
| Situation | Response |
|---|---|
| Edit failed | Check path and permissions, retry |
| P1 issues ≥ 3 | Fix one at a time, re-review after each |
| Major restructure needed | Request human approval |
Reference Files
| File | Content |
|---|---|
.github/instructions/writing/writing.instructions.md | Style guide |
.github/instructions/writing/writing-heading.instructions.md | Heading rules |
.github/instructions/writing/writing-notation.instructions.md | Notation rules |
docs/naming-conventions.md | File naming |
docs/page-allocation.md | Word count targets |
{{BOOK_TITLE}} - Copilot Guide
Overview
This is the book writing workspace for "{{BOOK_TITLE}}".
Target Readers
- (Define your target audience here)
Goals
- (Define what readers will learn)
Folder Structure
| Folder | Purpose |
|---|---|
keypoints/ | Outlines and key points |
sections/ | Final manuscripts |
re-view-output/ | Re:VIEW source and PDF |
images/ | Image assets |
materials/ | Reference materials |
docs/ | Project documentation |
scripts/ | Utility scripts |
Constraints
| Rule | Reference |
|---|---|
| Writing style | instructions/writing/writing.instructions.md |
| Heading levels | instructions/writing/writing-heading.instructions.md |
| Notation | instructions/writing/writing-notation.instructions.md |
| File naming | ../docs/naming-conventions.md |
| Agents | ../AGENTS.md |
Version Control
- Follow this repository's existing Git workflow.
- Do not push unless explicitly instructed.
- Verify the current directory before running commands.
\begin{titlepage}
\thispagestyle{empty}
\begin{center}
\mbox{} \vskip5zw
\reviewtitlefont
{\Huge\review@booktitlename\par}
\ifdefined\review@subtitlename
\vskip 1em
{\Large\review@subtitlename\par}
\fi
\vskip 12em
\ifx\review@titlepageauthors\@empty
\else
{\huge
\lineskip .75em
\begin{tabular}[t]{p{\textwidth}}
\centering \review@titlepageauthors
\end{tabular}\par}
\fi
\vfill
{\large\review@date\par}
\ifdefined\review@pbl
\vskip 1.2em
{\large\review@pbl\par}
\fi
\vskip4zw\mbox{}
\end{center}
\end{titlepage}
\clearpageFile Naming Conventions
Naming rules for files and folders in the book project.
Manuscript Files
Pattern
ch{chapter}-{number}_{title}.mdComponents
| Component | Description | Example |
|---|---|---|
{chapter} | Chapter number (0-9) | 0, 1, 5 |
{number} | Sequence (00-99) | 00, 01, 10 |
{title} | Section title (Japanese) | 日常業務を爆速化する |
Examples
| Filename | Usage |
|---|---|
ch0-00_Copilotで何が変わったのか.md | Chapter 0 |
ch1-00_事例ギャラリー.md | Chapter 1 |
ch2-01_Excelタスク自動化.md | Chapter 2, #1 |
ch2-02_メール作成支援.md | Chapter 2, #2 |
File Division Strategy
Basic Principle
1 file = 1 section
File Structure
Each chapter consists of:
ch{N}-00= Chapter introduction (300-500 chars)ch{N}-01= Section 1 (2,000-6,000 chars)ch{N}-02= Section 2 (2,000-6,000 chars)ch{N}-03= Section 3 (2,000-6,000 chars)
Image Files
Pattern
fig-{chapter}-{section}-{number}.{ext}Extensions
| Extension | Usage |
|---|---|
.png | Screenshots, diagrams |
.jpg | Photographs |
.pdf | Vector graphics for print |
Re:VIEW Files
Pattern
ch{NN}-{slug}.reExamples
| Filename | Chapter |
|---|---|
preface.re | Chapter 0 |
ch01-intro.re | Chapter 1 |
postscript.re | Conclusion |
Notes
1. Spaces are OK in folder titles 2. Japanese is OK in visible titles 3. Keep IDs alphanumeric and half-width
Page Allocation and Word Count Targets
Target word counts for each file type and chapter.
File Type Targets
1 file = 1 section
| File Type | Target (chars) | Range | Notes |
|---|---|---|---|
Chapter intro (ch*-00) | 300-500 | 200-700 | Chapter overview |
Section (ch*-01~) | 3,000-5,000 | 2,000-6,000 | Core section content |
| Column/sidebar | 2,000-3,000 | 1,500-3,500 | Supplementary material |
Chapter Targets
Customize this table for your book:
| Chapter | Target (chars) | Files | Notes |
|---|---|---|---|
| 0. Introduction | 3,000 | 1 | Book overview |
| 1. Chapter 1 | 20,000 | 5-7 | Customize |
| 2. Chapter 2 | 20,000 | 5-7 | Customize |
| 3. Chapter 3 | 20,000 | 5-7 | Customize |
| 4. Chapter 4 | 20,000 | 5-7 | Customize |
| 5. Chapter 5 | 20,000 | 5-7 | Customize |
| 6. Chapter 6 | 20,000 | 5-7 | Customize |
| 7. Conclusion | 5,000 | 2-3 | Wrap up |
| Total | ~130,000 |
Tolerance Rules
| Level | Tolerance | Action |
|---|---|---|
| Within range | ±20% | OK |
| Slightly off | ±30% | Review |
| Out of range | >30% | P1 issue, must fix |
Word Count Check
python scripts/count_chars.pyProject Schedule
Timeline for "{{BOOK_TITLE}}".
Milestones
| Phase | Target Date | Status | Notes |
|---|---|---|---|
| Key Points Complete | YYYY-MM-DD | ⏳ | All chapters outlined |
| Draft Complete | YYYY-MM-DD | ⏳ | First draft done |
| Review Complete | YYYY-MM-DD | ⏳ | P1/P2 = 0 |
| Final Edit | YYYY-MM-DD | ⏳ | Polish and proofread |
| PDF Build | YYYY-MM-DD | ⏳ | Re:VIEW output |
| Publication | YYYY-MM-DD | ⏳ | Release |
Chapter Progress
| Chapter | Key Points | Draft | Review | Final |
|---|---|---|---|---|
| 0. Introduction | ⏳ | ⏳ | ⏳ | ⏳ |
| 1. Chapter 1 | ⏳ | ⏳ | ⏳ | ⏳ |
| 2. Chapter 2 | ⏳ | ⏳ | ⏳ | ⏳ |
| 3. Chapter 3 | ⏳ | ⏳ | ⏳ | ⏳ |
| 4. Chapter 4 | ⏳ | ⏳ | ⏳ | ⏳ |
| 5. Chapter 5 | ⏳ | ⏳ | ⏳ | ⏳ |
| 6. Chapter 6 | ⏳ | ⏳ | ⏳ | ⏳ |
| 7. Conclusion | ⏳ | ⏳ | ⏳ | ⏳ |
Weekly Goals
Week of YYYY-MM-DD
- [ ] Complete key points for Chapter 1
- [ ] Start draft for Introduction
- [ ] Gather reference materials
Chapter Title
<!-- Chapter intro template for ch-00_.md --> <!-- Target: 300-500 characters -->
This chapter explains [chapter purpose and scope in 1-2 short paragraphs].
What You Will Learn
- [Learning point 1]
- [Learning point 2]
- [Learning point 3]
[Add a short background paragraph and chapter roadmap.]
---
Heading Rules
#= chapter title (only one per file)##= major heading###= subsection
Notation Rules
- Numbers: half-width (100, 2026, 80%)
- Japanese punctuation in prose: full-width (。,、)
- Parentheses in prose: full-width ()
- First term appearance: Japanese (English)
Section Title
<!-- Section template for ch-01_.md and later --> <!-- Target: 2,000-6,000 characters -->
[Write a short lead paragraph that explains this section goal.]
Subheading 1
[Write section content here.]
Key points:
- Point 1
- Point 2
- Point 3
Subheading 2
[Write section content here.]
Detailed Item (optional)
[Add deeper explanation if needed.]
Code Example (optional)
# Example
def example_function():
return "Hello, Copilot"Figure Example (optional)
!Figure caption
Summary
[Add a 1-2 paragraph summary for this section.]
---
Heading Rules (Important)
#is not allowed in section files##= section title (one at the top)###= subsection####= detailed subsection
Character Budget Suggestion
- Lead: 200-300
- Each subsection: 500-1,000
- Summary: 100-200
Writing Workflow Guide
Book writing workflow for "{{BOOK_TITLE}}".
Quick Reference
Writing Workflow
| Step | Action | Description |
|---|---|---|
| Key Points | Write in keypoints/ | Create outline |
| Draft | Write in sections/ | Full manuscript |
| Review | @writing-reviewer | P1/P2/P3 review |
| Convert | Optional | Re:VIEW/PDF output when enabled |
Folder Structure
keypoints/ # Outlines
sections/ # Final manuscripts
re-view-output/ # Optional Re:VIEW and PDF output
images/ # Images
materials/ # Reference materials
docs/ # Documentation
scripts/ # Utility scriptsTemplates
| Template | Usage |
|---|---|
| template-chapter-intro.md | ch*-00 chapter intro |
| template-section.md | ch*-01~ section file |
Reference Documents
| File | Content |
|---|---|
| writing.instructions.md | Style rules |
| writing-notation.instructions.md | Notation rules |
| writing-heading.instructions.md | Heading levels |
| naming-conventions.md | File naming |
| page-allocation.md | Word counts |
| AGENTS.md | Agent definitions |
Heading Level Instructions
Rules for Markdown heading levels in manuscripts.
Basic Rules
| File Type | Allowed Levels | Start With |
|---|---|---|
Chapter intro (ch*-00_*.md) | #, ##, ### | # |
| Section file | ##, ###, #### | ## |
| Column/sidebar | ##, ### | ## |
Why This Matters
- Chapter files define the chapter title with
# - Section files are part of a chapter, so they start with
## - This ensures correct hierarchy in PDF/EPUB output
Examples
Correct
## Section Title
Content here...
### Subsection
More content...Wrong
# Section Title
Content here...Using # in a section file is a P1 issue.
Writing Notation Instructions
Consistent notation and terminology rules.
Numbers
| Type | Rule | Example |
|---|---|---|
| Quantities | Half-width | 100個、5章 |
| Years | Half-width | 2024年 |
| Percentages | Half-width + % | 80%、50% |
| Ranges | Half-width + ~ | 1~10、5~6章 |
Punctuation
| Symbol | Japanese | Usage |
|---|---|---|
| Period | 。 | End of sentence |
| Comma | 、 | Clause separator |
| Colon | : | Full-width in prose |
| Parentheses | () | Full-width in prose |
| Quotes | 「」 | Terminology, titles |
| Double quotes | 『』 | Book titles |
Technical Terms
First Occurrence
Format: 日本語(English)
Examples:
- 機密性(Confidentiality)
- 多層防御(Defense in Depth)
- データ損失防止(Data Loss Prevention、DLP)
Subsequent Occurrences
Use Japanese only, or the acronym if already introduced.
Product Names
- Write exactly as official documentation
- Examples:
- Microsoft Purview
- Azure Active Directory
- GitHub Copilot
Diagram Labels
- Default diagram labels, box titles, and helper text to the main manuscript language.
- Keep original-language operation names, feature names, or product names only when they are the terms readers are expected to recognize.
- When needed, combine both forms instead of choosing one rigidly.
- Example:
pull (pull)orpull (fetch updates)depending on the audience language and book style - Do not leave the entire diagram in English when the surrounding manuscript is not, unless you are reproducing an official UI or quoting a source artifact.
- Keep explanatory text in diagrams reader-facing and plain; avoid tool-author jargon.
Consistency Checklist
- [ ] Numbers are half-width
- [ ] Punctuation follows rules
- [ ] Terms explained on first use
- [ ] Product names match official
- [ ] Acronyms introduced properly
- [ ] Diagram labels follow the manuscript language policy
Writing Instructions
Guidelines for manuscript writing style and quality.
Style Rules
Tone
- Use polite/desu-masu style (です・ます調)
- Write natural Japanese (avoid translation-style)
- Keep sentences under 500 characters
- Vary sentence length and endings for natural flow
Terminology
- Explain technical terms on first use
- Format: Japanese (English) - e.g., 機密性(Confidentiality)
- Use consistent terminology throughout
Structure
- Abstract -> Concrete -> Re-abstract
- Why -> What to protect -> How to design
- End with practical application, not just theory
Source Confidence
- Do not write feature behavior, limits, UI paths, metrics, or procedures from guesswork.
- If a point is not verified, do not smooth it into final prose with plausible wording.
- Keep unresolved items in outline or key-points notes as explicit verification tasks, not as normal explanatory sentences.
- Final manuscript files should contain only verified statements or clearly attributed interpretation.
- Avoid fallback phrasing such as "probably", "should be", or similar hedges when they are only hiding missing verification.
Introduction Pattern
1. Hook 2. Concrete scene 3. Core learning 4. Tomorrow's action 5. Lingering thought
Summary Pattern
- Key point recap
- Practical next step
- Lingering thought or question
- Bridge to next chapter
Figures and Lists
| Element | Rule |
|---|---|
| Bullet lists | Max 3 consecutive in a section, end with prose |
| Tables | Prefer for 3+ item comparisons; keep headers short and scannable |
| Figures | Plan from outline stage; use for concept, flow, hierarchy, or direction |
| Images | Format:  |
Diagram and Table Rules
- If a section compares terms, roles, plans, tools, or options, check whether a table is clearer than bullets or prose.
- If a section explains flow, direction, lifecycle, source/target movement, or hierarchy, plan a figure before expanding the prose.
- Keep diagram intent explicit in the outline: note both the visual type and the reader takeaway.
- Use tables for comparison and figures for flow; do not overload a table with process steps that belong in a diagram.
- When using a figure, add 1 to 2 sentences before or after it to tell the reader what to look at.
Word Count Targets
See docs/page-allocation.md for detailed targets per file type.
Forbidden Patterns
- Overuse of em-dashes for dramatic effect
- Consecutive bullet lists without prose breaks
- Translation-style Japanese
- Sentences over 500 characters
- Speculative statements presented as facts
{{BOOK_TITLE}}
This repository contains the manuscript workspace for "{{BOOK_TITLE}}".
Quick Start
1. Review docs/page-allocation.md and set character targets 2. Review docs/schedule.md and replace placeholder dates 3. Edit .github/copilot-instructions.md for audience and goals 4. Start outlining in keypoints/ 5. Write drafts in sections/
Repository Structure
| Path | Purpose |
|---|---|
keypoints/ | Outline and key-point drafts |
sections/ | Main manuscript files |
re-view-output/ | Optional Re:VIEW source and PDF output |
images/ | Figures and screenshots |
materials/ | Reference sources |
.github/agents/ | Writing workflow agents |
.github/instructions/ | Writing conventions |
docs/ | Schedule, naming rules, and writing docs |
scripts/ | Counting and conversion helpers |
Common Commands
python scripts/count_chars.py
# If Re:VIEW/PDF support is enabled:
# python scripts/convert_md_to_review.py
# python scripts/build_review_pdf.py
# python scripts/inspect_pdf.py pdf/book.pdfWorkflow
1. Draft the structure in keypoints/ 2. Write the manuscript in sections/ 3. Review with @writing-reviewer 4. Convert with scripts/convert_md_to_review.py if Re:VIEW support is enabled 5. Build or polish PDF assets only when PDF output is in scope
Metadata Layer
1. Put stable shared cover and colophon defaults in config/review-metadata/common.yml 2. Put project-specific author, publisher, title, subtitle, and series badge values in config/review-metadata/project.yml 3. If the repo contains a series, compare title naming patterns across books before fixing one title alone 4. Regenerate Re:VIEW config and cover assets through the helper scripts instead of editing generated files directly
# review-ext.rb — Auto-wrap long code lines in Re:VIEW PDF output
#
# Place this file next to config.yml in your Re:VIEW project root.
# Requires: gem install unicode-display_width
#
# Adjust WRAP_WIDTH constants to match your page layout:
# 10pt / 40zw → 76
# 9pt / 43zw → 82
module ReVIEW
module LATEXBuilderOverride
require 'unicode/display_width'
require 'unicode/display_width/string_ext'
CR = ''
# Normal code blocks
WRAP_WIDTH = 82
WRAP_WIDTH_COLUMN = 65
# Line-numbered code blocks
WRAP_WIDTH_NUM = 65
WRAP_WIDTH_NUM_COLUMN = 60
# Scale factor for CJK characters (adjust if needed)
ZWSCALE = 0.875
BREAKABLE_CHARS = [
' ', "\t", '/', '-', '_', '.', ',', ':', ';', ')', ']', '}',
'>', '、', '。', ':', ')', '】', '』', '」'
].freeze
def breakable_char?(char)
BREAKABLE_CHARS.include?(char)
end
def split_line(s, n)
lines = []
remaining = s
until remaining.empty?
width = 0
last_break_index = nil
break_index = nil
chars = remaining.each_char.to_a
chars.each_with_index do |char, idx|
char_width = char.display_width(2)
char_width *= ZWSCALE if char_width == 2
if width + char_width > n
break_index = idx
break
end
width += char_width
last_break_index = idx + 1 if breakable_char?(char)
end
if break_index.nil?
lines << remaining
break
end
split_index = last_break_index || break_index
head = chars[0...split_index].join
tail = chars[split_index..].join
if last_break_index
lines << head.rstrip
remaining = tail.lstrip
else
lines << head
remaining = tail
end
end
lines
end
def code_line(type, line, idx, id, caption, lang)
n = @doc_status[:column] ? WRAP_WIDTH_COLUMN : WRAP_WIDTH
a = split_line(unescape(detab(line)), n)
escape(a.join("\x01\n")).gsub("\x01", CR) + "\n"
end
def code_line_num(type, line, first_line_num, idx, id, caption, lang)
n = @doc_status[:column] ? WRAP_WIDTH_NUM_COLUMN : WRAP_WIDTH_NUM
a = split_line(unescape(detab(line)), n)
(idx + first_line_num).to_s.rjust(2) + ': ' + escape(a.join("\x01\n ")).gsub("\x01", CR) + "\n"
end
end
class LATEXBuilder
prepend LATEXBuilderOverride
end
end
colophon: true
colophon_order:
- aut
- pbl
cover:
enabled: true
image_name: cover.jpg
background_color: "#F6F4EE"
band_color: "#326450"
accent_color: "#C98B3A"
text_color: "#173026"
subtitle_color: "#355B4B"
author_color: "#244738"
publisher_color: "#355B4B"
author_suffix: 著
author_font_size: 54
author_y: 1860
publisher_font_size: 38
publisher_y: 2140
aut:
- 著者名
booktitle: "{{BOOK_TITLE}}"
subtitle: サブタイトル
pbl: 出版社名
cover:
badge: BOOK
strapline: Markdown / Re:VIEW / PDF / Manuscript Workflow
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from review_metadata import generate_cover_image, load_review_metadata
WORKSPACE_ROOT = Path(__file__).resolve().parent.parent
REVIEW_ROOT = WORKSPACE_ROOT / "re-view-output"
def bootstrap_sty() -> None:
if (REVIEW_ROOT / "sty").exists():
return
subprocess.run(
[
"docker",
"run",
"--rm",
"-v",
f"{REVIEW_ROOT}:/work",
"-w",
"/work",
"vvakame/review",
"sh",
"-lc",
"rm -rf /tmp/review-init; mkdir -p /tmp/review-init; cd /tmp/review-init; review-init sample >/dev/null; cp -r sample/sty /work/; cp sample/style.css /work/",
],
check=True,
)
def main() -> int:
for stale_pdf in REVIEW_ROOT.glob("*.pdf"):
stale_pdf.unlink()
bootstrap_sty()
metadata = load_review_metadata(WORKSPACE_ROOT, "project")
generate_cover_image(WORKSPACE_ROOT, REVIEW_ROOT, metadata)
subprocess.run(
[
"docker",
"run",
"--rm",
"-v",
f"{REVIEW_ROOT}:/work",
"-w",
"/work",
"vvakame/review",
"review-pdfmaker",
"config.yml",
],
check=True,
)
pdf_dir = WORKSPACE_ROOT / "pdf"
pdf_dir.mkdir(parents=True, exist_ok=True)
for stale_pdf in pdf_dir.glob("*.pdf"):
stale_pdf.unlink()
for pdf_path in REVIEW_ROOT.glob("*.pdf"):
shutil.move(str(pdf_path), pdf_dir / pdf_path.name)
print(f"Moved: {pdf_path.name} -> pdf/{pdf_path.name}")
# Post-process: insert cover image as full-bleed first page
cover_image = REVIEW_ROOT / "images" / "cover.png"
if not cover_image.exists():
cover_image = REVIEW_ROOT / "images" / "cover.jpg"
if cover_image.exists():
for pdf_file in pdf_dir.glob("*.pdf"):
_insert_cover_page(pdf_file, cover_image)
print(f"[cover] inserted {cover_image.name} into {pdf_file.name}")
return 0
def _insert_cover_page(pdf_path: Path, cover_image_path: Path) -> None:
"""Insert cover image as full-bleed first page using PyMuPDF."""
import fitz
doc = fitz.open(str(pdf_path))
rect = doc[0].rect
cover_doc = fitz.open()
cover_page = cover_doc.new_page(width=rect.width, height=rect.height)
cover_page.insert_image(rect, filename=str(cover_image_path))
doc.insert_pdf(cover_doc, from_page=0, to_page=0, start_at=0)
cover_doc.close()
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".pdf", dir=str(pdf_path.parent))
os.close(tmp_fd)
doc.save(tmp_path, deflate=True)
doc.close()
Path(tmp_path).replace(pdf_path)
if __name__ == "__main__":
raise SystemExit(main())# -*- coding: utf-8 -*-
"""
Markdown to Re:VIEW converter.
Converts Markdown files under sections/ into .re files under
re-view-output/. The script intentionally covers a compact,
predictable subset of Markdown used by this workspace.
"""
from pathlib import Path
import re
import sys
from review_metadata import load_review_metadata, write_review_support_files
def resolve_contents_dir() -> Path | None:
if len(sys.argv) > 1:
return Path(sys.argv[1])
cwd = Path.cwd()
if (cwd / "sections").exists():
return cwd / "sections"
if (cwd.parent / "sections").exists():
return cwd.parent / "sections"
return None
def slugify(value: str) -> str:
lowered = value.lower()
lowered = re.sub(r"[^a-z0-9]+", "-", lowered)
lowered = lowered.strip("-")
return lowered or "chapter"
def replace_inline(text: str) -> str:
text = re.sub(r"`([^`]+)`", r"@<code>{\1}", text)
# Unescape Markdown backslash escapes (e.g. \_ -> _)
text = re.sub(r"\\([_*`\\])", r"\1", text)
text = re.sub(r"\*\*([^*]+)\*\*", r"@<b>{\1}", text)
text = re.sub(r"\*([^*]+)\*", r"@<i>{\1}", text)
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"@<href>{\2, \1}", text)
return text
def is_markdown_table_delimiter(line: str) -> bool:
stripped = line.strip()
if "|" not in stripped:
return False
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells if cell)
def is_markdown_table_row(line: str) -> bool:
stripped = line.strip()
return stripped.startswith("|") and stripped.endswith("|") and "|" in stripped[1:-1]
def parse_markdown_table_row(line: str) -> list[str]:
cells = [replace_inline(cell.strip()) for cell in line.strip().strip("|").split("|")]
return [cell or "." for cell in cells]
def strip_review_inline(text: str) -> str:
previous = None
current = text
while previous != current:
previous = current
current = re.sub(r"@<[^>]+>\{([^{}]*)\}", r"\1", current)
return current.replace("{", "").replace("}", "")
def build_heading_label(
stem: str,
title: str,
heading_label_counts: dict[str, int],
) -> str:
cleaned_title = strip_review_inline(title)
slug = slugify(cleaned_title)
base_label = f"{stem}-{slug}"
occurrence = heading_label_counts.get(base_label, 0) + 1
heading_label_counts[base_label] = occurrence
if occurrence == 1:
return base_label
return f"{base_label}-{occurrence}"
def estimate_review_tsize(table_rows: list[list[str]]) -> str | None:
if not table_rows:
return None
column_count = max(len(row) for row in table_rows)
if column_count <= 1:
return None
normalized_rows: list[list[str]] = []
for row in table_rows:
normalized_rows.append(row + ["."] * (column_count - len(row)))
visible_lengths: list[int] = []
for column_index in range(column_count):
max_length = max(
len(strip_review_inline(row[column_index]))
for row in normalized_rows
)
visible_lengths.append(max(max_length, 4))
total_width_mm = 126
min_width_mm = 14 if column_count <= 4 else 10
remaining_width_mm = max(total_width_mm - min_width_mm * column_count, column_count)
total_length = sum(visible_lengths)
widths = [
min_width_mm + round(remaining_width_mm * (length / total_length))
for length in visible_lengths
]
width_diff = total_width_mm - sum(widths)
widths[-1] += width_diff
if column_count >= 2:
minimum_last_width = 34 if column_count == 2 else 42
if widths[-1] < minimum_last_width:
shortage = minimum_last_width - widths[-1]
widths[-1] = minimum_last_width
donor_indexes = list(range(column_count - 1))
while shortage > 0 and donor_indexes:
adjusted = False
for donor_index in donor_indexes:
if widths[donor_index] > min_width_mm + 2:
widths[donor_index] -= 1
shortage -= 1
adjusted = True
if shortage == 0:
break
if not adjusted:
break
return "|" + "|".join(f"L{{{width}mm}}" for width in widths) + "|"
def ensure_blank_line_before_list(output: list[str]) -> None:
if not output:
return
if output[-1] == "":
return
if re.match(r"^\s+(?:\*+|\d+\.)\s+", output[-1]):
return
output.append("")
def fence_caption_from_info(fence_info: str) -> tuple[str, str | None]:
code_lang = "text"
code_caption = None
if fence_info:
fence_parts = fence_info.split(maxsplit=1)
code_lang = fence_parts[0]
if len(fence_parts) > 1:
code_caption = fence_parts[1].strip()
return code_lang, code_caption
def consume_list_continuation(lines: list[str], index: int) -> tuple[list[str], int]:
items: list[str] = []
current_index = index
while current_index < len(lines):
raw_line = lines[current_index]
line = raw_line.rstrip()
if not line.strip():
break
if re.match(r"^\s{2,}\S", raw_line) or raw_line.startswith("\t"):
items.append(line.strip())
current_index += 1
continue
break
return items, current_index
def is_link_only_line(text: str) -> bool:
stripped = text.strip()
return bool(re.fullmatch(r"\[[^\]]+\]\([^\)]+\)", stripped))
def convert_markdown(text: str, stem: str) -> str:
output: list[str] = []
in_code_block = False
code_id = 1
code_lang = "text"
heading_label_counts: dict[str, int] = {}
lines = text.splitlines()
index = 0
while index < len(lines):
raw_line = lines[index]
line = raw_line.rstrip()
fence = re.match(r"^```(.*)$", line)
if fence:
if not in_code_block:
fence_info = fence.group(1).strip()
code_lang, code_caption = fence_caption_from_info(fence_info)
if code_caption:
output.append(f"//listnum[{stem}-{code_id}][{code_caption}]{{")
else:
output.append(f"//listnum[{stem}-{code_id}][]{{")
code_id += 1
in_code_block = True
else:
output.append("//}")
in_code_block = False
index += 1
continue
if in_code_block:
output.append(raw_line)
index += 1
continue
if index + 1 < len(lines) and is_markdown_table_row(line) and is_markdown_table_delimiter(lines[index + 1]):
table_rows = [parse_markdown_table_row(line)]
index += 2
while index < len(lines) and is_markdown_table_row(lines[index]):
table_rows.append(parse_markdown_table_row(lines[index]))
index += 1
tsize = estimate_review_tsize(table_rows)
if tsize:
output.append(f"//tsize[|latex|{tsize}]")
output.append("//table{")
output.append("\t".join(table_rows[0]))
output.append("-" * 60)
for row in table_rows[1:]:
output.append("\t".join(row))
output.append("//}")
continue
heading = re.match(r"^(#{1,6})\s+(.*)$", line)
if heading:
level = len(heading.group(1))
title = replace_inline(heading.group(2).strip())
label = build_heading_label(stem, title, heading_label_counts)
output.append(f"{'=' * level}{{{label}}} {title}")
index += 1
continue
image = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)$", line)
if image:
caption = image.group(1) or Path(image.group(2)).stem
image_id = slugify(Path(image.group(2)).stem)
output.append(f"//image[{image_id}][{caption}]{{")
output.append(image.group(2))
output.append("//}")
index += 1
continue
unordered = re.match(r"^\s*[-*+]\s+(.*)$", line)
if unordered:
ensure_blank_line_before_list(output)
continuation_lines, next_index = consume_list_continuation(lines, index + 1)
output.append(f" * {replace_inline(unordered.group(1))}")
for continuation_line in continuation_lines:
if is_link_only_line(continuation_line):
output.append(" @<br>{}")
output.append(f" {replace_inline(continuation_line)}")
index = next_index
continue
ordered = re.match(r"^\s*\d+\.\s+(.*)$", line)
if ordered:
ensure_blank_line_before_list(output)
continuation_lines, next_index = consume_list_continuation(lines, index + 1)
output.append(f" 1. {replace_inline(ordered.group(1))}")
for continuation_line in continuation_lines:
if is_link_only_line(continuation_line):
output.append(" @<br>{}")
output.append(f" {replace_inline(continuation_line)}")
index = next_index
continue
output.append(replace_inline(line))
index += 1
if in_code_block:
output.append("//}")
return "\n".join(output).strip() + "\n"
def build_output_name(md_file: Path) -> str:
stem = md_file.stem
match = re.match(r"ch(\d+)-(\d+)_(.*)", stem)
if match:
chapter = int(match.group(1))
slug = slugify(match.group(3))
return f"ch{chapter:02d}-{slug}.re"
return f"{slugify(stem)}.re"
def main() -> int:
contents_dir = resolve_contents_dir()
if contents_dir is None:
print("Error: Could not find sections/ folder")
print("Usage: python convert_md_to_review.py [path]")
return 1
if not contents_dir.exists():
print(f"Error: Path not found: {contents_dir}")
return 1
project_root = contents_dir.parent
output_root = project_root / "re-view-output"
output_root.mkdir(parents=True, exist_ok=True)
generated_files: list[str] = []
for md_file in sorted(contents_dir.rglob("*.md")):
output_name = build_output_name(md_file)
output_path = output_root / output_name
converted = convert_markdown(md_file.read_text(encoding="utf-8"), slugify(md_file.stem))
output_path.write_text(converted, encoding="utf-8")
generated_files.append(output_name)
print(f"Converted: {md_file.relative_to(project_root)} -> {output_path.relative_to(project_root)}")
metadata = load_review_metadata(project_root, "project")
write_review_support_files(output_root, generated_files, metadata)
print(f"Generated {len(generated_files)} Re:VIEW files in {output_root}")
return 0
if __name__ == "__main__":
raise SystemExit(main())# -*- coding: utf-8 -*-
"""
Character Counter Script
Count characters in Markdown files under sections/.
Excludes Markdown syntax such as headings, code blocks, and links.
"""
from collections import defaultdict
from pathlib import Path
import re
import sys
def clean_markdown(text: str) -> str:
text = re.sub(r"```[\s\S]*?```", "", text)
text = re.sub(r"`[^`]+`", "", text)
text = re.sub(r"^#+\s*", "", text, flags=re.MULTILINE)
text = re.sub(r"\|", "", text)
text = re.sub(r"^[-:]+$", "", text, flags=re.MULTILINE)
text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.MULTILINE)
text = re.sub(r"^\s*\d+\.\s+", "", text, flags=re.MULTILINE)
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
text = re.sub(r"\*([^*]+)\*", r"\1", text)
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
text = re.sub(r"!\[[^\]]*\]\([^)]+\)", "", text)
text = re.sub(r"^>\s*", "", text, flags=re.MULTILINE)
text = re.sub(r"^[-*_]{3,}$", "", text, flags=re.MULTILINE)
text = re.sub(r"[\s\r\n\t ]", "", text)
return text
def get_file_type(filename: str) -> tuple[str, str]:
if "Column" in filename or "コラム" in filename:
return "Column", "2,000-3,000"
if re.match(r"^ch\d+-00_", filename):
return "Intro", "300-500"
return "Main", "3,000-5,000"
def check_status(char_count: int, file_type: str) -> str:
ranges = {
"Column": (1500, 3500),
"Intro": (200, 700),
"Main": (2000, 6000),
}
min_val, max_val = ranges.get(file_type, (2000, 6000))
if char_count < min_val:
return "⚠️ Under"
if char_count > max_val:
return "⚠️ Over"
return "✅ OK"
def resolve_base_path() -> Path | None:
if len(sys.argv) > 1:
return Path(sys.argv[1])
cwd = Path.cwd()
if (cwd / "sections").exists():
return cwd / "sections"
if (cwd.parent / "sections").exists():
return cwd.parent / "sections"
return None
def main() -> int:
base_path = resolve_base_path()
if base_path is None:
print("Error: Could not find sections/ folder")
print("Usage: python count_chars.py [path]")
return 1
if not base_path.exists():
print(f"Error: Path not found: {base_path}")
return 1
results = []
chapter_totals = defaultdict(lambda: {"count": 0, "files": 0})
for md_file in sorted(base_path.rglob("*.md")):
content = md_file.read_text(encoding="utf-8")
char_count = len(clean_markdown(content))
relative_path = md_file.relative_to(base_path)
chapter = relative_path.parts[0] if relative_path.parts else "Unknown"
file_type, target = get_file_type(md_file.name)
status = check_status(char_count, file_type)
results.append(
{
"chapter": chapter,
"file_type": file_type,
"char_count": char_count,
"target": target,
"status": status,
"relative_path": relative_path.as_posix(),
}
)
chapter_totals[chapter]["count"] += char_count
chapter_totals[chapter]["files"] += 1
print("## Chapter Summary\n")
print("| Chapter | Files | Characters | Status |")
print("|:--------|------:|-----------:|:-------|")
total_chars = 0
total_files = 0
for chapter in sorted(chapter_totals):
data = chapter_totals[chapter]
total_chars += data["count"]
total_files += data["files"]
if data["count"] < 5000:
note = "⚠️ Under"
elif data["count"] < 15000:
note = "Low"
elif data["count"] > 40000:
note = "⚠️ Over"
else:
note = "✅ OK"
print(f"| {chapter} | {data['files']} | {data['count']:,} | {note} |")
print("\n## Total\n")
print(f"- Total files: {total_files}")
print(f"- Total characters: **{total_chars:,}**")
issues = [row for row in results if "⚠️" in row["status"]]
if issues:
print(f"\n## Issues ({len(issues)} files)\n")
print("| File | Type | Count | Target | Status |")
print("|:-----|:----:|------:|:-------|:------:|")
for row in issues:
print(
f"| {row['relative_path']} | {row['file_type']} | {row['char_count']:,} | {row['target']} | {row['status']} |"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())from __future__ import annotations
import argparse
from pathlib import Path
from pypdf import PdfReader
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Inspect a PDF file.")
parser.add_argument("pdf_path")
parser.add_argument("--pages", type=int, default=6)
return parser.parse_args()
def main() -> int:
args = parse_args()
reader = PdfReader(str(Path(args.pdf_path)))
print(f"pages={len(reader.pages)}")
for index in range(min(args.pages, len(reader.pages))):
text = (reader.pages[index].extract_text() or "").replace("\n", " ")
print(f"PAGE {index + 1}: {text[:300]}")
return 0
if __name__ == "__main__":
raise SystemExit(main())from __future__ import annotations
from copy import deepcopy
from datetime import date
from pathlib import Path
import re
import shutil
from PIL import Image, ImageDraw, ImageFont
import yaml
DEFAULT_TEXDOCUMENTCLASS = [
"review-jsbook",
"media=print,paper=a4,fontsize=10pt,baselineskip=15.4pt,line_length=40zw,number_of_lines=35,jafont=noto-otf,serial_pagination=true,openright",
]
def slugify(value: str) -> str:
lowered = value.lower()
lowered = re.sub(r"[^a-z0-9]+", "-", lowered)
lowered = lowered.strip("-")
return lowered or "book"
def load_yaml(path: Path) -> dict:
if not path.exists():
return {}
loaded = yaml.safe_load(path.read_text(encoding="utf-8"))
return loaded if isinstance(loaded, dict) else {}
def deep_merge(base: dict, override: dict) -> dict:
merged = deepcopy(base)
for key, value in override.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = deep_merge(merged[key], value)
else:
merged[key] = value
return merged
def load_review_metadata(workspace_root: Path, metadata_name: str = "project") -> dict:
root = workspace_root / "config" / "review-metadata"
common = load_yaml(root / "common.yml")
specific = load_yaml(root / f"{metadata_name}.yml")
merged = deep_merge(common, specific)
today = date.today().isoformat()
merged.setdefault("review_version", "5.0")
merged.setdefault("language", "ja")
merged.setdefault("toclevel", 3)
merged.setdefault("secnolevel", 3)
merged.setdefault("toc", True)
merged.setdefault("titlefile", "custom-titlepage.tex")
merged.setdefault("stylesheet", ["style.css"])
merged.setdefault("texdocumentclass", DEFAULT_TEXDOCUMENTCLASS)
merged.setdefault("contentdir", ".")
merged.setdefault("date", today)
merged.setdefault("history", [[f"{merged['date']} 初版第1刷"]])
merged.setdefault("colophon", True)
merged.setdefault("colophon_order", ["aut", "pbl"])
if merged.get("aut") and not merged.get("rights"):
merged["rights"] = f"(C) {merged['date'][:4]} {', '.join(merged['aut'])}"
if merged.get("booktitle") and not merged.get("bookname"):
merged["bookname"] = slugify(merged["booktitle"])
cover = merged.setdefault("cover", {})
cover.setdefault("enabled", True)
cover.setdefault("image_name", "cover.jpg")
cover.setdefault("background_color", "#F6F4EE")
cover.setdefault("band_color", "#326450")
cover.setdefault("accent_color", "#C98B3A")
cover.setdefault("text_color", "#173026")
cover.setdefault("subtitle_color", "#355B4B")
cover.setdefault("author_color", "#244738")
cover.setdefault("publisher_color", "#355B4B")
cover.setdefault("badge", "")
cover.setdefault("strapline", "")
cover.setdefault("author_suffix", "著")
cover.setdefault("author_font_size", 54)
cover.setdefault("author_y", 1860)
cover.setdefault("publisher_font_size", 38)
cover.setdefault("publisher_y", 2140)
return merged
def write_review_support_files(review_root: Path, generated_files: list[str], metadata: dict) -> None:
catalog_path = review_root / "catalog.yml"
config_path = review_root / "config.yml"
catalog_lines = ["PREDEF:", "CHAPS:"]
catalog_lines.extend(f" - {name}" for name in generated_files)
catalog_lines.append("POSTDEF:")
catalog_path.write_text("\n".join(catalog_lines) + "\n", encoding="utf-8")
config_data: dict = {
"bookname": metadata["bookname"],
"booktitle": metadata["booktitle"],
"review_version": metadata["review_version"],
"language": metadata["language"],
"toclevel": metadata["toclevel"],
"secnolevel": metadata["secnolevel"],
"toc": metadata["toc"],
"titlefile": metadata["titlefile"],
"texdocumentclass": metadata["texdocumentclass"],
"stylesheet": metadata["stylesheet"],
"contentdir": metadata["contentdir"],
"date": metadata["date"],
"history": metadata["history"],
"colophon": metadata["colophon"],
"colophon_order": metadata["colophon_order"],
}
for field in ["subtitle", "aut", "pbl", "rights"]:
value = metadata.get(field)
if value:
config_data[field] = value
# Cover image is handled by build_review_pdf.py post-processing (PyMuPDF).
# Do NOT set coverimage in config.yml — Re:VIEW's built-in handler produces
# blank pages with dvipdfmx in the vvakame/review Docker image.
config_path.write_text(
yaml.safe_dump(
config_data,
allow_unicode=True,
sort_keys=False,
default_flow_style=False,
),
encoding="utf-8",
)
def _find_font(candidates: list[str], size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
for candidate in candidates:
path = Path(candidate)
if path.exists():
try:
return ImageFont.truetype(str(path), size=size)
except OSError:
continue
return ImageFont.load_default()
def _wrap_text(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont, max_width: int) -> list[str]:
words = text.split()
if len(words) == 1 and " " not in text:
words = list(text)
lines: list[str] = []
current = ""
for word in words:
candidate = word if not current else f"{current} {word}" if words is not list(text) else current + word
if draw.textlength(candidate, font=font) <= max_width:
current = candidate
else:
if current:
lines.append(current)
current = word
if current:
lines.append(current)
return lines or [text]
def _draw_multiline(draw, text, font, fill, x, y, max_width, line_spacing):
lines = _wrap_text(draw, text, font, max_width)
current_y = y
for line in lines:
draw.text((x, current_y), line, font=font, fill=fill)
bbox = draw.textbbox((x, current_y), line, font=font)
current_y = bbox[3] + line_spacing
return current_y
def _format_cover_author_text(authors: list[str], suffix: str) -> str:
if not authors:
return ""
if len(authors) == 1:
return f"{authors[0]} {suffix}" if suffix else authors[0]
joined = " / ".join(authors)
return f"{joined} {suffix}" if suffix else joined
def generate_cover_image(workspace_root: Path, review_root: Path, metadata: dict) -> Path | None:
cover = metadata.get("cover", {})
if not cover.get("enabled", False):
return None
image_name = cover.get("image_name", "cover.jpg")
images_dir = review_root / "images"
images_dir.mkdir(parents=True, exist_ok=True)
output_path = images_dir / image_name
source = cover.get("source")
if source:
source_path = (workspace_root / source).resolve()
if source_path.exists():
shutil.copy2(source_path, output_path)
return output_path
width, height = 1600, 2400
image = Image.new("RGB", (width, height), cover["background_color"])
draw = ImageDraw.Draw(image)
draw.rectangle((0, 0, width, 260), fill=cover["band_color"])
draw.rectangle((90, 180, width - 90, 205), fill=cover["accent_color"])
draw.rectangle((100, 2080, width - 100, 2092), fill=cover["accent_color"])
title_font = _find_font([r"C:\Windows\Fonts\YuGothB.ttc", r"C:\Windows\Fonts\meiryob.ttc", r"C:\Windows\Fonts\msgothic.ttc"], 96)
subtitle_font = _find_font([r"C:\Windows\Fonts\YuGothM.ttc", r"C:\Windows\Fonts\meiryo.ttc", r"C:\Windows\Fonts\msgothic.ttc"], 42)
author_font = _find_font([r"C:\Windows\Fonts\YuGothM.ttc", r"C:\Windows\Fonts\meiryo.ttc", r"C:\Windows\Fonts\msgothic.ttc"], int(cover["author_font_size"]))
publisher_font = _find_font([r"C:\Windows\Fonts\YuGothM.ttc", r"C:\Windows\Fonts\meiryo.ttc", r"C:\Windows\Fonts\msgothic.ttc"], int(cover["publisher_font_size"]))
badge_font = _find_font([r"C:\Windows\Fonts\YuGothB.ttc", r"C:\Windows\Fonts\meiryob.ttc", r"C:\Windows\Fonts\msgothic.ttc"], 30)
badge = cover.get("badge", "")
if badge:
draw.rounded_rectangle((100, 96, 380, 182), radius=28, fill=cover["accent_color"])
draw.text((132, 116), badge, font=badge_font, fill=cover["background_color"])
strapline = cover.get("strapline", "")
if strapline:
draw.text((100, 320), strapline, font=subtitle_font, fill=cover["subtitle_color"])
current_y = _draw_multiline(draw, metadata["booktitle"], title_font, cover["text_color"], 100, 520, width - 200, 18)
subtitle = metadata.get("subtitle")
if subtitle:
current_y += 26
_draw_multiline(draw, subtitle, subtitle_font, cover["subtitle_color"], 100, current_y, width - 220, 12)
authors = metadata.get("aut", [])
if authors:
author_text = _format_cover_author_text(authors, str(cover.get("author_suffix", "")))
draw.text((100, int(cover["author_y"])), author_text, font=author_font, fill=cover["author_color"])
publisher = metadata.get("pbl")
if publisher:
publisher_bbox = draw.textbbox((0, 0), publisher, font=publisher_font)
publisher_width = publisher_bbox[2] - publisher_bbox[0]
draw.text((width - 100 - publisher_width, int(cover["publisher_y"])), publisher, font=publisher_font, fill=cover["publisher_color"])
image.save(output_path, quality=92)
# Also save as PNG for dvipdfmx compatibility and PyMuPDF insertion
png_path = output_path.with_suffix(".png")
image.save(png_path)
return output_path\RequirePackage{xcolor}
\RequirePackage{caption}
\renewcommand{\kanjifamilydefault}{\gtdefault}
\renewcommand{\familydefault}{\sfdefault}
\definecolor{ReviewChapterRule}{HTML}{326450}
\definecolor{ReviewSectionRule}{HTML}{326450}
\setlength{\parindent}{0pt}
\DeclareCaptionFont{reviewfigurecaption}{\fontsize{6pt}{8pt}\selectfont}
\captionsetup[figure]{font=reviewfigurecaption}
\DeclareRobustCommand{\reviewincludegraphics}[2][]{%
\includegraphics[width=0.6\linewidth,height=0.6\textheight,keepaspectratio,#1]{#2}}
\makeatletter
\def\@makechapterhead#1{%
\vspace*{12\p@}%
{\parindent \z@ \raggedright \normalfont
{\color{ReviewChapterRule}\hrule height 1.2pt\relax}\par
\vspace{1.2\baselineskip}%
\ifnum \c@secnumdepth >\m@ne
{\headfont\fontsize{11pt}{14pt}\selectfont \@chapapp\thechapter\@chappos\par\nobreak}%
\vspace{0.5\baselineskip}%
\fi
{\headfont\fontsize{20pt}{24pt}\selectfont #1\par\nobreak}%
\vspace{1.8\baselineskip}}}
\makeatother
\RequirePackage{tcolorbox}
\RequirePackage{etoolbox}
\tcbuselibrary{skins,breakable}
\definecolor{ReviewColumnPageBg}{HTML}{FFF6EA}
\definecolor{ReviewColumnFrame}{HTML}{C98B3A}
\AtBeginEnvironment{tabular}{%
\ifnum\value{chapter}=4
\fontsize{6.4pt}{9.6pt}\selectfont
\else
\fontsize{7.2pt}{10.8pt}\selectfont
\fi
}
\AtBeginEnvironment{longtable}{%
\ifnum\value{chapter}=4
\fontsize{6.4pt}{9.6pt}\selectfont
\else
\fontsize{7.2pt}{10.8pt}\selectfont
\fi
}
\renewenvironment{reviewcolumn}[1][]{%
\clearpage
\pagecolor{ReviewColumnPageBg}
\thispagestyle{plainhead}
\fontsize{5.6pt}{8.4pt}\selectfont
\begin{tcolorbox}[
enhanced jigsaw,
breakable,
sharp corners,
boxrule=0mm,
colback=ReviewColumnPageBg,
borderline west={0.9mm}{0mm}{ReviewColumnFrame},
left=4mm,
right=3mm,
top=4mm,
bottom=3mm,
pad at break=0mm,
before skip=0pt,
after skip=0pt
]
\reviewcolumnhead{}{#1}
}{%
\end{tcolorbox}
\clearpage
\nopagecolor
}
\renewcommand{\reviewcolumnhead}[2]{%
{\noindent\color{ReviewColumnFrame}\headfont\fontsize{11pt}{14pt}\selectfont コラム: #2\par\vspace{0.6\baselineskip}}}\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{review-style}[2021/01/06]
\RequirePackage{fancyhdr}
\RequirePackage{graphicx}
\RequirePackage{truncate}
\newcommand{\reviewcurrentsection}{}
\newcommand{\reviewtopsection}{\gtfamily\sffamily\bfseries\upshape\small \nouppercase{\truncate{.78\textwidth}{\reviewcurrentsection}}}
\newcommand{\reviewtopchapter}{\gtfamily\sffamily\bfseries\upshape\small \nouppercase{\truncate{.78\textwidth}{\leftmark}}}
\pagestyle{fancy}
\fancyhead{}
\fancyhead[CE]{\reviewtopchapter}
\fancyhead[CO]{\reviewtopsection}
\fancyfoot{}
\fancyfoot[C]{\thepage}
\renewcommand{\sectionmark}[1]{\gdef\reviewcurrentsection{\thesection~#1}\markright{}}
\renewcommand{\chaptermark}[1]{\markboth{\prechaptername\ \thechapter\ \postchaptername~#1}{}}
\renewcommand{\headfont}{\gtfamily\sffamily\bfseries}
\hypersetup{hidelinks}
\floatplacement{figure}{H}
\floatplacement{table}{H}
\usepackage{xurl}
\emergencystretch=3em