npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sushichan044/kg-core

v0.11.2

Published

Framework-independent parsing, composition, and proofreading for Japanese manuscripts

Readme

@sushichan044/kg-core

Framework-independent parsing, composition, and proofreading for Japanese manuscripts.

The package exposes plain readonly data transfer objects and pure processing functions. It does not own application state, persistence, React components, or DOM behavior.

Install

pnpm add @sushichan044/kg-core

Entry points

The package is split by what you are doing, so an application only ever sees the surface it needs.

| Entry | For | | ------------------------------ | ----------------------------------------------------------------------------------------------- | | @sushichan044/kg-core | Running the pipeline and reading what comes out: parse, compose, settings, results, diagnostics | | @sushichan044/kg-core/lint | Proofreading — running the rules, and writing your own | | @sushichan044/kg-core/plugin | Supplying an implementation to core: a parser, a composer, or a measurer |

A name is exported only when one of those three needs to write it. Helpers core keeps for itself are not part of the API.

Processing pipeline

import {
  ComposeError,
  composeManuscript,
  NovelCompositionSettings,
  novelComposer,
  ParseError,
  parseManuscript,
  kakuyomuParser,
  pixivParser,
} from "@sushichan044/kg-core";
import {
  createDefaultProofreadingRules,
  ProofreadError,
  proofreadManuscript,
} from "@sushichan044/kg-core/lint";

const parsed = parseManuscript(source, { parser: kakuyomuParser });
if (!parsed.ok) throw new Error(ParseError.describe(parsed.error));

const composed = composeManuscript(parsed.value, {
  composer: novelComposer,
  settings: NovelCompositionSettings.defaults,
});
if (!composed.ok) throw new Error(ComposeError.describe(composed.error));

const proofread = proofreadManuscript(composed.value, {
  rules: createDefaultProofreadingRules(),
});
if (!proofread.ok) throw new Error(ProofreadError.describe(proofread.error));

const diagnostics = [...parsed.warnings, ...proofread.value];

parseManuscript uses plainTextParser when no parser is supplied. The built-in pixivParser normalizes ruby, bold, italic, and emphasis notation; kakuyomuParser normalizes Kakuyomu ruby and emphasis marks. Malformed or unknown notation remains visible and produces a parser warning.

composeManuscript requires a composer. The built-in novelComposer produces a self-contained snapshot containing the parsed manuscript, accepted settings, geometry, statistics, and a page/stage/line hierarchy. Lines contain positioned inline items and annotation fragments. glyph items distinguish their typographic layoutSpan from their visual renderSpan; glue, kern, and suppressed items preserve every spacing decision. Each line also records its inlineSizeEm and whether it ended naturally, by shrinking, stretching, hanging, forcing, or reaching the paragraph end.

The composer classifies the core prose classes from JLReq, resolves pair spacing, and selects line breaks over the whole source paragraph. The space between two classes follows appendix 表1 of JLReq: a half em before an opening bracket and after a closing bracket, comma, or full stop, a quarter em on either side of a middle dot, a quarter em between Japanese and a numeral, unit symbol, or a Western character rotated 90 degrees or mixed into horizontal text, and solid where two of those punctuation marks meet. JLReq 3.2.4 sets that quarter em's scope: a Western character set upright — one letter, an all-caps or Xxx abbreviation, a lone digit, or a full-width letter — classes with kanji instead and sits solid against the kana or kanji beside it, the same as a tate-chu-yoko run.

A western word space is not a character with a box but an アキ of its own: a third em by rule rather than by measurement, nothing at all at a line head or a line end, and the first visible space either direction of the adjustment reaches for — only the line-end space, which the reader cannot see at all, is spent ahead of it. It leaves the composer as a glue item carrying the source text it came from.

