Skip to content
iyulabPublic

About

High-performance Rust library for extracting content from DOCX, XLSX, and PPTX into Markdown, plain text, or JSON with CJK support and .NET/Python bindings.

Topics

Resources

Security policy

Stars

37 stars

Watchers

2 watching

Forks

Repository files navigation

undoc

Crates.io Documentation PyPI NuGet CI License: MIT

A high-performance Rust library for extracting content from Microsoft Office documents (DOCX, XLSX, PPTX, and the 97-2003 DOC, XLS and PPT) to Markdown, plain text, and JSON.

Features

  • Multi-format support: DOCX (Word), XLSX (Excel), PPTX (PowerPoint), and the 97-2003 binary formats DOC, XLS and PPT — paragraphs, headings, tables, lists, notes and hyperlinks from DOC; every worksheet's cells, dates, links and comments from XLS; every slide's text, links and notes from PPT (encrypted files report Encrypted). Word 6.0/95 .doc files are read for their text — paragraphs, headings, tables, character formatting, breaks and field results, in the document's code page — without notes, headers or pictures. XLS covers every binary Excel version: 97-2003, 5.0/95 and the 2.x–4.0 files, reading pre-97 text in the workbook's code page
  • Multiple output formats: Markdown, Plain Text, JSON (with full metadata)
  • Structure preservation: Headings, lists, tables, inline formatting
  • Smart heading detection: Style-based heading recognition (English/Korean)
  • Table cell alignment: Proper left/center/right alignment in Markdown
  • Merged cells keep their columns: a merged cell renders at the column it starts in, with the columns it covers left empty — so grouped/multi-row headers stay above their own data instead of shifting
  • PPTX table extraction: Full table parsing from PowerPoint slides
  • CJK text support: Smart spacing for Korean, Chinese, Japanese content
  • Asset extraction: Images, charts, and embedded media with resolved paths (XLSX drawings included)
  • Rich content: Footnotes/endnotes, headers/footers, text boxes, cell comments, hyperlinks
  • Section markers: <!-- slide N: Name --> / <!-- sheet N: Name --> boundary markers for PPTX/XLSX
  • Text cleanup: Multiple presets for LLM training data preparation
  • Self-update: Built-in update mechanism via GitHub releases
  • Slide rendering (feature raster): a PPTX slide painted to PNG — shapes from the ECMA-376 preset definitions, connectors, fills and lines in the theme's colors, pictures and text — with a count, by kind, of what it could not paint
  • C-ABI FFI: Native library for C#, Python, and other languages
  • Parallel processing: Uses Rayon for multi-section documents
  • Streaming pipeline (0.3.0+): parse_file_streaming yields sections as they parse; peak memory bounded regardless of document size
  • WebAssembly (0.4.0+): @iyulab/undoc npm package — parse Office documents in the browser, no server required

Table of Contents


WASM / Browser

Parse Office documents in the browser — no server, no upload:

import { parse } from '@iyulab/undoc';
const doc = parse(new Uint8Array(await file.arrayBuffer()));
console.log(doc.format());      // "docx" | "xlsx" | "pptx"
console.log(doc.toMarkdown());

Install: npm install @iyulab/undoc

Live Playground →


Installation

Pre-built Binaries (Recommended)

Download the latest release from GitHub Releases.

Windows (x64)

# Download and extract (replace VERSION with the actual version, e.g. v0.3.1)
$VERSION = (Invoke-RestMethod "https://api.github.com/repos/iyulab/undoc/releases/latest").tag_name
Invoke-WebRequest -Uri "https://github.com/iyulab/undoc/releases/latest/download/undoc-windows-x86_64-${VERSION}.zip" -OutFile "undoc.zip"
Expand-Archive -Path "undoc.zip" -DestinationPath "."

# Move to a directory in PATH (optional)
Move-Item -Path "undoc.exe" -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\"

# Verify installation
undoc version

Linux (x64)

# Download and extract (replace VERSION with the actual version, e.g. v0.3.1)
VERSION=$(curl -s "https://api.github.com/repos/iyulab/undoc/releases/latest" | grep '"tag_name"' | cut -d'"' -f4)
curl -LO "https://github.com/iyulab/undoc/releases/latest/download/undoc-linux-x86_64-${VERSION}.tar.gz"
tar -xzf "undoc-linux-x86_64-${VERSION}.tar.gz"

