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.8.0

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 Western character, and solid where two of those punctuation marks meet.

An overflowing line is answered by reducing that space 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 Japanese-to-Western quarter em first, up to a half, then a solid pair between kana, kanji, and the marks that go with them, up to a quarter. Space around brackets, commas, full stops, middle dots, hyphens, and an ideographic space is never opened up. A line that admits none of these 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 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.

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. The resulting VerticalTextPresentation is carried by every glyph item, so renderers do not need to infer orientation independently.

The default logical measurer uses East Asian Width in a Japanese context. Upright ASCII advances by one em; proportional ASCII and the members of a tate-chu-yoko unit advance by half an em. Supply a synchronous measurer when the caller has more accurate font metrics. Base-text requests also include the selected presentation:

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

const composer = createNovelComposer({
  measurer: (request) => ({
    advanceEm: measureWithAvailableFont(request.text, {
      role: request.role,
      fontPreset: request.fontPreset,
      writingMode: request.writingMode,
      ...(request.role === "base" ? { presentation: request.presentation } : {}),
    }),
  }),
});

A measurer returns { advanceEm } in logical em units. Negative or non-finite results reject the composition instead of producing 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, ComposedGlyph, 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.

Source mapping

Source and display ranges are zero-based, end-exclusive UTF-16 offsets. Grapheme ranges index the normalized grapheme array. Diagnostics include one-based source line and column positions, so renderers do not need to search or recalculate locations.

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