An overflowing line is answered by reducing the space between characters, stage by stage, or by hanging a comma or full stop; a short line is answered by opening up space where JLReq 3.8.4 and appendix 表6 allow it — the word space first, up to a half, then the Japanese-to-Western quarter em, also up to a half, then a solid pair between kana, kanji, and the marks that go with them, up to a quarter. If those stages cannot fill the line, the remaining width is added equally to the admitted pairs in 表6 that are solid at rest, including pairs used by the third stage. Intrinsic spaces keep their finite limits: a word space and a mixed Japanese-to-Western, numeral, or unit-symbol gap stop at a half em, while punctuation and explicit full-em spaces retain their own width. This keeps the unbounded stage from producing double space or a hole disproportionate to a horizontal Western run's own length, since an upright Western run classes with kanji and has no gap of its own to begin with. Line-head and line-end space, inseparable pairs, and source-authored full-em gaps after question or exclamation marks stay fixed. A line with no admitted gap is forced. The choice is made for the paragraph rather than line by line, minimizing in order the number of forced lines, then stretched, then hanging, then shrunk lines that spent visible space. Which space may be reduced, and in which order, follows JLReq 3.8.3 and appendix 表3: the space at the line end goes first and costs the paragraph nothing, because it is invisible once that character lands there; then the word space, down to a quarter em; then a middle dot's quarters, then the half em before an opening bracket or after a closing bracket or comma, then the Japanese-to-Western quarter down to an eighth. The half em after a mid-line full stop marks the end of a sentence and is never reduced, and neither is the half em that keeps a line-head opening bracket off the edge.

JLReq puts the word space ahead of the line end; the composer swaps the two, because a stage here says how visible an adjustment is and the line end is the only space the reader cannot see at all.

The アキ at a line end is the one exception to spending a space in part. JLReq 3.1.9 admits the full アキ or ベタ組 and forbids anything between, so a line whose overflow is smaller than it leaves it whole and finds the difference elsewhere — or breaks elsewhere. A 中点類 at a line end gives up the quarter em before it together with the quarter after, as one amount.

Every stage is spent 文字サイズ比で均等に, as JLReq 3.8.3 and 3.8.4 both put it: the amount is split across all the spaces of that stage in proportion to what each of them can give, rather than taken out of the earliest space until it runs dry. A stage opens only once every stage before it is spent out.

JLReq 3.1.5 states the white before a line-head opening bracket as a pair of amounts: one for 改行行頭, the head of a line that starts a paragraph, and one for 折返し行頭, the head of a line the composer turned over. The specification lists three such pairs, and the composer takes the one Japanese novels are set with — scheme ③: a half em at 改行行頭, and 天付き, flush against the edge, at 折返し行頭.

The composer also owns Japanese line-start and line-end restrictions, inseparable punctuation, question/exclamation gap suppression, and ruby placement.

The composer classifies vertical text before measuring and breaking lines. Following JLReq, upright Latin initials and abbreviations advance by one em per character, Western words remain unbroken and render sideways, and two ASCII digits form one tate-chu-yoko unit. Each render unit carries its selected presentation as a string. A CombinedGlyphUnit renders the two digits together; the composer owns that decision, so renderers do not compare source group ranges.

ASCII sentences are recognized before individual words are classified. A continuous stretch of ASCII letters, digits, punctuation, and spaces containing a letter and multiple alphanumeric groups separated by spaces or punctuation renders sideways. Japanese characters, fullwidth spaces, and newlines end that context. Thus I love you, This is a Web novel, I'm happy!, and STR 10 keep their initials, abbreviations, numbers, and punctuation sideways. Standalone I, BGM, and Web retain their upright presentation, and standalone 12 still forms a tate-chu-yoko unit. Standalone dotted abbreviations whose parts match the upright abbreviation rule, such as U.S. and Ph.D., keep their letters upright and their periods in the existing mixed presentation. In a sentence such as I live in the U.S., the abbreviation and its periods render sideways with the surrounding words.

Sentences can wrap between words. Words, including attached punctuation, apostrophes, and hyphens, stay together; ASCII spaces retain their source positions and supply line-break opportunities. Ruby and inline styling do not interrupt sentence recognition. Non-ASCII Western text keeps the existing presentation rules.