# Install to /usr/local/bin (requires sudo)
sudo mv undoc /usr/local/bin/

# Or install to user directory
mkdir -p ~/.local/bin
mv undoc ~/.local/bin/
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Verify installation
undoc version

macOS

# Intel Mac (replace VERSION with the actual version, e.g. v0.3.1)
VERSION=$(curl -s "https://api.github.com/repos/iyulab/undoc/releases/latest" | grep '"tag_name"' | cut -d'"' -f4)
curl -LO "https://github.com/iyulab/undoc/releases/latest/download/undoc-macos-x86_64-${VERSION}.tar.gz"
tar -xzf "undoc-macos-x86_64-${VERSION}.tar.gz"

# Apple Silicon (M1/M2/M3/M4)
curl -LO "https://github.com/iyulab/undoc/releases/latest/download/undoc-macos-aarch64-${VERSION}.tar.gz"
tar -xzf "undoc-macos-aarch64-${VERSION}.tar.gz"

# Install
sudo mv undoc /usr/local/bin/

# Verify
undoc version

Available Binaries

Platform Architecture File
Windows x64 undoc-windows-x86_64-{version}.zip
Linux x64 undoc-linux-x86_64-{version}.tar.gz
macOS Intel undoc-macos-x86_64-{version}.tar.gz
macOS Apple Silicon undoc-macos-aarch64-{version}.tar.gz

Updating

undoc includes a built-in self-update mechanism:

# Check for updates
undoc update --check

# Update to latest version
undoc update

# Force reinstall (even if on latest)
undoc update --force

Network access. Apart from undoc update, the only request the CLI makes on its own is the update notice: at most once a day it asks the GitHub releases API (api.github.com/repos/iyulab/undoc/releases) whether a newer version exists, and caches the answer in your user cache directory, so the other runs make no request and wait for nothing. It is not made at all when --no-update-check is passed, when UNDOC_NO_UPDATE_CHECK=1 (or UNPARSER_NO_UPDATE_CHECK=1, for every tool of the family) is set, in CI (CI set), or when stderr is not a terminal (scripts, batch jobs, redirected runs).

Install via Cargo

If you have Rust installed:

# Install CLI
cargo install undoc-cli

# Add library to your project
cargo add undoc

CLI Usage

Quick Start

# Convert to Markdown + extract media (default)
undoc document.docx

# Specify output directory
undoc document.docx ./output

# With text cleanup for LLM training
undoc document.docx --cleanup aggressive

Output Structure

document_docx_output/
├── extract.md      # Markdown output
├── images/         # Extracted images
│   └── image1.jpeg
└── media/          # Other embedded media (if any)

Use undoc convert <file> --all to produce all three formats at once:

document_docx_output/
├── extract.md      # Markdown output
├── extract.txt     # Plain text output
├── content.json    # Full structured JSON
└── media/

Commands

undoc <file> [output]              # Convert to Markdown + extract media (default)
undoc convert <file> [OPTIONS]     # Convert with full format/streaming control
undoc markdown <file> [OPTIONS]    # Convert to Markdown only (alias: md)
undoc text <file> [OPTIONS]        # Convert to plain text only
undoc json <file> [OPTIONS]        # Convert to JSON only
undoc info <file>                  # Show document information
undoc extract <file> [OPTIONS]     # Extract resources only
undoc tables <file> [OPTIONS]      # Extract tables as CSV
undoc update [OPTIONS]             # Self-update to latest version
undoc version                      # Show version information

Convert (multi-format streaming pipeline)

# Markdown only (default)
undoc convert document.docx

# All formats + media
undoc convert document.docx --all -o ./output

# Specific formats
undoc convert document.docx --formats md,txt,json

# Skip media extraction
undoc convert document.docx --no-images

# Lossless mode (page breaks + headers/footers)
undoc convert document.docx --lossless

# Insert section boundary markers for PPTX/XLSX
undoc convert presentation.pptx --section-markers

Convert Options

