mq-ruby 0.1.0

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.
Files changed (11) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +1238 -0
  3. data/Cargo.toml +23 -0
  4. data/LICENSE +21 -0
  5. data/README.md +51 -0
  6. data/extconf.rb +6 -0
  7. data/lib/mq.rb +70 -0
  8. data/src/lib.rs +106 -0
  9. data/src/result.rs +99 -0
  10. data/src/value.rs +124 -0
  11. metadata +110 -0
data/Cargo.toml ADDED
@@ -0,0 +1,23 @@
1
+ [package]
2
+ authors = ["Takahiro Sato <harehare1110@gmail.com>"]
3
+ categories = ["command-line-utilities", "text-processing"]
4
+ description = "Ruby bindings for mq Markdown processing"
5
+ edition = "2024"
6
+ homepage = "https://mqlang.org/"
7
+ keywords = ["markdown", "jq", "query", "ruby"]
8
+ license = "MIT"
9
+ name = "mq-ruby"
10
+ publish = false
11
+ readme = "README.md"
12
+ repository = "https://github.com/harehare/mq"
13
+ version = "0.1.0"
14
+
15
+ [lib]
16
+ crate-type = ["cdylib"]
17
+ name = "mq_ruby"
18
+
19
+ [dependencies]
20
+ magnus = {version = "0.8"}
21
+ mq-lang = "0.5.9"
22
+ mq-markdown = "0.5.9"
23
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Takahiro Sato
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,51 @@
1
+ # mq-ruby
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ Ruby bindings for [mq](https://mqlang.org/), a jq-like command-line tool for processing Markdown.
6
+
7
+ ## Ruby API
8
+
9
+ Once complete, the Ruby API will look like this:
10
+
11
+ ```ruby
12
+ require 'mq'
13
+
14
+ # Basic usage
15
+ markdown = <<~MD
16
+ # Main Title
17
+ ## Section 1
18
+ Some content here.
19
+ ## Section 2
20
+ More content.
21
+ MD
22
+
23
+ result = MQ.run('.h2', markdown)
24
+ result.values.each do |heading|
25
+ puts heading
26
+ end
27
+ # => ## Section 1
28
+ # => ## Section 2
29
+
30
+ # With options
31
+ options = MQ::Options.new
32
+ options.input_format = MQ::InputFormat::HTML
33
+
34
+ result = MQ.run('.h1', '<h1>Hello</h1><p>World</p>', options)
35
+ puts result.text # => # Hello
36
+
37
+ # HTML to Markdown conversion
38
+ html = '<h1>Title</h1><p>Paragraph</p>'
39
+ markdown = MQ.html_to_markdown(html)
40
+ puts markdown # => # Title\n\nParagraph
41
+ ```
42
+
43
+ ## License
44
+
45
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
46
+
47
+ ## Links
48
+
49
+ - [mq Website](https://mqlang.org/)
50
+ - [GitHub Repository](https://github.com/harehare/mq)
51
+ - [Command-line Tool](https://github.com/harehare/mq#installation)
data/extconf.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("mq/mq_ruby")
data/lib/mq.rb ADDED
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ # Try to load the compiled extension
5
+ RUBY_VERSION =~ /(\d+\.\d+)/
6
+ require_relative "mq/#{Regexp.last_match(1)}/mq_ruby"
7
+ rescue LoadError
8
+ require_relative "mq/mq_ruby"
9
+ end
10
+
11
+ module MQ
12
+ class Error < StandardError; end
13
+
14
+ # Options class for configuring mq queries
15
+ class Options
16
+ attr_accessor :input_format
17
+
18
+ def initialize
19
+ @input_format = nil
20
+ end
21
+
22
+ def to_h
23
+ {
24
+ input_format: @input_format,
25
+ }.compact
26
+ end
27
+ end
28
+
29
+ # Conversion options for HTML to Markdown conversion
30
+ class ConversionOptions
31
+ attr_accessor :extract_scripts_as_code_blocks, :generate_front_matter, :use_title_as_h1
32
+
33
+ def initialize
34
+ @extract_scripts_as_code_blocks = false
35
+ @generate_front_matter = false
36
+ @use_title_as_h1 = false
37
+ end
38
+
39
+ def to_h
40
+ {
41
+ extract_scripts_as_code_blocks: @extract_scripts_as_code_blocks,
42
+ generate_front_matter: @generate_front_matter,
43
+ use_title_as_h1: @use_title_as_h1
44
+ }
45
+ end
46
+ end
47
+
48
+ class << self
49
+ # Run an mq query on the provided content
50
+ #
51
+ # @param code [String] The mq query string
52
+ # @param content [String] The markdown/HTML/text content to process
53
+ # @param options [Options, nil] Optional configuration options
54
+ # @return [Result] The query results
55
+ def run(code, content, options = nil)
56
+ options_hash = options&.to_h
57
+ _run(code, content, options_hash)
58
+ end
59
+
60
+ # Convert HTML to Markdown
61
+ #
62
+ # @param content [String] The HTML content to convert
63
+ # @param options [ConversionOptions, nil] Optional conversion options
64
+ # @return [String] The converted Markdown
65
+ def html_to_markdown(content, options = nil)
66
+ options_hash = options&.to_h
67
+ _html_to_markdown(content, options_hash)
68
+ end
69
+ end
70
+ end
data/src/lib.rs ADDED
@@ -0,0 +1,106 @@
1
+ //! Ruby bindings for the mq markdown processing library.
2
+ //!
3
+ //! This crate provides Ruby bindings for mq, allowing Ruby applications to
4
+ //! process markdown, MDX, and HTML using the mq query language.
5
+
6
+ pub mod result;
7
+ pub mod value;
8
+
9
+ use magnus::{Error, RHash, Ruby, TryConvert, function, prelude::*};
10
+ use result::MQResult;
11
+ use value::InputFormat;
12
+
13
+ /// Main entry point for the Ruby extension
14
+ #[magnus::init]
15
+ fn init(ruby: &Ruby) -> Result<(), Error> {
16
+ let mq_module = ruby.define_module("MQ")?;
17
+
18
+ // Define input format constants
19
+ InputFormat::define_constants(ruby, mq_module)?;
20
+
21
+ // Define result class
22
+ MQResult::define_class(ruby, mq_module)?;
23
+
24
+ // Define module functions
25
+ mq_module.define_singleton_method("_run", function!(run, 3))?;
26
+ mq_module.define_singleton_method("_html_to_markdown", function!(html_to_markdown, 2))?;
27
+
28
+ Ok(())
29
+ }
30
+
31
+ /// Run an mq query on the provided content
32
+ fn run(code: String, content: String, options_hash: Option<RHash>) -> Result<MQResult, Error> {
33
+ let ruby = Ruby::get().unwrap();
34
+ let mut engine = mq_lang::DefaultEngine::default();
35
+ engine.load_builtin_module();
36
+
37
+ // Parse options from hash
38
+ let input_format = if let Some(opts) = options_hash {
39
+ if let Some(val) = opts.get(ruby.to_symbol("input_format")) {
40
+ let format_val: i32 = TryConvert::try_convert(val)?;
41
+ InputFormat::from_i32(format_val)
42
+ } else {
43
+ InputFormat::Markdown
44
+ }
45
+ } else {
46
+ InputFormat::Markdown
47
+ };
48
+
49
+ let input = match input_format {
50
+ InputFormat::Markdown => mq_lang::parse_markdown_input(&content),
51
+ InputFormat::Mdx => mq_lang::parse_mdx_input(&content),
52
+ InputFormat::Text => mq_lang::parse_text_input(&content),
53
+ InputFormat::Html => mq_lang::parse_html_input(&content),
54
+ InputFormat::Raw => Ok(mq_lang::raw_input(&content)),
55
+ InputFormat::Null => Ok(mq_lang::null_input()),
56
+ }
57
+ .map_err(|e| {
58
+ Error::new(
59
+ ruby.exception_runtime_error(),
60
+ format!("Error parsing input: {}", e),
61
+ )
62
+ })?;
63
+
64
+ engine
65
+ .eval(&code, input.into_iter())
66
+ .map(|values| MQResult::from(values.into_iter().map(Into::into).collect::<Vec<_>>()))
67
+ .map_err(|e| {
68
+ Error::new(
69
+ ruby.exception_runtime_error(),
70
+ format!("Error evaluating query: {}", e),
71
+ )
72
+ })
73
+ }
74
+
75
+ /// Convert HTML to Markdown
76
+ fn html_to_markdown(content: String, options_hash: Option<RHash>) -> Result<String, Error> {
77
+ let ruby = Ruby::get().unwrap();
78
+ let opts = if let Some(opts) = options_hash {
79
+ let extract_scripts = get_bool_option(&ruby, &opts, "extract_scripts_as_code_blocks")?;
80
+ let generate_front = get_bool_option(&ruby, &opts, "generate_front_matter")?;
81
+ let use_title = get_bool_option(&ruby, &opts, "use_title_as_h1")?;
82
+
83
+ mq_markdown::ConversionOptions {
84
+ extract_scripts_as_code_blocks: extract_scripts,
85
+ generate_front_matter: generate_front,
86
+ use_title_as_h1: use_title,
87
+ }
88
+ } else {
89
+ mq_markdown::ConversionOptions::default()
90
+ };
91
+
92
+ mq_markdown::convert_html_to_markdown(&content, opts).map_err(|e| {
93
+ Error::new(
94
+ ruby.exception_runtime_error(),
95
+ format!("Error converting HTML to Markdown: {}", e),
96
+ )
97
+ })
98
+ }
99
+
100
+ fn get_bool_option(ruby: &Ruby, hash: &RHash, key: &str) -> Result<bool, Error> {
101
+ if let Some(val) = hash.get(ruby.to_symbol(key)) {
102
+ TryConvert::try_convert(val)
103
+ } else {
104
+ Ok(false)
105
+ }
106
+ }
data/src/result.rs ADDED
@@ -0,0 +1,99 @@
1
+ use crate::value::MQValue;
2
+ use magnus::{DataTypeFunctions, Error, RModule, Ruby, TypedData, Value, method, prelude::*};
3
+
4
+ /// Result of an mq query execution
5
+ #[derive(Debug, Clone, TypedData)]
6
+ #[magnus(class = "MQ::Result", free_immediately, mark)]
7
+ pub struct MQResult {
8
+ pub values: Vec<MQValue>,
9
+ }
10
+
11
+ impl DataTypeFunctions for MQResult {}
12
+
13
+ impl MQResult {
14
+ /// Define the MQResult class in Ruby
15
+ pub fn define_class(ruby: &Ruby, mq_module: RModule) -> Result<(), Error> {
16
+ let class = mq_module.define_class("Result", ruby.class_object())?;
17
+ class.define_method("text", method!(MQResult::text, 0))?;
18
+ class.define_method("values", method!(MQResult::values_as_strings, 0))?;
19
+ class.define_method("length", method!(MQResult::len, 0))?;
20
+ class.define_method("[]", method!(MQResult::get_at, 1))?;
21
+ class.define_method("each", method!(MQResult::each, 0))?;
22
+ Ok(())
23
+ }
24
+
25
+ /// Get the text representation of all values joined by newlines
26
+ pub fn text(&self) -> String {
27
+ self.values
28
+ .iter()
29
+ .filter_map(|value| {
30
+ if value.is_empty() {
31
+ None
32
+ } else {
33
+ Some(value.text())
34
+ }
35
+ })
36
+ .collect::<Vec<String>>()
37
+ .join("\n")
38
+ }
39
+
40
+ /// Get an array of text values
41
+ pub fn values_as_strings(&self) -> Vec<String> {
42
+ self.values
43
+ .iter()
44
+ .filter_map(|value| {
45
+ if value.is_empty() {
46
+ None
47
+ } else {
48
+ Some(value.text())
49
+ }
50
+ })
51
+ .collect()
52
+ }
53
+
54
+ /// Get the number of values
55
+ pub fn len(&self) -> usize {
56
+ self.values.len()
57
+ }
58
+
59
+ /// Check if the result is empty
60
+ pub fn is_empty(&self) -> bool {
61
+ self.values.is_empty()
62
+ }
63
+
64
+ /// Access a value by index (for Ruby)
65
+ fn get_at(&self, idx: usize) -> Result<String, Error> {
66
+ if idx < self.values.len() {
67
+ Ok(self.values[idx].text())
68
+ } else {
69
+ let ruby = Ruby::get().unwrap();
70
+ Err(Error::new(
71
+ ruby.exception_runtime_error(),
72
+ format!(
73
+ "Index {} out of range for MQResult with length {}",
74
+ idx,
75
+ self.len()
76
+ ),
77
+ ))
78
+ }
79
+ }
80
+
81
+ /// Iterator for Ruby each method
82
+ fn each(&self) -> Result<Value, Error> {
83
+ let ruby = Ruby::get().unwrap();
84
+ let block = ruby.block_proc()?;
85
+
86
+ for value in &self.values {
87
+ let text = value.text();
88
+ block.call::<(String,), Value>((text,))?;
89
+ }
90
+
91
+ Ok(ruby.qnil().as_value())
92
+ }
93
+ }
94
+
95
+ impl From<Vec<MQValue>> for MQResult {
96
+ fn from(values: Vec<MQValue>) -> Self {
97
+ Self { values }
98
+ }
99
+ }
data/src/value.rs ADDED
@@ -0,0 +1,124 @@
1
+ use magnus::{Error, Module, RModule, Ruby};
2
+ use std::{collections::HashMap, fmt};
3
+
4
+ #[derive(Debug, Clone, Copy, PartialEq, Default)]
5
+ pub enum InputFormat {
6
+ #[default]
7
+ Markdown,
8
+ Mdx,
9
+ Text,
10
+ Html,
11
+ Raw,
12
+ Null,
13
+ }
14
+
15
+ impl InputFormat {
16
+ pub fn define_constants(ruby: &Ruby, mq_module: RModule) -> Result<(), Error> {
17
+ let class = ruby.define_class("InputFormat", ruby.class_object())?;
18
+ mq_module.const_set("InputFormat", class)?;
19
+ class.const_set("MARKDOWN", 0)?;
20
+ class.const_set("MDX", 1)?;
21
+ class.const_set("TEXT", 2)?;
22
+ class.const_set("HTML", 3)?;
23
+ class.const_set("RAW", 4)?;
24
+ class.const_set("NULL", 5)?;
25
+ Ok(())
26
+ }
27
+
28
+ pub fn from_i32(val: i32) -> Self {
29
+ match val {
30
+ 0 => InputFormat::Markdown,
31
+ 1 => InputFormat::Mdx,
32
+ 2 => InputFormat::Text,
33
+ 3 => InputFormat::Html,
34
+ 4 => InputFormat::Raw,
35
+ 5 => InputFormat::Null,
36
+ _ => InputFormat::Markdown, // Default
37
+ }
38
+ }
39
+ }
40
+
41
+ #[derive(Debug, Clone)]
42
+ pub enum MQValue {
43
+ Array { value: Vec<MQValue> },
44
+ Dict { value: HashMap<String, MQValue> },
45
+ Markdown { value: String },
46
+ }
47
+
48
+ impl fmt::Display for MQValue {
49
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50
+ match self {
51
+ MQValue::Array { value } => write!(
52
+ f,
53
+ "{}",
54
+ value
55
+ .iter()
56
+ .map(|val| val.text())
57
+ .collect::<Vec<String>>()
58
+ .join("\n")
59
+ ),
60
+ MQValue::Dict { value } => write!(
61
+ f,
62
+ "{}",
63
+ value
64
+ .iter()
65
+ .map(|(k, v)| format!("{}: {}", k, v.text()))
66
+ .collect::<Vec<String>>()
67
+ .join("\n")
68
+ ),
69
+ MQValue::Markdown { value } => write!(f, "{}", value),
70
+ }
71
+ }
72
+ }
73
+
74
+ impl MQValue {
75
+ pub fn text(&self) -> String {
76
+ self.to_string()
77
+ }
78
+
79
+ pub fn is_empty(&self) -> bool {
80
+ match self {
81
+ MQValue::Array { value } => value.is_empty(),
82
+ MQValue::Dict { value } => value.is_empty(),
83
+ MQValue::Markdown { value } => value.is_empty(),
84
+ }
85
+ }
86
+ }
87
+
88
+ impl From<mq_lang::RuntimeValue> for MQValue {
89
+ fn from(value: mq_lang::RuntimeValue) -> Self {
90
+ match value {
91
+ mq_lang::RuntimeValue::Array(arr) => MQValue::Array {
92
+ value: arr.into_iter().map(|v| v.into()).collect(),
93
+ },
94
+ mq_lang::RuntimeValue::Dict(map) => MQValue::Dict {
95
+ value: map
96
+ .into_iter()
97
+ .map(|(k, v)| (k.as_str(), v.into()))
98
+ .collect(),
99
+ },
100
+ mq_lang::RuntimeValue::Markdown(node, _) => MQValue::Markdown {
101
+ value: node.to_string(),
102
+ },
103
+ mq_lang::RuntimeValue::String(s) => MQValue::Markdown { value: s },
104
+ mq_lang::RuntimeValue::Symbol(i) => MQValue::Markdown { value: i.as_str() },
105
+ mq_lang::RuntimeValue::Number(n) => MQValue::Markdown {
106
+ value: n.to_string(),
107
+ },
108
+ mq_lang::RuntimeValue::Boolean(b) => MQValue::Markdown {
109
+ value: b.to_string(),
110
+ },
111
+ mq_lang::RuntimeValue::Function(..)
112
+ | mq_lang::RuntimeValue::NativeFunction(..)
113
+ | mq_lang::RuntimeValue::Module(..) => MQValue::Markdown {
114
+ value: "".to_string(),
115
+ },
116
+ mq_lang::RuntimeValue::Ast(_) => MQValue::Markdown {
117
+ value: "".to_string(),
118
+ },
119
+ mq_lang::RuntimeValue::None => MQValue::Markdown {
120
+ value: "".to_string(),
121
+ },
122
+ }
123
+ }
124
+ }
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mq-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Takahiro Sato
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake-compiler
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.2'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.2'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rb_sys
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.9'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.9'
68
+ description: mq is a jq-like command-line tool for Markdown processing. This gem provides
69
+ Ruby bindings for mq.
70
+ email:
71
+ - harehare1110@gmail.com
72
+ executables: []
73
+ extensions:
74
+ - extconf.rb
75
+ extra_rdoc_files: []
76
+ files:
77
+ - Cargo.lock
78
+ - Cargo.toml
79
+ - LICENSE
80
+ - README.md
81
+ - extconf.rb
82
+ - lib/mq.rb
83
+ - src/lib.rs
84
+ - src/result.rs
85
+ - src/value.rs
86
+ homepage: https://mqlang.org/
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ homepage_uri: https://mqlang.org/
91
+ source_code_uri: https://github.com/harehare/mq
92
+ changelog_uri: https://github.com/harehare/mq/blob/main/crates/mq-ruby/CHANGELOG.md
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.0.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.6.9
108
+ specification_version: 4
109
+ summary: Ruby bindings for mq Markdown processing
110
+ test_files: []