An upright Western character sets solid against the kana or kanji beside it rather than taking the quarter em 3.2.6 puts around a sideways run, following JLReq 3.2.4: it classes with kanji, the same way 3.2.5 classes a tate-chu-yoko run. QR and URL set upright by this rule read flush against their surrounding Japanese text, the way a kanji compound does.

The default logicalRunMeasurer uses East Asian Width in a Japanese context. Upright ASCII advances by one em; sideways ASCII and tate-chu-yoko members advance by half an em. Measurement is synchronous and DOM-independent.

Supply a RunMeasurer through the plugin entry when metrics are already available:

import { createNovelComposer, logicalRunMeasurer } from "@sushichan044/kg-core/plugin";

const composer = createNovelComposer({ measurer: logicalRunMeasurer });

MeasurementRequest includes text, kind (base or ruby), presentation, fontPreset, body fontSizeQ, writingMode, and scale. A provider returns measurements in body em and applies scale once: 1 for base text, 0.5 for ruby. Measured widths exclude punctuation box adjustments, pair spacing, hanging, and ruby expansion.

RunMeasurement has two variants:

  • { kind: "advance", advanceEm } supplies aggregate width. The composer retains per-grapheme measurement for positioning; it never divides a nonadditive run width equally among its members. Complete reading widths still inform base widening.
  • { kind: "clustered", advanceEm, clusters } supplies authoritative cluster positions. Each MeasuredCluster has a request-local textRange, layoutSpan, and renderSpan. Use MeasurementTextRange.of({ start, end }) to construct the branded UTF-16 range. Clusters partition text at grapheme boundaries and layout advance within 1e-9; render spans can overlap and overhang. The composer does not remeasure cluster members or break a line inside a cluster.

A multi-grapheme sideways cluster, such as a shaped ffi ligature, is drawn in a combined run with shared source positions. Upright QR and URL are orientation choices; they do not by themselves imply a shared cluster. The combined-unit contract supports sideways text and tate-chu-yoko. A provider result that cannot be represented by that contract is refused rather than split or approximated. The logical provider returns advance; a real shaped provider and matching font realization remain later work.

Tate-chu-yoko retains the sum of member advances, including custom widths. The composer does not silently rescale a custom measurement to one em. Negative or non-finite advances and invalid cluster mappings produce a typed composition failure rather than a partial layout.

Ruby annotations retain one of three associations: group for one reading over the entire base, mono for one reading segment per base grapheme, and jukugo for a compound whose segments remain associated with each base grapheme. A parser result with mismatched mono or jukugo segment counts is invalid.

Extensions and validation

Everything in this section comes from @sushichan044/kg-core/plugin, except proofreading rules, which are authored against @sushichan044/kg-core/lint alongside the built-in ones.

Parsers implement ManuscriptParser. Composers implement ManuscriptComposer and provide Valibot schemas for their settings and layout. Proofreading rules carry kind: "parsed" or kind: "composed" to declare which manuscript they need, and report findings through context.report. A report may name a severity; omitting it means error.

A plugin signals its own failure with a Rejection — a plain reason string. Core turns that into a variant of the stage's error union, so plugins never construct core's error types.

Each concept lives in its own module as a type plus a companion object of the same name, holding its schema and operations: ManuscriptRange.merge, PaperSize.of, NovelCompositionSettings.defaults, and so on. Types are inferred from the schemas with v.InferOutput; source, display, and grapheme ranges are distinct branded types, and plugin IDs are branded NamespacedId.

A companion object is exported as a value only when one of the three roles calls something on it. Concepts you only ever read out of a composed layout — NovelPage, SingleGlyphUnit, CombinedGlyphUnit, LineBreakResult, and the like — are exported as types alone, so their schemas are not part of the API. The settings you persist and re-validate keep theirs: NovelCompositionSettings.schema, ManuscriptAppearanceSettings.schema, ManuscriptOffsets.schema, NovelFlowSettings.schema, and the appearance picklists.