Option Description Default
-o, --output Output directory <stem>_<ext>_output/ next to the input
--formats Comma-separated formats: md,txt,json md
--all Output all formats (MD + TXT + JSON) false
--no-images Skip media extraction false
--cleanup Text cleanup: minimal, standard, aggressive none
--section-markers Insert <!-- slide/sheet N: Name --> markers (PPTX/XLSX) false
--emit-page-breaks Emit --- for hard page breaks false
--include-headers-footers Include section headers/footers as blockquotes false
--lossless Enable both --emit-page-breaks and --include-headers-footers false
-q, --quiet Suppress progress output false

Convert to Markdown

# Basic conversion (output to stdout)
undoc markdown document.docx

# Save to file
undoc markdown document.docx -o output.md

# With YAML frontmatter
undoc markdown document.docx --frontmatter -o output.md

# With text cleanup for LLM training
undoc markdown document.docx --cleanup standard -o cleaned.md

# Table rendering options
undoc markdown spreadsheet.xlsx --table-mode html -o output.md
# `markdown` keeps a merged cell's column position but flattens the merge itself —
# Markdown has no colspan. Use `html` when the merge has to survive.

# Limit heading depth
undoc markdown document.docx --max-heading 3 -o output.md

# Insert section boundary markers for PPTX/XLSX
undoc markdown presentation.pptx --section-markers -o slides.md

# Lossless mode (preserve page breaks and headers/footers)
undoc markdown document.docx --lossless -o output.md

Markdown Options

Option Description Default
-o, --output Output file path stdout
-f, --frontmatter Include YAML frontmatter false
--table-mode Table rendering: markdown, html markdown
--cleanup Text cleanup: minimal, standard, aggressive none
--max-heading Maximum heading level (1-6) 4
--section-markers Insert <!-- slide/sheet N: Name --> markers (PPTX/XLSX) false
--emit-page-breaks Emit --- for hard page breaks false
--include-headers-footers Include section headers/footers as blockquotes false
--lossless Enable both --emit-page-breaks and --include-headers-footers false

Convert to Plain Text

# Basic extraction
undoc text document.docx

# With cleanup
undoc text document.docx --cleanup standard -o output.txt

Convert to JSON

# Pretty-printed JSON
undoc json document.docx -o output.json

# Compact JSON
undoc json document.docx --compact -o output.json

Show Document Information

undoc info document.docx

Output:

Document Information
────────────────────────────────────────
File: document.docx
Format: Docx
Sections: 5
Resources: 3
Title: My Document
Author: John Doe
Pages/Slides/Sheets: 10
Created: 2025-01-15T10:30:00Z
Modified: 2025-01-20T14:45:00Z

Content Statistics
────────────────────────────────────────
Words: 2500
Characters: 15000

Extract Resources

# Extract to current directory
undoc extract presentation.pptx

# Extract to specific directory
undoc extract presentation.pptx -o ./media

Extract Tables as CSV

# Every table to standard output, a blank line between two
undoc tables workbook.xlsx > tables.csv

# One file per table, named for its section (sheet, slide) and place: s1-t1.csv, s2-t1.csv, ...
undoc tables workbook.xlsx -o ./tables

# Tab-separated instead (files named .tsv)
undoc tables report.docx --tsv -o ./tables

The output is RFC 4180 CSV: a field holding a comma, a quote or a line break is quoted. A merged cell's text is in its top-left position and the positions it covers are empty, so every row has the same number of fields. A table nested in a cell is written as a table of its own, right after the one that holds it. From Rust, the same comes from Table::to_csv().

Self-Update

# Check for updates
undoc update --check

# Update to latest version
undoc update

# Force reinstall
undoc update --force

Examples

# Convert Word document to Markdown with frontmatter
undoc md report.docx --frontmatter -o report.md

# Convert Excel to Markdown tables
undoc md data.xlsx -o tables.md

# Convert PowerPoint to Markdown with slide markers
undoc md presentation.pptx --section-markers -o slides.md

# Extract all images from a document
undoc extract report.docx -o ./images

# Get document metadata
undoc info document.docx

# Convert with aggressive cleanup for AI training
undoc md document.docx --cleanup aggressive -o cleaned.md

# Batch conversion (shell)
for f in *.docx; do undoc md "$f" -o "${f%.docx}.md"; done

# Batch conversion (PowerShell)
Get-ChildItem *.docx | ForEach-Object { undoc md $_.FullName -o "$($_.BaseName).md" }

