PubliraGitHub

Pieces you can take on their own

Each is Apache-2.0, each is used by Publira in production paths, and none asks you to adopt the platform to use it.

@publira/comic-viewer

The React comic viewer that renders the episode screen, extracted so the UI and the data pipeline stay entirely yours.

  • Headless UI architecture — compose independent, tree-shakeable components and style every one of them yourself.
  • Responsive spreads that switch between single and double pages as the container width changes.
  • Virtualization that holds large volumes of high-resolution pages without crashing a mobile browser.
  • A pluggable data pipeline: inject fetching, decryption, or analytics through definePlugin().
  • Right-to-left by default for manga, left-to-right for Western comics, with swipe, click, and keyboard navigation built in.
  • Lazy page metadata through resolvePage(), for long documents and expiring signed URLs.

Pre-1.0: within a 0.x line, patch releases aim to preserve compatibility, while a minor release may change the public API. Read the changelog before upgrading.

Read the documentation
Install
npm install @publira/comic-viewer
app/reader.tsx
import * as ComicViewer from "@publira/comic-viewer";
import "@publira/comic-viewer/core.css";

export function Reader({ pages }: ReaderProps) {
  return (
    <ComicViewer.Root
      initialReadingDirection="rtl"
      pages={pages}
    >
      <ComicViewer.Viewport />
      <ComicViewer.Toolbar />
      <ComicViewer.PageNavigation />
    </ComicViewer.Root>
  );
}

publira/epub

A Go library for decoding, encoding, and validating EPUB 3, built for servers that never see a file on disk.

  • Filesystem-independent: Decode takes an io.ReaderAt plus the archive size, and nothing else.
  • Fail-fast — a structural violation comes back as a DecodeError immediately.
  • Memory efficient, with on-demand stream access through Asset.Open.
  • Strict validation of EBPAJ and KADOKAWA-style naming and directory rules.
  • Fixed-layout image pages wrapped in SVG, so page-fit survives the reading system.
  • Structural semantics for cover, table of contents, bodymatter, index, and glossary landmarks.

Generated publications can be checked against the official W3C EPUBCheck through the script the repository ships, which caches the tool under .tools/.

Read the documentation
Install
go get github.com/publira/epub
main.go
f, err := os.Open("book.epub")
if err != nil {
	log.Fatal(err)
}
defer f.Close()

st, err := f.Stat()
if err != nil {
	log.Fatal(err)
}

opt := epub.WithCompliance(epub.LevelEBPAJ)
doc, err := epub.Decode(f, st.Size(), opt)
if err != nil {
	log.Fatal(err)
}

if err := epub.Encode(out, doc); err != nil {
	log.Fatal(err)
}

publira/messageformat-dart

A pure-Dart implementation of Unicode MessageFormat 2.0, conformant to LDML 48.2, that formats the messages of the mobile client.

  • Pure Dart with no dependency on Flutter, so it runs on a server, on the command line, and in a Flutter app alike.
  • All 461 cases of the MessageFormat Working Group's conformance suite at LDML48.2 pass, none skipped, on every change in CI.
  • Every function the specification marks Stable — :string, :number, :integer, :offset, :currency, and :percent — with numbers formatted in every CLDR locale.
  • The Draft date and time functions live in messageformat_datetime, so an app that formats no dates does not ship their CLDR data.
  • formatToParts returns a sealed hierarchy of parts, for output such as styled text.
  • Formatting reports errors to a callback and falls back instead of throwing, and custom functions can be registered alongside the defaults.

Pre-1.0. It follows LDML 48.2 (UTS #35 Part 9, tr35-78) with Unicode CLDR 48.2 data, and a move to a later version of the specification is a release of its own.

Read the documentation
Install
dart pub add messageformat
main.dart
import 'package:messageformat/messageformat.dart';

final mf = MessageFormat(
  'en',
  r'''
.input {$count :number}
.match $count
0   {{No episodes}}
one {{{$count} episode}}
*   {{{$count} episodes}}
''',
  options: const MessageFormatOptions(
    bidiIsolation: BidiIsolation.none,
  ),
);

mf.format({'count': 0}); // 'No episodes'
mf.format({'count': 1}); // '1 episode'
mf.format({'count': 1000}); // '1,000 episodes'