anydoc-ruby 0.1.7-aarch64-linux

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: 8c4ad832495b176c63d22f5c081121a2dbfa6f44aa672c3bd9a0f024ff7a1287
4
+ data.tar.gz: 179e918f328a4a766dc082d03be3f39e087c61ab1f8a8877d5b7baf556dbf6ba
5
+ SHA512:
6
+ metadata.gz: e9361cdb238260b7bef297b97fcfd5722e7a5f607c57fb8b16c6de9f21f2a960b6fa7d8ce3271378a4004e8dd43ff5b4c94c3b9af40191669bdbbff9633f5136
7
+ data.tar.gz: 81a6725073be6b7cdcc8df3987f57e323e7d9a8992b9a77f80694ad316e4a177b2f0ee93deab9acf884c95a1133b8704325a392bc37b0f5236484e4745bbc451
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sideguide Technologies Inc.
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # anydoc-ruby
2
+
3
+ [![Gem](https://img.shields.io/gem/v/anydoc-ruby.svg)](https://rubygems.org/gems/anydoc-ruby)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/honzasterba/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://crates.io/crates/anydoc) Rust crate, which is built by [Firecrawl](https://firecrawl.dev).
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 the same no matter which format goes in. Conversion releases the GVL, so other threads keep running. RBS signatures ship with the gem.
9
+
10
+ ```bash
11
+ bundle add anydoc-ruby
12
+ ```
13
+
14
+ The gem installs as `anydoc-ruby` and requires as `anydoc`, so a Gemfile entry that Bundler auto-requires needs `gem "anydoc-ruby", require: "anydoc"`. Precompiled native gems cover Linux (glibc and musl), macOS, and Windows on Ruby 3.2+; anywhere else, `gem install` builds the extension from source, which needs a [Rust toolchain](https://rustup.rs).
15
+
16
+ ## Supported formats
17
+
18
+ | Format | Extensions |
19
+ | ---------------- | ---------------------------------------------------------- |
20
+ | Word | `.doc`, `.docx`, `.docm` |
21
+ | PowerPoint | `.ppt`, `.pps`, `.pot`, `.pptx`, `.pptm`, `.ppsx`, `.ppsm` |
22
+ | Excel | `.xls`, `.xlsx`, `.xlsm`, `.xlsb` |
23
+ | OpenDocument | `.odt`, `.ods`, `.odp` |
24
+ | Rich Text Format | `.rtf` |
25
+ | EPUB | `.epub` |
26
+ | CSV | `.csv` |
27
+ | PDF | `.pdf` |
28
+
29
+ ## Usage
30
+
31
+ ```ruby
32
+ require "anydoc"
33
+
34
+ # From a file path:
35
+ markdown = Anydoc.to_markdown("report.docx")
36
+
37
+ # From bytes, with the format detected from the content:
38
+ markdown = Anydoc.to_markdown_bytes(data)
39
+
40
+ # Or name it, which signature-less formats (CSV) need:
41
+ markdown = Anydoc.to_markdown_bytes(data, format: :csv)
42
+
43
+ # Or stop at the document model, which also carries embedded assets:
44
+ document = Anydoc.to_document(data)
45
+ ```
46
+
47
+ `to_markdown` takes anything path-like, including a `Pathname`. A format is named with a symbol from `Anydoc::FORMATS`; a string is accepted too.
48
+
49
+ ## Errors
50
+
51
+ A conversion raises only when no meaningful Markdown could come out of the file. The exception class names what went wrong:
52
+
53
+ ```ruby
54
+ begin
55
+ Anydoc.to_markdown(path)
56
+ rescue Anydoc::EncryptedError, Anydoc::UnsupportedError => error
57
+ # No document comes out of these, so record the file and take the next one.
58
+ unconverted << [path, error.class]
59
+ nil
60
+ end
61
+ ```
62
+
63
+ | Exception | Raised when |
64
+ | -------------------------- | ------------------------------------------------------------------- |
65
+ | `Anydoc::UnsupportedError` | Unknown format, or one that cannot be converted (an image-only PDF) |
66
+ | `Anydoc::MalformedError` | Structurally unusable: no meaningful content could be extracted |
67
+ | `Anydoc::EncryptedError` | Encrypted or password-protected |
68
+ | `Anydoc::ResourceLimitError` | Crossed a fixed safety limit (decompression, nesting, node count) |
69
+ | `Anydoc::MissingPartError` | A part required for any meaningful output is absent |
70
+ | `Errno::*` | The file could not be read, from `to_markdown` only |
71
+
72
+ The five conversion failures subclass `Anydoc::ConvertError`, so rescuing that handles all of them at once. `MalformedError#part` and `MissingPartError#part` name the package part at fault, `ResourceLimitError#limit` names the limit crossed as a symbol, and `#message` carries the whole message. A `format:` argument naming no supported format raises `ArgumentError`.
73
+
74
+ ## Format detection
75
+
76
+ The format is read from the file content, using the marker its specification designates: the PDF header, the RTF open group, OLE stream names, the ZIP package mimetype and content types. CSV has no such marker, so detection returns `nil` for it and the extension, or an explicit format, names it instead.
77
+
78
+ ```ruby
79
+ Anydoc.format_from_bytes(data) # :docx, or nil when nothing matches
80
+ Anydoc.format_from_extension(".pptm") # :pptx
81
+ Anydoc.format_from_path("report.odt") # :odt
82
+ ```
83
+
84
+ ## The document model
85
+
86
+ `Anydoc.to_document` stops at the parsed model instead of rendering it. Every class in it is a `Data`, so instances are frozen, compare by value, answer `to_h`, and destructure in `case/in`:
87
+
88
+ ```ruby
89
+ document = Anydoc.to_document(File.binread("report.docx"))
90
+
91
+ titles = document.blocks.filter_map do |block|
92
+ case block
93
+ in Anydoc::Block[kind: :heading, level: 1, content:]
94
+ content.filter_map(&:text).join
95
+ else
96
+ nil
97
+ end
98
+ end
99
+ ```
100
+
101
+ Variants are a `kind` symbol plus the members that kind carries; members belonging to other kinds are `nil`. `Document#blocks` holds the body, `Document#notes` the footnote and endnote bodies that `:note_ref` inlines point at, and `Document#assets` the embedded bytes.
102
+
103
+ PDFs convert straight to Markdown and have no document-model form, so `to_document` raises `UnsupportedError` for them; use `to_markdown_bytes`.
104
+
105
+ ## Images and embedded objects
106
+
107
+ Markdown cannot embed bytes, so an embedded image renders as its alt text while the bytes stay on `document.assets` as binary strings, tagged with a media type and the part they came from. Images that carry an external URL render as ordinary Markdown images.
108
+
109
+ ```ruby
110
+ document.assets.each do |asset|
111
+ File.binwrite("asset-#{asset.id}", asset.data) if asset.media_type.start_with?("image/")
112
+ end
113
+ ```
114
+
115
+ Full behavior notes and benchmarks live in the [repository README](https://github.com/honzasterba/anydoc#readme).
116
+
117
+ ## Development
118
+
119
+ From this directory, with a [Rust toolchain](https://rustup.rs) installed:
120
+
121
+ ```bash
122
+ bundle install
123
+ bundle exec rake # compile the extension, then run the tests
124
+ bundle exec rake compile
125
+ bundle exec rake test
126
+ ```
127
+
128
+ Inside this repository the extension builds against the crate in the parent directory rather than the published one; `.cargo/config.toml` is what redirects it.
129
+
130
+ ### Packaging
131
+
132
+ The source gem needs nothing but RubyGems:
133
+
134
+ ```bash
135
+ bundle exec rake build # pkg/anydoc-ruby-<version>.gem
136
+ ```
137
+
138
+ The precompiled gems are cross-compiled in [rb-sys-dock](https://github.com/oxidize-rb/rb-sys) containers, so they need Docker running. Each one carries an extension per Ruby ABI:
139
+
140
+ ```bash
141
+ bundle exec rake gems:x86_64-linux # one platform
142
+ bundle exec rake gems:all # the source gem and all seven platforms
143
+ ```
144
+
145
+ Run these through Rake rather than calling `rb-sys-dock` from this directory: the container mounts the shell's working directory, and the build needs the crate one level above it, so the tasks run the CLI from the repository root with `--directory ruby`. The first build of each platform pulls a multi-gigabyte image.
146
+
147
+ ## License
148
+
149
+ [MIT](https://github.com/honzasterba/anydoc/blob/main/LICENSE)
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anydoc
4
+ # The document model {Anydoc.to_document} returns.
5
+ #
6
+ # Every class here is a +Data+, so instances are frozen, compare by value,
7
+ # answer +to_h+, and destructure in +case/in+:
8
+ #
9
+ # case block
10
+ # in Anydoc::Block[kind: :heading, level:, content:]
11
+ # ...
12
+ # end
13
+ #
14
+ # Variants (block kinds, link targets, ...) are a +kind+ symbol plus the
15
+ # members that kind carries; members belonging to other kinds are +nil+.
16
+ #
17
+ # @!parse
18
+ # # Only fully resolved content lives in the model: style cascades,
19
+ # # numbering, and references are resolved before it is built.
20
+ class Document < Data.define(
21
+ # @return [Array<Block>] body content, in reading order.
22
+ :blocks,
23
+ # @return [Array<Note>] footnote and endnote bodies, referenced from text
24
+ # by a +:note_ref+ inline.
25
+ :notes,
26
+ # @return [Array<Asset>] every embedded asset, indexed by its +id+.
27
+ :assets
28
+ )
29
+ end
30
+
31
+ # One block-level piece of content.
32
+ #
33
+ # @!attribute [r] kind
34
+ # @return [Symbol] +:heading+, +:paragraph+, +:list+, +:table+,
35
+ # +:block_quote+, +:code_block+, or +:rule+.
36
+ # @!attribute [r] level
37
+ # @return [Integer, nil] heading: 1-6.
38
+ # @!attribute [r] anchor
39
+ # @return [String, nil] heading: stable anchor id, when the document
40
+ # targets this heading.
41
+ # @!attribute [r] content
42
+ # @return [Array<Inline>, nil] heading, paragraph.
43
+ # @!attribute [r] list
44
+ # @return [List, nil]
45
+ # @!attribute [r] table
46
+ # @return [Table, nil]
47
+ # @!attribute [r] blocks
48
+ # @return [Array<Block>, nil] block_quote.
49
+ # @!attribute [r] lang
50
+ # @return [String, nil] code_block.
51
+ # @!attribute [r] text
52
+ # @return [String, nil] code_block.
53
+ class Block < Data.define(
54
+ :kind, :level, :anchor, :content, :list, :table, :blocks, :lang, :text
55
+ )
56
+ end
57
+
58
+ # One inline-level piece of content.
59
+ #
60
+ # @!attribute [r] kind
61
+ # @return [Symbol] +:text+, +:link+, +:image+, +:anchor+ (a zero-width
62
+ # marker for an internal link target at this position), +:note_ref+, or
63
+ # +:line_break+.
64
+ # @!attribute [r] text
65
+ # @return [String, nil] text.
66
+ # @!attribute [r] style
67
+ # @return [Style, nil] text.
68
+ # @!attribute [r] content
69
+ # @return [Array<Inline>, nil] link.
70
+ # @!attribute [r] target
71
+ # @return [LinkTarget, nil] link.
72
+ # @!attribute [r] alt
73
+ # @return [String, nil] image.
74
+ # @!attribute [r] source
75
+ # @return [ImageSource, nil] image.
76
+ # @!attribute [r] anchor
77
+ # @return [String, nil] anchor: the anchor id.
78
+ # @!attribute [r] note_id
79
+ # @return [String, nil] note_ref: the id of the note in
80
+ # {Document#notes}.
81
+ class Inline < Data.define(
82
+ :kind, :text, :style, :content, :target, :alt, :source, :anchor, :note_id
83
+ )
84
+ end
85
+
86
+ # Fully resolved character style.
87
+ #
88
+ # @!attribute [r] bold
89
+ # @return [Boolean]
90
+ # @!attribute [r] italic
91
+ # @return [Boolean]
92
+ # @!attribute [r] strike
93
+ # @return [Boolean]
94
+ # @!attribute [r] code
95
+ # @return [Boolean] monospace, from a code or teletype character style.
96
+ class Style < Data.define(:bold, :italic, :strike, :code)
97
+ # @return [Boolean] whether no toggle is set.
98
+ def plain? = !(bold || italic || strike || code)
99
+ end
100
+
101
+ # Where a link points.
102
+ #
103
+ # @!attribute [r] kind
104
+ # @return [Symbol] +:external+ (absolute URL with a scheme), +:relative+
105
+ # (scheme-less reference, preserved as written), or +:anchor+ (an
106
+ # internal target: a heading anchor or an +:anchor+ inline).
107
+ # @!attribute [r] value
108
+ # @return [String] the URL, relative reference, or anchor id.
109
+ class LinkTarget < Data.define(:kind, :value)
110
+ end
111
+
112
+ # Where an image's bytes come from.
113
+ #
114
+ # @!attribute [r] kind
115
+ # @return [Symbol] +:external+ (absolute URL with a scheme), +:asset+
116
+ # (embedded, carried in {Document#assets}), or +:unavailable+ (the
117
+ # image's part is missing or unreadable and it has no URL, so only the
118
+ # alt text remains).
119
+ # @!attribute [r] url
120
+ # @return [String, nil] external.
121
+ # @!attribute [r] asset_id
122
+ # @return [Integer, nil] asset: index into {Document#assets}.
123
+ class ImageSource < Data.define(:kind, :url, :asset_id)
124
+ end
125
+
126
+ # A list and the marker family the source document used for it.
127
+ #
128
+ # @!attribute [r] marker
129
+ # @return [Symbol] +:bullet+, +:decimal+, +:lower_alpha+, +:upper_alpha+,
130
+ # +:lower_roman+, or +:upper_roman+.
131
+ # @!attribute [r] start
132
+ # @return [Integer] the ordinal the first item counts from.
133
+ # @!attribute [r] items
134
+ # @return [Array<ListItem>]
135
+ class List < Data.define(:marker, :start, :items)
136
+ # @return [Boolean] whether the list is numbered.
137
+ def ordered? = marker != :bullet
138
+ end
139
+
140
+ # One item of a {List}.
141
+ #
142
+ # @!attribute [r] blocks
143
+ # @return [Array<Block>]
144
+ # @!attribute [r] checked
145
+ # @return [Boolean, nil] task-list state, when the item carries a
146
+ # checkbox.
147
+ # @!attribute [r] marker_label
148
+ # @return [String, nil] literal marker text that overrides the list marker
149
+ # when the source number text cannot be reproduced from the marker and
150
+ # position alone (composite number text such as +1-a)+).
151
+ class ListItem < Data.define(:blocks, :checked, :marker_label)
152
+ end
153
+
154
+ # A table as a canonical grid: every logical grid position appears exactly
155
+ # once. Content and spans live on the origin slot, and each position a span
156
+ # covers holds a +:covered+ slot pointing back at that origin.
157
+ #
158
+ # @!attribute [r] grid
159
+ # @return [Array<Array<CellSlot>>] rows of slots. Rows may differ in
160
+ # length when the source is ragged.
161
+ # @!attribute [r] header_rows
162
+ # @return [Integer] number of leading rows that are header rows (0 = no
163
+ # header).
164
+ # @!attribute [r] kind
165
+ # @return [Symbol] +:data+ for a real data table, +:layout+ for layout
166
+ # scaffolding (text boxes, positioning tables).
167
+ class Table < Data.define(:grid, :header_rows, :kind)
168
+ end
169
+
170
+ # One position in a {Table#grid}: either a cell or the shadow of one.
171
+ #
172
+ # @!attribute [r] kind
173
+ # @return [Symbol] +:origin+ or +:covered+.
174
+ # @!attribute [r] cell
175
+ # @return [Cell, nil] origin.
176
+ # @!attribute [r] origin_row
177
+ # @return [Integer, nil] covered: row of the origin this position belongs
178
+ # to.
179
+ # @!attribute [r] origin_col
180
+ # @return [Integer, nil] covered: column of the origin this position
181
+ # belongs to.
182
+ class CellSlot < Data.define(:kind, :cell, :origin_row, :origin_col)
183
+ end
184
+
185
+ # A table cell and the extent it spans.
186
+ #
187
+ # @!attribute [r] blocks
188
+ # @return [Array<Block>]
189
+ # @!attribute [r] col_span
190
+ # @return [Integer] columns covered, at least 1.
191
+ # @!attribute [r] row_span
192
+ # @return [Integer] rows covered, at least 1.
193
+ class Cell < Data.define(:blocks, :col_span, :row_span)
194
+ end
195
+
196
+ # A footnote or endnote body, referenced from text by a +:note_ref+ inline.
197
+ #
198
+ # @!attribute [r] id
199
+ # @return [String] the id the referencing inline carries.
200
+ # @!attribute [r] kind
201
+ # @return [Symbol] +:footnote+ or +:endnote+, by where the source places
202
+ # the note.
203
+ # @!attribute [r] blocks
204
+ # @return [Array<Block>] the note's own content.
205
+ class Note < Data.define(:id, :kind, :blocks)
206
+ end
207
+
208
+ # An embedded binary asset (image, object payload). Bytes are always
209
+ # retained, so a document stays self-contained.
210
+ #
211
+ # @!attribute [r] id
212
+ # @return [Integer] index into {Document#assets}, as referenced by an
213
+ # image source.
214
+ # @!attribute [r] media_type
215
+ # @return [String] MIME type, e.g. +image/png+.
216
+ # @!attribute [r] origin_part
217
+ # @return [String] package part or stream the asset came from, for
218
+ # provenance.
219
+ # @!attribute [r] data
220
+ # @return [String] the payload, exactly as stored in the source, as a
221
+ # binary (ASCII-8BIT) string.
222
+ class Asset < Data.define(:id, :media_type, :origin_part, :data)
223
+ end
224
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anydoc
4
+ # Base class for everything this gem raises. Unreadable files raise the
5
+ # +Errno+ exception any other read of them would, and a format argument
6
+ # naming no supported format raises +ArgumentError+.
7
+ class Error < StandardError; end
8
+
9
+ # Meaningful conversion was impossible. Rescue this to handle every kind of
10
+ # failure at once, or one of its subclasses to single one out.
11
+ #
12
+ # Recoverable producer quirks never reach here: they are recovered or
13
+ # skipped while conversion continues.
14
+ class ConvertError < Error; end
15
+
16
+ # The format is unknown, or cannot be converted at all: a scanned or
17
+ # image-only PDF needs OCR, which anydoc does not do.
18
+ class UnsupportedError < ConvertError; end
19
+
20
+ # The document is structurally unusable: no meaningful content could be
21
+ # extracted.
22
+ class MalformedError < ConvertError
23
+ # @return [String, nil] the package part or stream at fault, or +nil+ when
24
+ # no single part is.
25
+ attr_reader :part
26
+ end
27
+
28
+ # The document is encrypted or password-protected.
29
+ class EncryptedError < ConvertError; end
30
+
31
+ # A fixed safety limit was crossed: decompression, nesting depth, node
32
+ # count, repeat expansion, or retained asset bytes.
33
+ class ResourceLimitError < ConvertError
34
+ # @return [Symbol] the limit that was crossed, e.g. +:max_entry_bytes+.
35
+ attr_reader :limit
36
+ end
37
+
38
+ # A part required for any meaningful output is absent.
39
+ class MissingPartError < ConvertError
40
+ # @return [String] the part or stream that is missing.
41
+ attr_reader :part
42
+ end
43
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anydoc
4
+ # The gem version, which is the version of the anydoc crate it wraps.
5
+ VERSION = "0.1.7"
6
+ end
data/lib/anydoc.rb ADDED
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "anydoc/version"
4
+ require_relative "anydoc/errors"
5
+ require_relative "anydoc/document"
6
+
7
+ # Precompiled gems carry one extension per Ruby ABI; a gem built from source
8
+ # puts its single extension straight into lib/anydoc.
9
+ begin
10
+ RUBY_VERSION =~ /(\d+\.\d+)/
11
+ require_relative "anydoc/#{Regexp.last_match(1)}/anydoc"
12
+ rescue LoadError
13
+ require_relative "anydoc/anydoc"
14
+ end
15
+
16
+ # Convert documents (Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV,
17
+ # and PDF) to GitHub-Flavored Markdown.
18
+ #
19
+ # Anydoc.to_markdown("report.docx")
20
+ #
21
+ # Every format parses into one shared document model and renders through a
22
+ # single Markdown serializer, so headings, tables, lists, and footnotes come
23
+ # out the same no matter which format goes in.
24
+ #
25
+ # @see FORMATS the formats that can be named explicitly.
26
+ module Anydoc
27
+ # @!parse
28
+ # # Every format anydoc reads, named after the extension that identifies
29
+ # # it. Container variants that share a parser (+.docm+, +.xlsm+,
30
+ # # +.ppsx+, ...) map onto these through {format_from_bytes} or
31
+ # # {format_from_extension}.
32
+ # #
33
+ # # @return [Array<Symbol>]
34
+ # FORMATS = %i[doc docx odt pdf ppt pptx rtf epub xlsx ods odp csv].freeze
35
+
36
+ class << self
37
+ # Convert a document file to Markdown. The format is detected from the
38
+ # file content; the extension is the fallback for signature-less formats
39
+ # (CSV) and unrecognizable containers.
40
+ #
41
+ # @param path [String, Pathname] the file to convert.
42
+ # @return [String] GitHub-Flavored Markdown.
43
+ # @raise [ConvertError] if no meaningful Markdown could come out of it.
44
+ # @raise [SystemCallError] if the file could not be read.
45
+ def to_markdown(path)
46
+ path = File.path(path)
47
+ data = File.binread(path)
48
+ format = format_from_bytes(data) || format_from_path(path)
49
+ unless format
50
+ raise UnsupportedError,
51
+ "unsupported input: unrecognized file content and extension: #{path}"
52
+ end
53
+
54
+ to_markdown_bytes(data, format: format)
55
+ end
56
+
57
+ # Convert an in-memory document to Markdown.
58
+ #
59
+ # @param data [String] the document's bytes.
60
+ # @param format [Symbol, String, nil] which format to parse it as. Left
61
+ # out, the format is detected from the content, which signature-less
62
+ # formats (CSV) have to name explicitly.
63
+ # @return [String] GitHub-Flavored Markdown.
64
+ # @raise [ConvertError] if no meaningful Markdown could come out of it.
65
+ # @raise [ArgumentError] if +format+ names no supported format.
66
+ def to_markdown_bytes(data, format: nil)
67
+ Native.to_markdown_bytes(data, format_symbol(format))
68
+ end
69
+
70
+ # Parse an in-memory document into the document model, which also carries
71
+ # the embedded assets.
72
+ #
73
+ # Unsupported for +:pdf+: PDF conversion produces Markdown directly and
74
+ # has no document-model form; use {to_markdown_bytes}.
75
+ #
76
+ # @param data [String] the document's bytes.
77
+ # @param format [Symbol, String, nil] which format to parse it as. Left
78
+ # out, the format is detected from the content.
79
+ # @return [Document]
80
+ # @raise [ConvertError] if the document could not be parsed.
81
+ # @raise [ArgumentError] if +format+ names no supported format.
82
+ def to_document(data, format: nil)
83
+ Native.to_document(data, format_symbol(format))
84
+ end
85
+
86
+ # Detect the format from the content itself: the signature and identity
87
+ # each container specification designates (PDF header, RTF open group, OLE
88
+ # stream names, ZIP package mimetype/content types).
89
+ #
90
+ # @param data [String] the document's bytes.
91
+ # @return [Symbol, nil] +nil+ for plain-text formats, which carry no
92
+ # signature, and for anything unrecognized.
93
+ def format_from_bytes(data)
94
+ Native.format_from_bytes(data)
95
+ end
96
+
97
+ # The format an extension names, with or without a leading dot, matched
98
+ # case-insensitively.
99
+ #
100
+ # @param extension [String, Symbol] e.g. +".pptm"+.
101
+ # @return [Symbol, nil] +nil+ for anything unrecognized.
102
+ def format_from_extension(extension)
103
+ Native.format_from_extension(extension.to_s.delete_prefix("."))
104
+ end
105
+
106
+ # The format a path's extension names.
107
+ #
108
+ # @param path [String, Pathname] the path to read the extension off.
109
+ # @return [Symbol, nil] +nil+ when the path has no extension or names
110
+ # nothing recognized.
111
+ def format_from_path(path)
112
+ extension = File.extname(File.path(path))
113
+ extension.empty? ? nil : format_from_extension(extension)
114
+ end
115
+
116
+ private
117
+
118
+ # Format arguments are symbols; strings are accepted for convenience.
119
+ def format_symbol(format)
120
+ case format
121
+ when nil, Symbol then format
122
+ when String then format.to_sym
123
+ else
124
+ raise ArgumentError, "format must be a Symbol, a String, or nil, got #{format.class}"
125
+ end
126
+ end
127
+ end
128
+ end
data/sig/anydoc.rbs ADDED
@@ -0,0 +1,163 @@
1
+ # Type signatures for anydoc-ruby.
2
+
3
+ module Anydoc
4
+ VERSION: String
5
+
6
+ # Every format anydoc reads, named after the extension that identifies it.
7
+ FORMATS: Array[Symbol]
8
+
9
+ type format = Symbol | String
10
+ type path = String | ::Pathname | _ToPath
11
+
12
+ def self.to_markdown: (path path) -> String
13
+ def self.to_markdown_bytes: (String data, ?format: format?) -> String
14
+ def self.to_document: (String data, ?format: format?) -> Document
15
+ def self.format_from_bytes: (String data) -> Symbol?
16
+ def self.format_from_extension: (String | Symbol extension) -> Symbol?
17
+ def self.format_from_path: (path path) -> Symbol?
18
+
19
+ # Argument coercion and file reading happen in Ruby; these are the raw
20
+ # entry points the extension defines.
21
+ #
22
+ # @api private
23
+ module Native
24
+ def self.to_markdown_bytes: (String data, Symbol? format) -> String
25
+ def self.to_document: (String data, Symbol? format) -> Document
26
+ def self.format_from_bytes: (String data) -> Symbol?
27
+ def self.format_from_extension: (String extension) -> Symbol?
28
+ end
29
+
30
+ class Error < StandardError
31
+ end
32
+
33
+ class ConvertError < Error
34
+ end
35
+
36
+ class UnsupportedError < ConvertError
37
+ end
38
+
39
+ class MalformedError < ConvertError
40
+ attr_reader part: String?
41
+ end
42
+
43
+ class EncryptedError < ConvertError
44
+ end
45
+
46
+ class ResourceLimitError < ConvertError
47
+ attr_reader limit: Symbol
48
+ end
49
+
50
+ class MissingPartError < ConvertError
51
+ attr_reader part: String
52
+ end
53
+
54
+ class Document < ::Data
55
+ attr_reader blocks: Array[Block]
56
+ attr_reader notes: Array[Note]
57
+ attr_reader assets: Array[Asset]
58
+
59
+ def self.new: (Array[Block] blocks, Array[Note] notes, Array[Asset] assets) -> Document
60
+ | (blocks: Array[Block], notes: Array[Note], assets: Array[Asset]) -> Document
61
+ end
62
+
63
+ class Block < ::Data
64
+ # :heading | :paragraph | :list | :table | :block_quote | :code_block | :rule
65
+ attr_reader kind: Symbol
66
+ attr_reader level: Integer?
67
+ attr_reader anchor: String?
68
+ attr_reader content: Array[Inline]?
69
+ attr_reader list: List?
70
+ attr_reader table: Table?
71
+ attr_reader blocks: Array[Block]?
72
+ attr_reader lang: String?
73
+ attr_reader text: String?
74
+ end
75
+
76
+ class Inline < ::Data
77
+ # :text | :link | :image | :anchor | :note_ref | :line_break
78
+ attr_reader kind: Symbol
79
+ attr_reader text: String?
80
+ attr_reader style: Style?
81
+ attr_reader content: Array[Inline]?
82
+ attr_reader target: LinkTarget?
83
+ attr_reader alt: String?
84
+ attr_reader source: ImageSource?
85
+ attr_reader anchor: String?
86
+ attr_reader note_id: String?
87
+ end
88
+
89
+ class Style < ::Data
90
+ attr_reader bold: bool
91
+ attr_reader italic: bool
92
+ attr_reader strike: bool
93
+ attr_reader code: bool
94
+
95
+ def self.new: (bool bold, bool italic, bool strike, bool code) -> Style
96
+ | (bold: bool, italic: bool, strike: bool, code: bool) -> Style
97
+
98
+ def plain?: () -> bool
99
+ end
100
+
101
+ class LinkTarget < ::Data
102
+ # :external | :relative | :anchor
103
+ attr_reader kind: Symbol
104
+ attr_reader value: String
105
+ end
106
+
107
+ class ImageSource < ::Data
108
+ # :external | :asset | :unavailable
109
+ attr_reader kind: Symbol
110
+ attr_reader url: String?
111
+ attr_reader asset_id: Integer?
112
+ end
113
+
114
+ class List < ::Data
115
+ # :bullet | :decimal | :lower_alpha | :upper_alpha | :lower_roman | :upper_roman
116
+ attr_reader marker: Symbol
117
+ attr_reader start: Integer
118
+ attr_reader items: Array[ListItem]
119
+
120
+ def ordered?: () -> bool
121
+ end
122
+
123
+ class ListItem < ::Data
124
+ attr_reader blocks: Array[Block]
125
+ attr_reader checked: bool?
126
+ attr_reader marker_label: String?
127
+ end
128
+
129
+ class Table < ::Data
130
+ attr_reader grid: Array[Array[CellSlot]]
131
+ attr_reader header_rows: Integer
132
+ # :data | :layout
133
+ attr_reader kind: Symbol
134
+ end
135
+
136
+ class CellSlot < ::Data
137
+ # :origin | :covered
138
+ attr_reader kind: Symbol
139
+ attr_reader cell: Cell?
140
+ attr_reader origin_row: Integer?
141
+ attr_reader origin_col: Integer?
142
+ end
143
+
144
+ class Cell < ::Data
145
+ attr_reader blocks: Array[Block]
146
+ attr_reader col_span: Integer
147
+ attr_reader row_span: Integer
148
+ end
149
+
150
+ class Note < ::Data
151
+ attr_reader id: String
152
+ # :footnote | :endnote
153
+ attr_reader kind: Symbol
154
+ attr_reader blocks: Array[Block]
155
+ end
156
+
157
+ class Asset < ::Data
158
+ attr_reader id: Integer
159
+ attr_reader media_type: String
160
+ attr_reader origin_part: String
161
+ attr_reader data: String
162
+ end
163
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anydoc-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.7
5
+ platform: aarch64-linux
6
+ authors:
7
+ - Jan Sterba
8
+ - Firecrawl
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2026-08-06 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Ruby bindings for the anydoc Rust crate. Converts Word, PowerPoint, Excel,
15
+ OpenDocument, RTF, EPUB, CSV, and PDF documents into clean GitHub-Flavored Markdown,
16
+ with one consistent output no matter which format goes in.
17
+ email:
18
+ - info@jansterba.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - LICENSE
24
+ - README.md
25
+ - lib/anydoc.rb
26
+ - lib/anydoc/3.2/anydoc.so
27
+ - lib/anydoc/3.3/anydoc.so
28
+ - lib/anydoc/3.4/anydoc.so
29
+ - lib/anydoc/4.0/anydoc.so
30
+ - lib/anydoc/document.rb
31
+ - lib/anydoc/errors.rb
32
+ - lib/anydoc/version.rb
33
+ - sig/anydoc.rbs
34
+ homepage: https://github.com/honzasterba/anydoc#readme
35
+ licenses:
36
+ - MIT
37
+ metadata:
38
+ homepage_uri: https://github.com/honzasterba/anydoc#readme
39
+ source_code_uri: https://github.com/honzasterba/anydoc/tree/main/ruby
40
+ bug_tracker_uri: https://github.com/honzasterba/anydoc/issues
41
+ changelog_uri: https://github.com/honzasterba/anydoc/releases
42
+ documentation_uri: https://github.com/honzasterba/anydoc/blob/main/ruby/README.md
43
+ rubygems_mfa_required: 'true'
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '3.2'
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: 4.1.dev
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.5.23
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Convert documents (doc, docx, odt, rtf, epub, pdf, presentations, spreadsheets,
66
+ csv) to GitHub-Flavored Markdown
67
+ test_files: []