Rust Library Usage

Quick Start

use undoc::{parse_file, render};

fn main() -> undoc::Result<()> {
    // Parse document
    let doc = parse_file("document.docx")?;

    // Convert to Markdown
    let options = render::RenderOptions::default();
    let markdown = render::to_markdown(&doc, &options)?;
    println!("{}", markdown);

    // Get plain text
    let text = render::to_text(&doc, &options)?;

    // Get JSON
    let json = render::to_json(&doc, render::JsonFormat::Pretty)?;

    Ok(())
}

Convenience Functions

// One-shot conversions without building a Document first
let text     = undoc::extract_text("document.docx")?;
let markdown = undoc::to_markdown("document.docx")?;
let json     = undoc::to_json("document.docx", undoc::render::JsonFormat::Pretty)?;

// Parse from bytes
let doc = undoc::parse_bytes(&file_bytes)?;

Error Handling Defaults

There is no error mode: a part that cannot be read is always an error. An OOXML package is a set of named parts, and a damaged one is a specific, identifiable thing — a slide layout, a document stream, a worksheet — rather than an unrecoverable region of a byte stream. Reporting it is therefore always possible, and a caller that receives a deck missing a slide has no way to notice on its own.

So undoc has a single contract in place of a mode: every function returns an error naming what failed, and a successful result is the whole document. A caller that wants to continue past a damaged file decides that for itself, at the level where it knows what the file is for:

match undoc::parse_file(path) {
    Ok(doc) => index(&doc),
    // The error says which part failed; the caller decides whether to skip the file.
    Err(e) => eprintln!("{}: {e}", path.display()),
}

Its sibling parsers do have a mode, because their formats leave less to name: unpdf defaults to lenient and undoc's strict-only behaviour matches unhwp's default. Code that drives all three should not assume a shared default.

Render Options

use undoc::render::{RenderOptions, CleanupPreset, TableFallback};
use undoc::SectionMarkerStyle;

let options = RenderOptions::new()
    .with_frontmatter(true)
    .with_table_fallback(TableFallback::Html)
    .with_cleanup_preset(CleanupPreset::Aggressive)
    .with_max_heading(3)
    .with_section_markers(SectionMarkerStyle::Comment); // <!-- slide N: Name -->

let markdown = render::to_markdown(&doc, &options)?;

// Lossless mode: preserve page breaks and headers/footers
let lossless = RenderOptions::lossless();
let markdown = render::to_markdown(&doc, &lossless)?;

Working with Document Structure

use undoc::parse_file;

let doc = parse_file("document.docx")?;

// Access metadata
println!("Title: {:?}", doc.metadata.title);
println!("Author: {:?}", doc.metadata.author);
println!("Created: {:?}", doc.metadata.created);

// Iterate sections
for section in &doc.sections {
    println!("Section: {:?}", section.name);
    for element in &section.elements {
        // Process paragraphs, tables, etc.
    }
}

// Extract resources
for (id, resource) in &doc.resources {
    let filename = resource.suggested_filename(id);
    std::fs::write(&filename, &resource.data)?;
}

A picture can reference more than one file. Office writes a raster rendering beside a picture's SVG original, and stores artistic effects as an HD Photo layer (.wdp). Each resource says which it is: role is Primary for an image the document shows (or media it plays), Alternate for another encoding of one, and Layer for a layer composited onto one; companion_of names the primary. To take the images the document shows, keep the primaries:

use undoc::ResourceRole;

let shown = doc.resources.iter().filter(|(_, r)| r.role == ResourceRole::Primary);

Rendering Slides

With the raster feature, a slide can be painted to an image — for a reader that needs what a slide looks like: the boxes, connectors and fills that carry a diagram slide's structure have no text of their own.

use undoc::pptx::PptxParser;
use undoc::raster::SlideRasterOptions;

let parser = PptxParser::open("deck.pptx")?;
let slide = parser.render_slide(0, &SlideRasterOptions::default())?; // 150 dpi
std::fs::write("slide1.png", slide.to_png())?;
if !slide.gaps.is_empty() {
    eprintln!("not painted: {:?}", slide.gaps); // charts, embedded objects, custom geometry, …
}
# Ok::<(), undoc::Error>(())

