etassia-native 0.1.0-x64-mingw-ucrt

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: 1eed9202ef7ece01570619ae28ce3d7f9a9815e12e0e348fb95ed29f3e01e6b2
4
+ data.tar.gz: f86d98f5329e4d67e666e931e282020d78eb16ac46b99ca5482f05f93dd7ef66
5
+ SHA512:
6
+ metadata.gz: a8f5c2df0816d15804345935503dfd5bec004cbd3b0b95a3e1e9dbf50eb5e35ef38f58fcce298fb6ed78d1bd79c88d4993a7475c14f16b569069c430b7f84a12
7
+ data.tar.gz: c1504a177eb8ea6962541c11c9426f867cf9bd89e9b5158c84e3b6db18e38eeff85847f62bbea081b466f5b48493ce386ef28b6c27a88d9df9dc265c8fc36047
Binary file
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Etassia
4
+ module Native
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rbconfig"
5
+ require "ffi"
6
+
7
+ module Etassia
8
+ # Renders through the etassia-ffi C ABI (see crates/etassia-ffi/README.md).
9
+ #
10
+ # The shared library is looked up in this order:
11
+ # 1. ENV["ETASSIA_NATIVE_LIB"] — exact file path; when set, nothing else is searched.
12
+ # 2. lib/etassia/native/ inside an installed platform gem (precompiled, no compile at install).
13
+ # 3. <repo>/target/release, then <repo>/target/debug.
14
+ # When no library is found, or the Rust core answers `Unported`, render
15
+ # falls back to a pure-Ruby callable (default: the `etassia` gem's Etassia.render).
16
+ module Native
17
+ LIBRARY_ENV = "ETASSIA_NATIVE_LIB"
18
+ REPO_ROOT = File.expand_path("../../../..", __dir__)
19
+ PACKAGED_DIR = File.join(__dir__, "native").freeze
20
+ SEARCH_DIRS = [PACKAGED_DIR,
21
+ File.join(REPO_ROOT, "target", "release"),
22
+ File.join(REPO_ROOT, "target", "debug")].freeze
23
+
24
+ # The Rust core declined this diagram type; the caller should fall back.
25
+ class Unported < StandardError; end
26
+
27
+ # Any other error the native library reports (parse, encoding, ...). Never falls back.
28
+ class RenderError < StandardError
29
+ attr_reader :error_class, :line, :col
30
+
31
+ def initialize(error_class, message, line, col)
32
+ @error_class = error_class
33
+ @line = line
34
+ @col = col
35
+ super("#{error_class} at #{line}:#{col}: #{message}")
36
+ end
37
+ end
38
+
39
+ # Neither the native library nor the pure-Ruby fallback can render.
40
+ class Unavailable < StandardError; end
41
+
42
+ # Loads one cdylib and calls its three entry points.
43
+ class Library
44
+ # Matches `struct EtassiaBuf` in crates/etassia-ffi/src/lib.rs (24 bytes on 64-bit).
45
+ class Buf < FFI::Struct
46
+ layout :ptr, :pointer,
47
+ :len, :size_t,
48
+ :status, :int
49
+ end
50
+
51
+ attr_reader :path
52
+
53
+ def initialize(path)
54
+ @path = path
55
+ buf = Buf
56
+ @lib = Module.new do
57
+ extend FFI::Library
58
+
59
+ ffi_lib path
60
+ attach_function :etassia_render, %i[pointer size_t pointer], buf.by_value
61
+ attach_function :etassia_detect, %i[pointer size_t], buf.by_value
62
+ attach_function :etassia_free, [buf.by_value], :void
63
+ end
64
+ end
65
+
66
+ def render(source)
67
+ ptr, len = input(source)
68
+ take(@lib.etassia_render(ptr, len, nil))
69
+ end
70
+
71
+ def detect(source)
72
+ ptr, len = input(source)
73
+ take(@lib.etassia_detect(ptr, len))
74
+ end
75
+
76
+ private
77
+
78
+ def input(source)
79
+ bytes = source.to_s.encode(Encoding::UTF_8).b
80
+ ptr = FFI::MemoryPointer.new(:uint8, [bytes.bytesize, 1].max)
81
+ ptr.put_bytes(0, bytes)
82
+ [ptr, bytes.bytesize]
83
+ end
84
+
85
+ # Copies the buffer out, always frees it, then maps status to result or error.
86
+ def take(buf)
87
+ body = buf[:ptr].read_bytes(buf[:len]).force_encoding(Encoding::UTF_8)
88
+ status = buf[:status]
89
+ @lib.etassia_free(buf)
90
+ return body if status.zero?
91
+
92
+ raise_error(body)
93
+ end
94
+
95
+ def raise_error(json)
96
+ info = JSON.parse(json)
97
+ raise Unported, info["message"] if info["class"] == "Unported"
98
+
99
+ raise RenderError.new(info["class"], info["message"], info["line"], info["col"])
100
+ end
101
+ end
102
+
103
+ class << self
104
+ # Pure-Ruby renderer used when the native path cannot answer. Injectable.
105
+ attr_writer :fallback
106
+
107
+ def fallback
108
+ @fallback ||= method(:pure_ruby_render)
109
+ end
110
+
111
+ # Path of the library that would be loaded, or nil.
112
+ def library_path
113
+ override = ENV.fetch(LIBRARY_ENV, nil)
114
+ return (File.file?(override) ? override : nil) if override && !override.empty?
115
+
116
+ SEARCH_DIRS.map { |dir| File.join(dir, library_filename) }.find { |path| File.file?(path) }
117
+ end
118
+
119
+ def available?
120
+ !library_path.nil?
121
+ end
122
+
123
+ # SVG for `source`. Uses the native library, or the fallback when it is
124
+ # missing or reports Unported. Other native errors raise RenderError.
125
+ def render(source, fallback: self.fallback)
126
+ lib = library
127
+ return fallback.call(source) if lib.nil?
128
+
129
+ lib.render(source)
130
+ rescue Unported
131
+ fallback.call(source)
132
+ end
133
+
134
+ # Diagram type name for `source`. Native only; raises Unavailable without a library.
135
+ def detect(source)
136
+ lib = library or raise Unavailable,
137
+ "#{LIBRARY_ENV} not set and no #{library_filename} in the gem or under target/"
138
+
139
+ lib.detect(source)
140
+ end
141
+
142
+ def library
143
+ path = library_path
144
+ return nil if path.nil?
145
+
146
+ @libraries ||= {}
147
+ @libraries[path] ||= Library.new(path)
148
+ end
149
+
150
+ private
151
+
152
+ def library_filename
153
+ case RbConfig::CONFIG["host_os"]
154
+ when /darwin/ then "libetassia_ffi.dylib"
155
+ when /mswin|mingw/ then "etassia_ffi.dll"
156
+ else "libetassia_ffi.so"
157
+ end
158
+ end
159
+
160
+ def pure_ruby_render(source)
161
+ begin
162
+ require "etassia"
163
+ rescue LoadError
164
+ raise Unavailable, "native library not found and the pure-Ruby 'etassia' gem is not installed"
165
+ end
166
+ raise Unavailable, "the 'etassia' gem does not define Etassia.render" unless Etassia.respond_to?(:render)
167
+
168
+ Etassia.render(source)
169
+ end
170
+ end
171
+ end
172
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: etassia-native
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: x64-mingw-ucrt
6
+ authors:
7
+ - Etassia contributors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ description:
28
+ email:
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/etassia/native.rb
34
+ - lib/etassia/native/etassia_ffi.dll
35
+ - lib/etassia/native/version.rb
36
+ homepage:
37
+ licenses:
38
+ - BSD-2-Clause
39
+ metadata:
40
+ rubygems_mfa_required: 'true'
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.1'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.5.23
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Ruby binding to the etassia-ffi C ABI, with a pure-Ruby fallback
60
+ test_files: []