mejiro breaks Japanese vertical text the way a typesetter would, and hands you the result as data. Give it a string or an EPUB and get back line breaks that honour kinsoku shori, hanging punctuation, ruby and tate-chu-yoko, laid out into pages you render however you like. The core engine is arithmetic over typed arrays — no DOM, no Canvas, no dependencies — so the same breaks come out in a browser, in Node, and during server-side rendering.
Reach for it when you need to:
- Break Japanese lines correctly — JIS X 4051 / JLREQ kinsoku, hanging punctuation, ruby that never clips its base text, tate-chu-yoko runs that never split across a column.
- Read and write EPUB — parse a file into text plus inline annotations, edit it in place and write it back, or author one from a plain-text manuscript. Untrusted archives are bounded by resource limits.
- Wrap text around images — per-column exclusion across a single page or a two-page spread, recomputed live while the reader drags the image.
- Ship a reader or an editor — React and Vue components sit on the same engine; take the hooks instead and render the pages yourself.
📖 Documentation · Getting started · API reference
Vertical readers and manuscript editors, in the browser or pre-rendered on a
server. examples/ holds runnable starters for each shape — a paginated reader,
a spread-based EPUB editor, a manuscript authoring tool, a bookshelf, a headless
setup that uses only the hooks, and an iframe embed with no framework at all.
Each starter is a workspace member, so copy it out before installing:
npx degit libraz/mejiro/examples/react my-readerThe copied package.json still carries workspace:* versions; each starter's
README has the one-liner that pins them to the published release.
| Subpath | Description |
|---|---|
@libraz/mejiro |
Core: computeBreaks(), kinsoku, hanging punctuation, preprocessRuby(), preprocessTcy() (tate-chu-yoko), normalizeText() NFC normalization, ExclusionEngine, paginate(), reading-position persistence |
@libraz/mejiro/browser |
Browser: MejiroBrowser, font measurement and width caching, ruby font derivation, createOverlayDragSession() for image drag and resize |
@libraz/mejiro/epub |
EPUB: parseEpub() with EpubParseOptions.limits for untrusted input, EditableEpub, cloneEditableEpubBook(), EpubProject, parseManuscript() |
@libraz/mejiro/render |
Render: buildRenderPage(), buildParagraphMeasures(), segmentToInlineNode(), paragraphClassName(), renderEpubStatic(), and the mejiro.css / mejiro-reader.css / mejiro-editor.css / mejiro-fonts.css / mejiro-print.css stylesheets |
@libraz/mejiro/book |
Book: MejiroBook, ChapterLayout, estimateReadingTime() — layout, pagination, image exclusion, text search, anchors and snapshots in one class |
@libraz/mejiro/image |
prepareImage(file, opts?) — decode, downscale and re-encode before embedding |
@libraz/mejiro-react |
React: <MejiroReader>, <MejiroEditor>, <MejiroManuscriptEditor>, <MejiroShelf>, <MejiroToc>, <MejiroPage> and the useEpub / useMejiroBook / useChapterLayout / useSpread / useReadingPosition / useAnnotations hooks |
@libraz/mejiro-vue |
Vue: the same component and composable set as React |
The React and Vue packages are experimental — the hooks and low-level components are the primary API, and the composed components are their reference implementation.
npm install @libraz/mejiro # core, browser, EPUB, render, book, image
npm install @libraz/mejiro-react react react-dom # React components (experimental)
npm install @libraz/mejiro-vue vue # Vue components (experimental)import { getLineRanges, paginate } from '@libraz/mejiro';
import { MejiroBrowser } from '@libraz/mejiro/browser';
const mejiro = new MejiroBrowser({
fixedFontFamily: '"Noto Serif JP"',
fixedFontSize: 16,
});
const text = '吾輩は猫である。名前はまだ無い。';
// 1. Lay out text (fontFamily/fontSize use instance defaults)
const result = await mejiro.layout({
text,
lineWidth: mejiro.verticalLineWidth(600), // effective line width from container height
});
// 2. Get line ranges → [[start, end), ...]
const lines = getLineRanges(result.breakPoints, text.length);
// 3. Paginate into pages of 400px width
const pages = paginate(400, [
{ lineCount: lines.length, linePitch: 16 * 1.8, gapBefore: 0 },
]);MejiroBook is the high-level entry point: it owns measurement, breaking and
pagination, so a reader only deals in chapters and spreads.
import { DEFAULT_HEADING_STYLES, MejiroBook } from '@libraz/mejiro/book';
import { parseEpub } from '@libraz/mejiro/epub';
const book = new MejiroBook({
fontFamily: '"Noto Serif JP"',
fontSize: 16,
lineSpacing: 1.8,
headingStyles: DEFAULT_HEADING_STYLES,
});
// Auto-compute page size from a container element
book.computePageSize(document.querySelector('.reading-surface')!);
const epub = await parseEpub(epubArrayBuffer);
const layout = await book.layoutChapter(epub.chapters[0]);
// Get a two-page spread
const spread = layout.getSpread(0);
// spread.right.page → RenderPage (paragraphs → lines → segments)
// spread.right.lines / spread.right.slots → for absolute positioning
// spread.totalPages → total page count
// Place images with text wrapping (returns updated spread)
const updated = layout.syncImages(0, [{ x: 80, y: 100, w: 120, h: 160 }]);import { MejiroReader } from '@libraz/mejiro-react';
import '@libraz/mejiro/render/mejiro-reader.css';
<MejiroReader file={epubFile} theme="sepia" />;Vue exposes the same component under @libraz/mejiro-vue. See the
React / Vue guide for props, theming, controlled
usage and SSR.
Kinsoku shori (禁則処理) keeps certain characters off the start or end of a line, as defined in JIS X 4051 and JLREQ. Two modes ship:
- Strict (default) — closing brackets, punctuation, small kana, the long vowel mark and iteration marks never start a line; opening brackets never end one.
- Loose — strict, except small kana and
ーmay start a line. Useful for narrow columns.
Hanging punctuation lets 。 、 , . protrude past the line end
instead of being pushed to the next line.
Ruby is resolved before line breaking: each annotated span reserves the wider of its base text and its reading, so a reading can never be clipped by the line it sits on. Tate-chu-yoko collapses a horizontal run inside a vertical column into one upright box that the breaker treats as indivisible.
Custom kinsoku rules go through LayoutInput.kinsokuRules on the core
computeBreaks() API. Line breaking has the full
character lists, the conformance table and worked examples.
- Core — line breaking, kinsoku, hanging, ruby and tate-chu-yoko preprocessing, image exclusion. Zero dependencies, no DOM.
- Browser — font measurement over Canvas and the FontFace API, ruby font derivation, overlay drag sessions.
- Render — turns layout results into a framework-agnostic
RenderPagestructure plus the shipped stylesheets. - Book and EPUB are siblings over render, not a stack:
MejiroBookorchestrates measurement, breaking and pagination, while the EPUB layer parses and authors files (its only external dependency isjszip). Neither imports the other. - Image stands alone — it prepares images for placement and is usable on its own.
Guides and the full API reference live in docs/en/
(日本語).
- Learn — Getting started · Core concepts · Line breaking · Ruby
- Build — Browser integration · EPUB · Pagination and rendering · React and Vue
- Details — Advanced · API reference
- TypedArray core —
Uint32Arrayfor codepoints,Float32Arrayfor advances. No string manipulation in the hot path. - O(n) line breaking — a single-pass greedy algorithm with backtracking for kinsoku, not dynamic programming.
- Ruby and tate-chu-yoko as preprocessing — both resolve to effective advances and cluster IDs before the main loop, so the breaking algorithm itself stays unchanged.
- Deterministic — the same input always produces the same output, which is what makes server-side rendering and snapshot replay possible.
- Separation of concerns — the core is pure arithmetic, the browser layer measures, the EPUB layer parses and authors, and the render layer emits data. Producing the final DOM is the caller's job.
mejiro lays out text; it does not draw it. There is no renderer of its own, no horizontal-writing layout engine, no font file parsing (measurement goes through Canvas), and no PDF or print pipeline beyond the stylesheet. EPUB support targets what a vertical Japanese reader needs rather than the whole specification — fixed-layout EPUB, audio overlays and DRM are out of scope. The React and Vue packages are reference implementations of the hooks, not a design system.
Report vulnerabilities through SECURITY.md, not the public
issue tracker. EPUB parsing accepts untrusted archives and bounds them with
EpubParseOptions.limits; the defaults are in
DEFAULT_EPUB_PARSE_LIMITS.