What the renderer cannot paint yet — charts, embedded objects and other graphic frames, custom geometry, pictures other than PNG and JPEG, text in a script that needs shaping — is left out and counted in gaps; the rest of the slide is painted. Fills drawn as a stand-in — a pattern in its foreground color, a rectangular or shape-following gradient as a radial one, a tiled picture stretched — are counted as approximated. No font is bundled. Text is drawn in the faces you pass, then those in the directories you name, then the system's. A Linux container without fonts draws no text — install a font package (Noto Sans CJK covers Latin and East Asian text) or pass a font directory — and reports the runs as gaps. An index the presentation does not have is ErrorKind::SectionOutOfRange.

Streaming Pipeline

Supported for PPTX (per slide) and XLSX (per sheet). DOCX is not yet supported.

use std::ops::ControlFlow;
use undoc::{parse_file_streaming, ParseEvent, SectionStreamOptions};

parse_file_streaming("slides.pptx", SectionStreamOptions::default(), |event| {
    match event {
        ParseEvent::DocumentStart { metadata, section_count, .. } => {
            println!("{} sections", section_count);
        }
        ParseEvent::SectionParsed(section) => {
            // section memory is freed after this callback returns
            println!("Slide: {:?}", section.name);
        }
        ParseEvent::ResourceExtracted { name, data } => {
            std::fs::write(&name, &data).ok();
        }
        _ => {}
    }
    ControlFlow::Continue(())
})?;

Format Detection

use undoc::{detect_format_from_path, detect_format_from_bytes, FormatType};

// From file path
let format = detect_format_from_path("document.docx")?;
assert_eq!(format, FormatType::Docx);

// From bytes
let data = std::fs::read("document.docx")?;
let format = detect_format_from_bytes(&data)?;

C# / .NET Integration

undoc ships a .NET package that bundles the native library for every supported platform.

dotnet add package Undoc

Usage

using Undoc;

// Parse and convert to Markdown
using var doc = UndocDocument.ParseFile("document.docx");
string markdown = doc.ToMarkdown(new MarkdownOptions { IncludeFrontmatter = true });
Console.WriteLine(markdown);

// Document metadata — null means "not set", not "failed"
Console.WriteLine($"Title: {doc.Title}");
Console.WriteLine($"Author: {doc.Author}");
Console.WriteLine($"Sections: {doc.SectionCount}");
Console.WriteLine($"Resources: {doc.ResourceCount}");

// Other output formats
string text = doc.ToText();
string json = doc.ToJson(compact: false);

// Resources
string[] resourceIds = doc.GetResourceIds();
JsonDocument? info = doc.GetResourceInfo("rId1");   // role: primary | alternate | layer
byte[]? imageData = doc.GetResourceData("rId1");

UndocDocument.ParseBytes(byte[]) parses content already in memory.

Rendering a slide

A slide of a .pptx renders to PNG from the parsed document, without reading the file again:

using var doc = UndocDocument.ParseFile("deck.pptx");
for (var i = 0; i < doc.SectionCount; i++)
{
    var slide = doc.RenderSection(i, new RenderSectionOptions { Dpi = 150 });
    File.WriteAllBytes($"slide{i + 1}.png", slide.Png);
    if (!slide.Gaps.IsEmpty)
        Console.WriteLine($"slide {i + 1}: {slide.Gaps.Charts} charts, {slide.Gaps.TextRuns} text runs not painted");
}

RenderSectionOptions.FontDirectories names directories of font files; SystemFonts = false leaves the system's out. A document that is not a presentation throws UnsupportedFormat.

Classifying failures

UndocException.Kind says why a call failed, so callers can react to the reason instead of matching on message text:

try
{
    using var doc = UndocDocument.ParseFile(path);
    Console.WriteLine(doc.ToMarkdown());
}
catch (UndocException ex)
{
    switch (ex.Kind)
    {
        case UndocErrorKind.ZipArchive:
            Console.Error.WriteLine("The file is damaged.");
            break;
        case UndocErrorKind.UnknownFormat:
        case UndocErrorKind.UnsupportedFormat:
            Console.Error.WriteLine("Not a supported Office document.");
            break;
        case UndocErrorKind.Encrypted:
            Console.Error.WriteLine("The document is encrypted.");
            break;
        default:
            // Also the right branch for a reason this build has no name for.
            Console.Error.WriteLine($"Extraction failed ({ex.Kind}): {ex.Message}");
            break;
    }
}

