anydoc 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ad2ef90ad06d780f64b4d8ffc6e7881654ede6984587679b90b746450ec6c509
4
+ data.tar.gz: 9df934da5ab5c6110ced6e23eb63b6f8d2972fe685460b39d83382abc20deb68
5
+ SHA512:
6
+ metadata.gz: 22e930d99a40fb5117befacf5e9e4af541c1696eb8eb925a6d92e02a13e46324253e4f65edfd4473f3df7f0fc5e3e1c7452646b80791d46fc85b688a407de8c4
7
+ data.tar.gz: 78f1a749bbab09c9c68f9d1e08f6d8aac41c3ca4d2b5ec6e9ffb72359a5e5bde6dece98b2be284f0fbfa2269c218e76b018b1f51f6a345fd879f9bfdde00b328
data/Cargo.toml ADDED
@@ -0,0 +1,11 @@
1
+ # This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
2
+ # a Rust project. Your extensions dependencies should be added to the Cargo.toml
3
+ # in the ext/ directory.
4
+
5
+ [workspace]
6
+ members = ["./ext/anydoc"]
7
+ resolver = "2"
8
+
9
+ [profile.release]
10
+ lto = "thin"
11
+ strip = "symbols"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Nick Pezza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # anydoc
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/anydoc.svg)](https://rubygems.org/gems/anydoc)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/firecrawl/anydoc/blob/main/LICENSE)
5
+
6
+ Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF files into clean GitHub-Flavored Markdown. Ruby bindings for the [anydoc](https://github.com/firecrawl/anydoc) Rust crate, built by [Firecrawl](https://firecrawl.dev). Also available as a hosted API through [Firecrawl Parse](https://firecrawl.dev/parse), which adds OCR for scanned pages anydoc cannot read on its own.
7
+
8
+ Every format parses into one shared document model and renders through a single Markdown serializer, so headings, tables, lists, and footnotes come out consistently. Conversion runs without holding Ruby's global VM lock, and RBS signatures ship with the gem.
9
+
10
+ ```bash
11
+ gem install anydoc
12
+ ```
13
+
14
+ Or add it with Bundler:
15
+
16
+ ```bash
17
+ bundle add anydoc
18
+ ```
19
+
20
+ ## Supported formats
21
+
22
+ | Format | Extensions |
23
+ | ---------------- | ---------------------------------------------------------- |
24
+ | Word | `.doc`, `.docx`, `.docm` |
25
+ | PowerPoint | `.ppt`, `.pps`, `.pot`, `.pptx`, `.pptm`, `.ppsx`, `.ppsm` |
26
+ | Excel | `.xls`, `.xlsx`, `.xlsm`, `.xlsb` |
27
+ | OpenDocument | `.odt`, `.ods`, `.odp` |
28
+ | Rich Text Format | `.rtf` |
29
+ | EPUB | `.epub` |
30
+ | CSV | `.csv` |
31
+ | PDF | `.pdf` |
32
+
33
+ ## Usage
34
+
35
+ ```ruby
36
+ require "anydoc"
37
+
38
+ # From a file path. Pathname and other #to_path objects are accepted.
39
+ markdown = Anydoc.to_markdown("report.docx")
40
+
41
+ # From bytes, with the format detected from the content.
42
+ markdown = Anydoc.to_markdown_bytes(data)
43
+
44
+ # Signature-less formats such as CSV need an explicit format.
45
+ markdown = Anydoc.to_markdown_bytes(data, :csv)
46
+
47
+ # Or stop at the immutable document model, which carries embedded assets.
48
+ document = Anydoc.to_document(data)
49
+ document.blocks
50
+ document.notes
51
+ document.assets
52
+ ```
53
+
54
+ ## Format detection
55
+
56
+ The format is read from the file content using the marker designated by its specification: the PDF header, RTF open group, OLE stream names, or ZIP package metadata. CSV has no signature, so detection returns `nil`; its extension or an explicit format names it instead.
57
+
58
+ ```ruby
59
+ Anydoc.format_from_bytes(data) # => :docx, or nil
60
+ Anydoc.format_from_extension(".pptm") # => :pptx
61
+ Anydoc.format_from_path("report.odt") # => :odt
62
+ ```
63
+
64
+ ## Images and embedded objects
65
+
66
+ Markdown cannot embed bytes. Embedded images render as alt text while their binary strings remain in `document.assets`, tagged with a media type and the package part they came from. Images with external URLs render as ordinary Markdown images.
67
+
68
+ PDF conversion emits Markdown directly and does not have a document-model form; use `Anydoc.to_markdown` or `Anydoc.to_markdown_bytes` for PDFs.
69
+
70
+ ## Development
71
+
72
+ ```bash
73
+ bin/setup
74
+ bundle exec rake compile test
75
+ ```
76
+
77
+ ## License
78
+
79
+ [MIT](https://github.com/firecrawl/anydoc/blob/main/LICENSE)
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+ require "tmpdir"
10
+
11
+ RuboCop::RakeTask.new do |task|
12
+ task.options = ["--cache-root", File.join(File.realpath(Dir.tmpdir), "anydoc-rubocop-cache")]
13
+ end
14
+
15
+ require "rb_sys/extensiontask"
16
+
17
+ task build: :compile
18
+
19
+ GEMSPEC = Gem::Specification.load("anydoc.gemspec")
20
+
21
+ RbSys::ExtensionTask.new("anydoc", GEMSPEC) do |ext|
22
+ ext.lib_dir = "lib/anydoc"
23
+ end
24
+
25
+ task default: %i[compile test rubocop]
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "anydoc"
3
+ version = "0.1.3-ruby.0"
4
+ edition = "2024"
5
+ authors = ["Nick Pezza <pezza@hey.com>"]
6
+ license = "MIT"
7
+ publish = false
8
+
9
+ [lib]
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ anydoc-core = { package = "anydoc", version = "=0.1.3" }
14
+ magnus = "0.8.2"
15
+ rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
16
+
17
+ [build-dependencies]
18
+ rb-sys-env = "0.2.2"
19
+
20
+ [dev-dependencies]
21
+ rb-sys-test-helpers = { version = "0.2.2" }
@@ -0,0 +1,5 @@
1
+ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
2
+ let _ = rb_sys_env::activate()?;
3
+
4
+ Ok(())
5
+ }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("anydoc/anydoc")
@@ -0,0 +1,187 @@
1
+ //! The document model, converted eagerly into immutable Ruby `Data` objects.
2
+
3
+ use anydoc_core::model;
4
+ use magnus::{Error, RArray, RClass, RModule, RString, Ruby, Value, prelude::*};
5
+
6
+ fn class(ruby: &Ruby, name: &str) -> Result<RClass, Error> {
7
+ let module: RModule = ruby.class_object().const_get("Anydoc")?;
8
+ module.const_get(name)
9
+ }
10
+
11
+ fn array(ruby: &Ruby, values: impl Iterator<Item = Result<Value, Error>>) -> Result<RArray, Error> {
12
+ let values = values.collect::<Result<Vec<_>, _>>()?;
13
+ Ok(ruby.ary_new_from_values(&values))
14
+ }
15
+
16
+ fn blocks(ruby: &Ruby, items: Vec<model::Block>) -> Result<RArray, Error> {
17
+ array(ruby, items.into_iter().map(|item| block(ruby, item)))
18
+ }
19
+
20
+ fn inlines(ruby: &Ruby, items: Vec<model::Inline>) -> Result<RArray, Error> {
21
+ array(ruby, items.into_iter().map(|item| inline(ruby, item)))
22
+ }
23
+
24
+ fn block(ruby: &Ruby, block: model::Block) -> Result<Value, Error> {
25
+ let (kind, level, anchor, content, list, table, inner, lang, text) = match block {
26
+ model::Block::Heading { level, anchor, content } => (
27
+ "heading",
28
+ Some(level),
29
+ anchor,
30
+ Some(inlines(ruby, content)?),
31
+ None,
32
+ None,
33
+ None,
34
+ None,
35
+ None,
36
+ ),
37
+ model::Block::Paragraph(content) => {
38
+ ("paragraph", None, None, Some(inlines(ruby, content)?), None, None, None, None, None)
39
+ }
40
+ model::Block::List(value) => {
41
+ ("list", None, None, None, Some(list_value(ruby, value)?), None, None, None, None)
42
+ }
43
+ model::Block::Table(value) => {
44
+ ("table", None, None, None, None, Some(table_value(ruby, value)?), None, None, None)
45
+ }
46
+ model::Block::BlockQuote(value) => {
47
+ ("block_quote", None, None, None, None, None, Some(blocks(ruby, value)?), None, None)
48
+ }
49
+ model::Block::CodeBlock { lang, text } => {
50
+ ("code_block", None, None, None, None, None, None, lang, Some(text))
51
+ }
52
+ model::Block::Rule => ("rule", None, None, None, None, None, None, None, None),
53
+ };
54
+ class(ruby, "Block")?
55
+ .funcall("new", (kind, level, anchor, content, list, table, inner, lang, text))
56
+ }
57
+
58
+ fn inline(ruby: &Ruby, inline: model::Inline) -> Result<Value, Error> {
59
+ let (kind, text, style, content, target, alt, source, anchor, note_id) = match inline {
60
+ model::Inline::Text { text, style } => (
61
+ "text",
62
+ Some(text),
63
+ Some(style_value(ruby, style)?),
64
+ None,
65
+ None,
66
+ None,
67
+ None,
68
+ None,
69
+ None,
70
+ ),
71
+ model::Inline::Link { content, target } => (
72
+ "link",
73
+ None,
74
+ None,
75
+ Some(inlines(ruby, content)?),
76
+ Some(link_target(ruby, target)?),
77
+ None,
78
+ None,
79
+ None,
80
+ None,
81
+ ),
82
+ model::Inline::Image { alt, source } => (
83
+ "image",
84
+ None,
85
+ None,
86
+ None,
87
+ None,
88
+ Some(alt),
89
+ Some(image_source(ruby, source)?),
90
+ None,
91
+ None,
92
+ ),
93
+ model::Inline::Anchor(id) => ("anchor", None, None, None, None, None, None, Some(id), None),
94
+ model::Inline::NoteRef(id) => {
95
+ ("note_ref", None, None, None, None, None, None, None, Some(id))
96
+ }
97
+ model::Inline::LineBreak => ("line_break", None, None, None, None, None, None, None, None),
98
+ };
99
+ class(ruby, "Inline")?
100
+ .funcall("new", (kind, text, style, content, target, alt, source, anchor, note_id))
101
+ }
102
+
103
+ fn style_value(ruby: &Ruby, style: model::Style) -> Result<Value, Error> {
104
+ class(ruby, "Style")?.funcall("new", (style.bold, style.italic, style.strike, style.code))
105
+ }
106
+
107
+ fn link_target(ruby: &Ruby, target: model::LinkTarget) -> Result<Value, Error> {
108
+ let (kind, value) = match target {
109
+ model::LinkTarget::External(value) => ("external", value),
110
+ model::LinkTarget::Relative(value) => ("relative", value),
111
+ model::LinkTarget::Anchor(value) => ("anchor", value),
112
+ };
113
+ class(ruby, "LinkTarget")?.funcall("new", (kind, value))
114
+ }
115
+
116
+ fn image_source(ruby: &Ruby, source: model::ImageSource) -> Result<Value, Error> {
117
+ let (kind, url, asset_id) = match source {
118
+ model::ImageSource::External(url) => ("external", Some(url), None),
119
+ model::ImageSource::Asset(id) => ("asset", None, Some(id.0)),
120
+ model::ImageSource::Unavailable => ("unavailable", None, None),
121
+ };
122
+ class(ruby, "ImageSource")?.funcall("new", (kind, url, asset_id))
123
+ }
124
+
125
+ fn list_value(ruby: &Ruby, list: model::List) -> Result<Value, Error> {
126
+ let marker = match list.marker {
127
+ model::MarkerKind::Bullet => "bullet",
128
+ model::MarkerKind::Decimal => "decimal",
129
+ model::MarkerKind::LowerAlpha => "lower_alpha",
130
+ model::MarkerKind::UpperAlpha => "upper_alpha",
131
+ model::MarkerKind::LowerRoman => "lower_roman",
132
+ model::MarkerKind::UpperRoman => "upper_roman",
133
+ };
134
+ let items = array(ruby, list.items.into_iter().map(|item| list_item(ruby, item)))?;
135
+ class(ruby, "List")?.funcall("new", (marker, list.start, items))
136
+ }
137
+
138
+ fn list_item(ruby: &Ruby, item: model::ListItem) -> Result<Value, Error> {
139
+ class(ruby, "ListItem")?
140
+ .funcall("new", (blocks(ruby, item.blocks)?, item.checked, item.marker_label))
141
+ }
142
+
143
+ fn table_value(ruby: &Ruby, table: model::Table) -> Result<Value, Error> {
144
+ let rows = table.grid.into_iter().map(|row| {
145
+ let row = array(ruby, row.into_iter().map(|slot| cell_slot(ruby, slot)))?;
146
+ Ok(row.as_value())
147
+ });
148
+ let grid = array(ruby, rows)?;
149
+ let kind = match table.kind {
150
+ model::TableKind::Data => "data",
151
+ model::TableKind::Layout => "layout",
152
+ };
153
+ class(ruby, "Table")?.funcall("new", (grid, table.header_rows, kind))
154
+ }
155
+
156
+ fn cell_slot(ruby: &Ruby, slot: model::CellSlot) -> Result<Value, Error> {
157
+ let (kind, cell, origin_row, origin_col) = match slot {
158
+ model::CellSlot::Origin(value) => ("origin", Some(cell(ruby, value)?), None, None),
159
+ model::CellSlot::Covered { origin_row, origin_col } => {
160
+ ("covered", None, Some(origin_row), Some(origin_col))
161
+ }
162
+ };
163
+ class(ruby, "CellSlot")?.funcall("new", (kind, cell, origin_row, origin_col))
164
+ }
165
+
166
+ fn cell(ruby: &Ruby, cell: model::Cell) -> Result<Value, Error> {
167
+ class(ruby, "Cell")?.funcall("new", (blocks(ruby, cell.blocks)?, cell.col_span, cell.row_span))
168
+ }
169
+
170
+ fn note(ruby: &Ruby, note: model::Note) -> Result<Value, Error> {
171
+ let kind = match note.kind {
172
+ model::NoteKind::Footnote => "footnote",
173
+ model::NoteKind::Endnote => "endnote",
174
+ };
175
+ class(ruby, "Note")?.funcall("new", (note.id, kind, blocks(ruby, note.blocks)?))
176
+ }
177
+
178
+ fn asset(ruby: &Ruby, asset: model::Asset) -> Result<Value, Error> {
179
+ let data: RString = ruby.str_from_slice(&asset.bytes);
180
+ class(ruby, "Asset")?.funcall("new", (asset.id.0, asset.media_type, asset.origin_part, data))
181
+ }
182
+
183
+ pub fn document(ruby: &Ruby, document: model::Document) -> Result<Value, Error> {
184
+ let notes = array(ruby, document.notes.into_iter().map(|value| note(ruby, value)))?;
185
+ let assets = array(ruby, document.assets.into_iter().map(|value| asset(ruby, value)))?;
186
+ class(ruby, "Document")?.funcall("new", (blocks(ruby, document.blocks)?, notes, assets))
187
+ }
@@ -0,0 +1,147 @@
1
+ //! Ruby bindings for anydoc.
2
+
3
+ use magnus::{
4
+ Error, Exception, RString, Ruby, Value, exception::ExceptionClass, function, prelude::*,
5
+ };
6
+ use std::{ffi::c_void, panic::AssertUnwindSafe, ptr};
7
+
8
+ mod document;
9
+
10
+ const FORMATS: [(&str, anydoc_core::Format); 12] = [
11
+ ("doc", anydoc_core::Format::Doc),
12
+ ("docx", anydoc_core::Format::Docx),
13
+ ("odt", anydoc_core::Format::Odt),
14
+ ("pdf", anydoc_core::Format::Pdf),
15
+ ("ppt", anydoc_core::Format::Ppt),
16
+ ("pptx", anydoc_core::Format::Pptx),
17
+ ("rtf", anydoc_core::Format::Rtf),
18
+ ("epub", anydoc_core::Format::Epub),
19
+ ("xlsx", anydoc_core::Format::Excel),
20
+ ("ods", anydoc_core::Format::Ods),
21
+ ("odp", anydoc_core::Format::Odp),
22
+ ("csv", anydoc_core::Format::Csv),
23
+ ];
24
+
25
+ fn parse_format(ruby: &Ruby, name: &str) -> Result<anydoc_core::Format, Error> {
26
+ FORMATS.iter().find(|(candidate, _)| *candidate == name).map(|(_, format)| *format).ok_or_else(
27
+ || {
28
+ let names = FORMATS.iter().map(|(name, _)| *name).collect::<Vec<_>>().join(", ");
29
+ Error::new(
30
+ ruby.exception_arg_error(),
31
+ format!("unknown format {name:?}; expected one of {names}"),
32
+ )
33
+ },
34
+ )
35
+ }
36
+
37
+ fn format_name(format: anydoc_core::Format) -> &'static str {
38
+ FORMATS
39
+ .iter()
40
+ .find(|(_, candidate)| *candidate == format)
41
+ .map(|(name, _)| *name)
42
+ .expect("every format is named")
43
+ }
44
+
45
+ fn convert_error(ruby: &Ruby, error: anydoc_core::ConvertError) -> Error {
46
+ match error {
47
+ anydoc_core::ConvertError::Io(error) => match error.raw_os_error() {
48
+ Some(errno) => ruby
49
+ .exception_system_call_error()
50
+ .funcall::<_, _, Exception>("new", (error.to_string(), errno))
51
+ .map(Error::from)
52
+ .unwrap_or_else(|error| error),
53
+ None => Error::new(ruby.exception_io_error(), error.to_string()),
54
+ },
55
+ other => {
56
+ let module = ruby.define_module("Anydoc").expect("Anydoc module is defined");
57
+ let class: ExceptionClass =
58
+ module.const_get("ConvertError").expect("Anydoc::ConvertError is defined");
59
+ Error::new(class, other.to_string())
60
+ }
61
+ }
62
+ }
63
+
64
+ fn bytes(data: RString) -> Vec<u8> {
65
+ // Copy before conversion so Rust never retains a pointer into Ruby's heap.
66
+ unsafe { data.as_slice() }.to_vec()
67
+ }
68
+
69
+ /// Run CPU- and I/O-heavy parsing without holding Ruby's global VM lock.
70
+ fn without_gvl<F, T>(function: F) -> T
71
+ where
72
+ F: FnOnce() -> T + Send,
73
+ T: Send,
74
+ {
75
+ struct Call<F, T> {
76
+ function: Option<F>,
77
+ result: Option<std::thread::Result<T>>,
78
+ }
79
+
80
+ unsafe extern "C" fn call<F, T>(data: *mut c_void) -> *mut c_void
81
+ where
82
+ F: FnOnce() -> T,
83
+ {
84
+ let call = unsafe { &mut *data.cast::<Call<F, T>>() };
85
+ let function = call.function.take().expect("without_gvl callback runs once");
86
+ call.result = Some(std::panic::catch_unwind(AssertUnwindSafe(function)));
87
+ ptr::null_mut()
88
+ }
89
+
90
+ let mut state = Call { function: Some(function), result: None };
91
+ unsafe {
92
+ rb_sys::rb_thread_call_without_gvl(
93
+ Some(call::<F, T>),
94
+ ptr::from_mut(&mut state).cast(),
95
+ None,
96
+ ptr::null_mut(),
97
+ );
98
+ }
99
+ match state.result.expect("without_gvl callback completed") {
100
+ Ok(value) => value,
101
+ Err(panic) => std::panic::resume_unwind(panic),
102
+ }
103
+ }
104
+
105
+ fn format_from_bytes(data: RString) -> Option<&'static str> {
106
+ anydoc_core::Format::from_bytes(&bytes(data)).map(format_name)
107
+ }
108
+
109
+ fn format_from_extension(extension: String) -> Option<&'static str> {
110
+ anydoc_core::Format::from_extension(extension.trim_start_matches('.')).map(format_name)
111
+ }
112
+
113
+ fn format_from_path(path: String) -> Option<&'static str> {
114
+ anydoc_core::Format::from_path(std::path::Path::new(&path)).map(format_name)
115
+ }
116
+
117
+ fn to_markdown(ruby: &Ruby, path: String) -> Result<String, Error> {
118
+ without_gvl(move || anydoc_core::to_markdown(path)).map_err(|error| convert_error(ruby, error))
119
+ }
120
+
121
+ fn to_markdown_bytes(ruby: &Ruby, data: RString, format: Option<String>) -> Result<String, Error> {
122
+ let format = format.as_deref().map(|name| parse_format(ruby, name)).transpose()?;
123
+ let data = bytes(data);
124
+ without_gvl(move || anydoc_core::to_markdown_bytes(&data, format))
125
+ .map_err(|error| convert_error(ruby, error))
126
+ }
127
+
128
+ fn to_document(ruby: &Ruby, data: RString, format: Option<String>) -> Result<Value, Error> {
129
+ let format = format.as_deref().map(|name| parse_format(ruby, name)).transpose()?;
130
+ let data = bytes(data);
131
+ let parsed = without_gvl(move || anydoc_core::to_document(&data, format))
132
+ .map_err(|error| convert_error(ruby, error))?;
133
+ document::document(ruby, parsed)
134
+ }
135
+
136
+ #[magnus::init]
137
+ fn init(ruby: &Ruby) -> Result<(), Error> {
138
+ let module = ruby.define_module("Anydoc")?;
139
+ module.define_singleton_method("_format_from_bytes", function!(format_from_bytes, 1))?;
140
+ module
141
+ .define_singleton_method("_format_from_extension", function!(format_from_extension, 1))?;
142
+ module.define_singleton_method("_format_from_path", function!(format_from_path, 1))?;
143
+ module.define_singleton_method("_to_markdown", function!(to_markdown, 1))?;
144
+ module.define_singleton_method("_to_markdown_bytes", function!(to_markdown_bytes, 2))?;
145
+ module.define_singleton_method("_to_document", function!(to_document, 2))?;
146
+ Ok(())
147
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anydoc
4
+ VERSION = "0.1.3"
5
+ end
data/lib/anydoc.rb ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "anydoc/version"
4
+
5
+ module Anydoc
6
+ class Error < StandardError; end
7
+
8
+ # Raised when an input can be read, but meaningful conversion is impossible.
9
+ class ConvertError < Error; end
10
+
11
+ Document = Data.define(:blocks, :notes, :assets)
12
+ Block = Data.define(:kind, :level, :anchor, :content, :list, :table, :blocks, :lang, :text)
13
+ Inline = Data.define(:kind, :text, :style, :content, :target, :alt, :source, :anchor, :note_id)
14
+ Style = Data.define(:bold, :italic, :strike, :code)
15
+ LinkTarget = Data.define(:kind, :value)
16
+ ImageSource = Data.define(:kind, :url, :asset_id)
17
+ List = Data.define(:marker, :start, :items)
18
+ ListItem = Data.define(:blocks, :checked, :marker_label)
19
+ Table = Data.define(:grid, :header_rows, :kind)
20
+ CellSlot = Data.define(:kind, :cell, :origin_row, :origin_col)
21
+ Cell = Data.define(:blocks, :col_span, :row_span)
22
+ Note = Data.define(:id, :kind, :blocks)
23
+ Asset = Data.define(:id, :media_type, :origin_part, :data)
24
+ end
25
+
26
+ require "anydoc/anydoc"
27
+
28
+ module Anydoc
29
+ FORMATS = %i[doc docx odt pdf ppt pptx rtf epub xlsx ods odp csv].freeze
30
+
31
+ class << self
32
+ def format_from_bytes(data)
33
+ _format_from_bytes(data)&.to_sym
34
+ end
35
+
36
+ def format_from_extension(extension)
37
+ _format_from_extension(extension)&.to_sym
38
+ end
39
+
40
+ def format_from_path(path)
41
+ _format_from_path(File.path(path))&.to_sym
42
+ end
43
+
44
+ def to_markdown(path)
45
+ _to_markdown(File.path(path))
46
+ end
47
+
48
+ def to_markdown_bytes(data, format = nil)
49
+ _to_markdown_bytes(data, format&.to_s)
50
+ end
51
+
52
+ def to_document(data, format = nil)
53
+ _to_document(data, format&.to_s)
54
+ end
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anydoc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Nick Pezza
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.9.128
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.9.128
26
+ description: Ruby bindings for the anydoc Rust document converter.
27
+ email:
28
+ - pezza@hey.com
29
+ executables: []
30
+ extensions:
31
+ - ext/anydoc/extconf.rb
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Cargo.toml
35
+ - LICENSE.txt
36
+ - README.md
37
+ - Rakefile
38
+ - ext/anydoc/Cargo.toml
39
+ - ext/anydoc/build.rs
40
+ - ext/anydoc/extconf.rb
41
+ - ext/anydoc/src/document.rs
42
+ - ext/anydoc/src/lib.rs
43
+ - lib/anydoc.rb
44
+ - lib/anydoc/version.rb
45
+ homepage: https://github.com/firecrawl/anydoc#readme
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ homepage_uri: https://github.com/firecrawl/anydoc#readme
50
+ source_code_uri: https://github.com/firecrawl/anydoc/tree/main/ruby
51
+ changelog_uri: https://github.com/firecrawl/anydoc/releases
52
+ rubygems_mfa_required: 'true'
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.2.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 4.0.15
68
+ specification_version: 4
69
+ summary: Convert documents to GitHub-Flavored Markdown
70
+ test_files: []