Processing functions return ManuscriptResult: success carries warnings, and failure carries exactly one error from a discriminated union, so callers can switch on error.kind exhaustively. Each error variant exposes its context as typed fields; describe renders one for display. Invalid settings and plugin output fail explicitly; values are never clamped or partially trusted.

Physical page settings

NovelCompositionSettings combines flow, appearance, page, and offsets. appearance.fontSizeQ specifies the body size in Q (1Q = 0.25mm). ManuscriptPageSettings specifies lineGapMm, stageGapMm, topMm, bottomMm, innerMm (binding edge), and outerMm (fore edge). Line gap is the space between body cells; line pitch is fontSizeQ / 4 + lineGapMm.

A5 is fixed at 12.7Q, 27 characters per line, 23 lines per stage, and two stages. Its line gap is 1.825mm, stage gap 10mm, top margin 12mm, bottom margin 16.55mm, inner margin 21.825mm, and outer margin 13mm. NovelCompositionSettings.defaults contains this format. Typeface and reserved lines remain configurable.

A6 is fixed at 12Q, 41 characters per line, 17 lines, and one stage. Its line gap is 7H (1.75mm), giving a 19H (4.75mm) line pitch, with 12.5mm top/bottom margins, a 16mm inner margin, and a 10mm outer margin.

NovelCompositionSettings.forPaper(paperSize, settings) selects the fixed A5 or A6 format or centres the current grid on another paper size. For other formats, ManuscriptPageSettings.centered(flow, appearance) computes millimetre settings with half-cell line gaps and two-cell stage gaps. These helpers calculate values; validate the result before persisting or rendering an oversized grid.

Custom page settings on other paper sizes can provide explicit millimetre gaps and margins. The settings schema checks that text dimensions plus margins equal the paper size within 0.000001mm. Gaps and margins must be nonnegative, and the line gap must accommodate the existing half-size annotation area. A mismatch returns InvalidSettings through composeManuscript, including unused space:

height = stages × charactersPerLine × (fontSizeQ / 4) + (stages - 1) × stageGapMm
width  = linesPerStage × (fontSizeQ / 4) + (linesPerStage - 1) × lineGapMm

The public settings and measurement contracts use Q and millimetres only for physical sizes. Body-relative Em coordinates and flow counts remain logical units. Point-based settings and previous persisted preference versions have no adapter or migration.

Source mapping and render units

Source and display ranges are zero-based, end-exclusive UTF-16 offsets. Grapheme ranges index the normalized grapheme array. Measurement text ranges are local to a request and have a different brand.

NovelLine.items contains PositionedInlineItem: single glyphs, combined glyphs, glue, kern, and suppressed source text. Render units have distinct layoutSpan and renderSpan values plus sources. An exact source placement locates one parsed grapheme; a shared placement associates several graphemes with one span without inventing interior caret positions. Default tate-chu-yoko retains exact half-em positions for each digit. A diagnostic on a shared member highlights the shared span while keeping the diagnostic's original range.

Hanging glyphs have zero layout advance and a nonzero render span. Authored glue and suppressed separators keep their source ranges, including at line edges. NovelLine.range includes all source-backed members. Generated spacing has no source range.

Ruby fragments contain readingItems, each with a fragment-local textRange and AnnotationPlacement. Emphasis fragments contain placements. Inline positions are line-relative; block offsets start at the body's right edge and increase leftward. The current right-side, half-size decorations use side: "before", blockOffsetEm: -0.5, and blockSizeEm: 0.5. The viewer consumes these coordinates without computing ruby or emphasis placement.

Annotations retain their original logical association even on part of a combined unit, whose whole physical span anchors the decoration. Incompatible readings on members of the same unit cause a composition refusal. Bold and italic retain source-range fragments. Ruby overhang depends on adjacent characters, resolved punctuation spacing, and the candidate's line edges. Mono readings anchor to their own bases; jukugo readings use solid shoulder placement and joint arrangement. Jukugo placement tries compound-interior overhang before outside neighbours, then distributes required spacing around long readings in proportion to their measured widths. Line-edge shares go inward. A single-base fragment touching both edges uses head alignment and box expansion when needed. Oversized group readings are assigned as contiguous measurement-cluster intervals before line scoring.

Emphasis placements have a half-em inline extent centered on their render unit. Brackets, commas, and full stops receive no marks. Ruby and nonempty emphasis must fit the fixed half-em right-side area without colliding. Core may choose another line break; an impossible combination returns ComposerRejected with a reason. Annotation placement preserves body pitch and source ranges. Actual font shaping, selectable sides, sidelines, and mixed sizes remain deferred.

Diagnostics include one-based source line and column positions, so renderers do not need to search or recalculate locations.

Breaking API migration

Update core and viewer together. SingleGlyphUnit, CombinedGlyphUnit, and PositionedInlineItem replace ComposedGlyph and ComposedInlineItem; PresentationKind replaces the VerticalTextPresentation group object. The plugin entry replaces InlineMeasureRequest, InlineMeasurement, InlineMeasurer, and logicalInlineMeasurer with the run measurement contracts above. createNovelComposer({ measurer }) keeps its option name but requires the new request and discriminated result. Ruby readingGraphemes becomes readingItems; decoration positions move into core output. Parser/proofreading entrypoints remain unchanged. Previous persisted settings are ignored without migration.

Proofreading rules

Proofreading has its own entry point, @sushichan044/kg-core/lint. Nothing in the root entry imports from it, so an application that only parses and composes never pulls the rules into its bundle.

createDefaultProofreadingRules() returns the rules whose answer does not depend on the work: how a paragraph opens and how far it is indented, punctuation before a closing bracket, spacing after ! and ?, ellipsis and dash forms and counts, repeated punctuation and interpuncts, a minus sign that no number follows, halfwidth Japanese punctuation, Arabic numeral length, and Unicode variation sequences.

Rules that depend on the work's own conventions are exported individually and report warning instead of error, so a caller opts into them:

import {
  consistentKanjiOpeningRule,
  consistentLatinWidthRule,
  consistentNumeralWidthRule,
  createConsistentKanjiOpeningRule,
  createDefaultProofreadingRules,
} from "@sushichan044/kg-core/lint";

const rules = [
  ...createDefaultProofreadingRules(),
  consistentNumeralWidthRule(),
  consistentLatinWidthRule(),
  consistentKanjiOpeningRule(),
];

createConsistentKanjiOpeningRule({ pairs }) replaces the built-in kanji and kana pairs; like every configurable rule it returns a ManuscriptResult and fails with InvalidRuleOptions rather than dropping bad options.

dashRule() prefers ― (U+2015). Use createDashRule({ preferred: "—" }) or createDashRule({ preferred: "─" }) when a work uses U+2014 or U+2500 instead. The selected character must appear in even-length runs. Repeated choonpu (ー) are not treated as dashes.

Diagnostics do not modify the source and do not contain automatic fixes.

Browser support

The package targets browser features that are Baseline Widely Available. It does not include polyfills.

License

MIT

Sentence-ending symbols

♡, ♪, and ☆ follow the same line-breaking and separator-space rules as Japanese question and exclamation marks. Each symbol remains a separate cell. Set additionalSentenceEndingMarks on composition settings to add Unicode symbols:

const settings = {
  ...NovelCompositionSettings.defaults,
  additionalSentenceEndingMarks: "★",
};

For proofreading, import createSpaceAfterQuestionOrExclamationRule from @sushichan044/kg-core/lint and pass the same option. The factory returns a ManuscriptResult. Replace the default rule with ID kg/space-after-question-or-exclamation with the successful result's value. An empty string adds nothing; built-in marks remain enabled. Each Unicode code point is a separate symbol. Non-symbol characters are rejected.