The kind numbers are a stable ABI contract: a new failure reason takes the next free number and existing ones are never renumbered. Always keep a default branch — treat an unrecognised value as a generic failure so a newer library stays usable. Kind is Other for failures raised by the wrapper itself, and never None (which means success).

Using the native library directly

Download from GitHub Releases:

Platform Archive Library
Windows x64 libundoc-windows-x86_64-v<version>.zip undoc.dll
Linux x64 (glibc) libundoc-linux-x86_64-v<version>.tar.gz libundoc.so
Linux x64 (musl) libundoc-linux-x86_64-musl-v<version>.tar.gz libundoc.so
macOS x64 libundoc-macos-x86_64-v<version>.tar.gz libundoc.dylib
macOS arm64 libundoc-macos-aarch64-v<version>.tar.gz libundoc.dylib

Each archive holds the library and its C header, undoc.h.

Or build from source:

cargo build --release --features ffi

The C API is declared in include/undoc.h. Failures return NULL (or -1); call undoc_last_error() for the message and undoc_last_error_kind() for the UndocErrorKind classification.


Output Formats

Markdown

Structured Markdown with preserved formatting:

  • Headings: Document headings → #, ##, ###
  • Lists: Ordered and unordered with nesting
  • Tables: Markdown tables (with an HTML fallback for merged cells)
  • Inline styles: Bold (**), italic (*), underline (<u>), strikethrough, superscript/subscript
  • Hyperlinks: Preserved as Markdown links (DOCX, XLSX, PPTX)
  • Images: Linked image references from document drawings
  • Footnotes: Markdown reference-style ([^N] / [^N]: text)
  • Headers/Footers: Rendered as blockquotes (opt-in via --include-headers-footers)

Plain Text

Pure text content without formatting markers.

JSON

Complete document structure with metadata:

{
  "metadata": {
    "title": "Document Title",
    "author": "Author Name",
    "created": "2025-01-15T10:30:00Z",
    "modified": "2025-01-20T14:45:00Z"
  },
  "sections": [...],
  "resources": [...]
}

Supported Formats

Format Extension Status
Word .docx Supported
Excel .xlsx Supported
PowerPoint .pptx Supported
Word 97-2003 .doc Supported (Word 6.0/95: without notes, headers, pictures)
Excel binary .xls Supported — Excel 97-2003, 5.0/95 and 2.x–4.0
PowerPoint 97-2003 .ppt Supported

Feature Flags

Feature Description Default
docx Word document support Yes
xlsx Excel workbook support Yes
pptx PowerPoint presentation support Yes
doc Word 97-2003 document support Yes
xls Excel binary workbook support (97-2003, 5.0/95, 2.x–4.0) Yes
ppt PowerPoint 97-2003 presentation support Yes
codepages Decode legacy text in any Windows code page (East Asian, Cyrillic, …); without it, only Windows-1252, UTF-8/16 and ASCII Yes
refine Markdown shape-refinement pass (RenderOptions::refine) Yes
raster Slide rendering to PNG (PptxParser::render_slide) — pure Rust (tiny-skia, skrifa); no bundled fonts No
ffi C-ABI foreign function interface (includes raster: undoc_render_section) No
async Async I/O with Tokio No
cargo add undoc --features ffi

To take only one format, or to drop the refinement pass and the CommonMark parser it pulls in:

cargo add undoc --no-default-features --features docx

Benchmarks

On a DOCX edition of opendataloader-bench (200 real documents typeset as Word, official evaluator), undoc 0.16.0 scores 0.993 overall. What that measures and how to reproduce: docs/BENCHMARKS.md.

Performance

  • Parallel section/sheet/slide processing with Rayon
  • Efficient XML parsing with quick-xml
  • Memory-efficient handling of large documents
  • Streaming pipeline for bounded peak memory on large files

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Related Projects

  • unhwp - Korean HWP document extraction
  • unpdf - PDF document extraction

About

High-performance Rust library for extracting content from DOCX, XLSX, and PPTX into Markdown, plain text, or JSON with CJK support and .NET/Python bindings.

Topics

Resources

Security policy

Stars

37 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages