mdx-tex 0.1.13 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8343fef64d295ffcb1ceeebe1486fe2311bbfd8a1c8b0f8bdcf3f95ac1114ce5
4
- data.tar.gz: 8606c4ccb96a3494499e221d3c7c6d7c816234bb75f90c11dd4f7889a487043e
3
+ metadata.gz: c30f6b611b9feb5094eb58bff7920f23bdac921d2eb8d4bf82a5700516672d43
4
+ data.tar.gz: c571088c6cae7523ce3ece74dc648f10b08ba74c9b53fafa262c78c37e981a01
5
5
  SHA512:
6
- metadata.gz: 071b049091b240866d57edd34dd35ee53052d012a32ab9c573ad3156eb557c5e4ae57d65fa7f55d7f1a7338fb34bb75ca063db57a819a6094a3ae678f3e327fa
7
- data.tar.gz: 1edcbbdd10d9b07461281e43c8b6455e3f48083ca660b85acd0ee0f9ed26c5181645852c5f608d3e475b9266452bbb78e5e34a2bce2747e3959664a38a0be748
6
+ metadata.gz: 45806c1388c49103d659f12ad50f144081477f479db973a9ad2900dbfb7eacb1e569632fbdf8adf7958dd1de331ba40ed2bc71ffdc2abca5f5f759eebace4584
7
+ data.tar.gz: 737d4a0f4c3a07ded53396994ed78d38ee7acdfb40ea4900fb182c6628e5bff957892e8d259d040b9eb28b74e231d2204a42326490e28ddf9c1ee8fec57b816a
@@ -6,6 +6,10 @@ module MdxTex
6
6
  def to_textile(**options)
7
7
  MdxTex.to_textile(markdown: self, **options)
8
8
  end
9
+
10
+ def to_markdown
11
+ MdxTex.to_markdown(textile: self)
12
+ end
9
13
  end
10
14
  end
11
15
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MdxTex
4
+ class ToMarkdown
5
+ # Converts Textile bold syntax to Markdown bold syntax.
6
+ # Textile bold is *text* with non-whitespace immediately inside both asterisks;
7
+ # whitespace-padded asterisks are not bold (and are typically list markers).
8
+ #
9
+ # | Input (Textile) | Output (Markdown) |
10
+ # |-------------------|-------------------|
11
+ # | *hello* | **hello** |
12
+ # | *a* and *b* | **a** and **b** |
13
+ # | * hello * | * hello * |
14
+ module Bold
15
+ # Matches a Textile bold span: *...*
16
+ # \* opening literal *
17
+ # ( capture the content
18
+ # [^\s*] first char must be non-whitespace and non-*
19
+ # (Textile rule: no padding inside the asterisks;
20
+ # also keeps us from matching list markers like `* item`)
21
+ # (?: optional trailing run, present only for 2+ char content
22
+ # [^*]*? any chars except *, non-greedy so we stop at the
23
+ # nearest closing * rather than spanning into another span
24
+ # [^\s*] last char must also be non-whitespace and non-*
25
+ # )? optional, so single-char bold like *a* still matches
26
+ # )
27
+ # \* closing literal *
28
+ PATTERN = /\*([^\s*](?:[^*]*?[^\s*])?)\*/.freeze
29
+
30
+ def self.execute(line)
31
+ line.gsub(PATTERN, '**\1**')
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MdxTex
4
+ class ToMarkdown
5
+ # Converts a Textile heading to a Markdown heading.
6
+ # The Textile tag's level determines the number of leading #s.
7
+ # A space must follow the period for the line to be recognised as a heading.
8
+ # Lines that do not match are returned unchanged.
9
+ #
10
+ # | Input (Textile) | Output (Markdown) |
11
+ # |--------------------|--------------------|
12
+ # | h1. Title | # Title |
13
+ # | h3. Note | ### Note |
14
+ # | h6. Tiny | ###### Tiny |
15
+ # | h3.NoSpace | h3.NoSpace |
16
+ # | h7. TooDeep | h7. TooDeep |
17
+ module Header
18
+ # Matches a Textile heading line: hN. content
19
+ # \A anchor to start of line (no leading whitespace allowed)
20
+ # h literal 'h'
21
+ # ([1-6]) capture the heading level digit, restricted to 1-6
22
+ # (Textile/HTML only have h1..h6)
23
+ # \. literal period
24
+ # \s+ one or more whitespace chars
25
+ # (required: `h3.NoSpace` is not a heading)
26
+ # (.+) capture the heading content (at least one char)
27
+ # \z anchor to end of line
28
+ PATTERN = /\Ah([1-6])\.\s+(.+)\z/.freeze
29
+
30
+ def self.execute(line)
31
+ line.sub(PATTERN) { "#{'#' * ::Regexp.last_match(1).to_i} #{::Regexp.last_match(2)}" }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MdxTex
4
+ class ToMarkdown
5
+ # Converts a Textile ordered list item to a Markdown ordered list item.
6
+ # Depth is derived from the leading-hash count minus +base_list_depth+
7
+ # (the smallest hash count seen anywhere in the document, detected by the
8
+ # coordinator). Markdown indents 2 spaces per depth level. The +number+
9
+ # is supplied by the coordinator, which maintains per-depth counters.
10
+ #
11
+ # | Input (Textile) | base_list_depth | number | Output (Markdown) |
12
+ # |-----------------|-----------------|--------|-------------------|
13
+ # | # item | 1 | 1 | 1. item |
14
+ # | # item | 1 | 5 | 5. item |
15
+ # | ## nested | 1 | 1 | 1. nested |
16
+ # | ## item | 2 | 1 | 1. item |
17
+ module OrderedList
18
+ INDENT_SIZE = 2
19
+
20
+ # Matches a Textile ordered list line: optional leading whitespace, a run
21
+ # of hashes, a mandatory space, and at least one content character.
22
+ # \A start of line
23
+ # \s* tolerate leading whitespace (not preserved in the output)
24
+ # (#+) capture the run of hashes (depth indicator)
25
+ # \s+ one or more whitespace chars
26
+ # (required: distinguishes a list marker from things like
27
+ # `#foo` which is not a Textile ordered list item)
28
+ # (.+) capture the item content
29
+ # \z end of line
30
+ PATTERN = /\A\s*(#+)\s+(.+)\z/.freeze
31
+
32
+ def self.execute(line, base_list_depth:, number:)
33
+ line.sub(PATTERN) do
34
+ depth = ::Regexp.last_match(1).length - base_list_depth + 1
35
+ indent = ' ' * ((depth - 1) * INDENT_SIZE)
36
+ "#{indent}#{number}. #{::Regexp.last_match(2)}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MdxTex
4
+ class ToMarkdown
5
+ # Converts a Textile unordered list item to a Markdown unordered list item.
6
+ # Depth is derived from the leading-asterisk count minus +base_list_depth+
7
+ # (the smallest asterisk count seen anywhere in the document, detected by
8
+ # the coordinator). Markdown indents 2 spaces per depth level.
9
+ #
10
+ # | Input (Textile) | base_list_depth | Output (Markdown) |
11
+ # |-----------------|-----------------|-------------------|
12
+ # | *** item | 3 | - item |
13
+ # | **** nested | 3 | - nested |
14
+ # | ***** deep | 3 | - deep |
15
+ # | * item | 1 | - item |
16
+ # | ** nested | 1 | - nested |
17
+ module UnorderedList
18
+ INDENT_SIZE = 2
19
+
20
+ # Matches a Textile unordered list line: optional leading whitespace, a run
21
+ # of asterisks, a mandatory space, and at least one content character.
22
+ # \A start of line
23
+ # \s* tolerate leading whitespace (not preserved in the output)
24
+ # (\*+) capture the run of asterisks (depth indicator)
25
+ # \s+ one or more whitespace chars
26
+ # (required: this is what distinguishes a list marker from
27
+ # inline bold like `*foo*` or a bare `*`)
28
+ # (.+) capture the item content
29
+ # \z end of line
30
+ PATTERN = /\A\s*(\*+)\s+(.+)\z/.freeze
31
+
32
+ def self.execute(line, base_list_depth:)
33
+ line.sub(PATTERN) do
34
+ depth = ::Regexp.last_match(1).length - base_list_depth + 1
35
+ indent = ' ' * ((depth - 1) * INDENT_SIZE)
36
+ "#{indent}- #{::Regexp.last_match(2)}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mdx_tex/to_markdown/bold'
4
+ require 'mdx_tex/to_markdown/header'
5
+ require 'mdx_tex/to_markdown/unordered_list'
6
+ require 'mdx_tex/to_markdown/ordered_list'
7
+
8
+ module MdxTex
9
+ # Converts Textile to Markdown.
10
+ #
11
+ # The base list depth is auto-detected per document (smallest asterisks/hashes
12
+ # count across all list lines) so that input with inconsistent base
13
+ # indentation still produces a clean depth-1 Markdown list. Ordered list
14
+ # items are numbered with incrementing per-depth counters that reset on
15
+ # blank lines or any non-ordered-list line.
16
+ class ToMarkdown
17
+ def execute(input)
18
+ return nil if input.nil?
19
+
20
+ lines = input.to_s.split("\n", -1)
21
+ @unordered_base, @ordered_base = detect_bases(lines)
22
+ @counters = {}
23
+
24
+ lines.map { |line| convert_line(line) }.join("\n")
25
+ end
26
+
27
+ private
28
+
29
+ # Single pass over the document:
30
+ # - every line is matched against the list patterns once,
31
+ # and the smallest marker run seen for each kind becomes the depth-1 base.
32
+ # - Falls back to 1 when no list of that kind is present
33
+ # (the value is irrelevant in that case — no line will match for conversion).
34
+ def detect_bases(lines)
35
+ unordered_counts = []
36
+ ordered_counts = []
37
+ lines.each do |line|
38
+ if (m = line.match(UnorderedList::PATTERN))
39
+ unordered_counts << m[1].length
40
+ elsif (m = line.match(OrderedList::PATTERN))
41
+ ordered_counts << m[1].length
42
+ end
43
+ end
44
+ [unordered_counts.min || 1, ordered_counts.min || 1]
45
+ end
46
+
47
+ def convert_line(line)
48
+ # List conversion must run before Bold (and before Header for symmetry):
49
+ # - Textile uses `*` for both unordered list markers and inline bold.
50
+ # A line like `*** *foo*` is a depth-3 list item containing the bold word "foo".
51
+ # - If Bold ran first, the leading `*` characters would be eaten by its
52
+ # regex and the list structure would be lost.
53
+ # Converting lists first rewrites the leading markers to Markdown `-`/`1.`,
54
+ # leaving only the inline `*foo*` for Bold to handle on the next pass.
55
+ line = convert_ordered_and_unordered_list(line)
56
+ line = Header.execute(line)
57
+ Bold.execute(line)
58
+ end
59
+
60
+ def convert_ordered_and_unordered_list(line)
61
+ match = line.match(OrderedList::PATTERN)
62
+ if match
63
+ # Ordered-list line: bump the counter at this depth
64
+ # (which also drops any deeper counters so they restart fresh next time we descend).
65
+ depth = match[1].length - @ordered_base + 1
66
+ number = bump_counter(depth)
67
+ OrderedList.execute(line, base_list_depth: @ordered_base, number: number)
68
+ else
69
+ # Anything else (blank line, unordered item, header, paragraph)
70
+ # ends the current run of ordered items, so all ordered counters reset.
71
+ # The next `# foo` encountered will start over at 1.
72
+ @counters.clear
73
+ UnorderedList.execute(line, base_list_depth: @unordered_base)
74
+ end
75
+ end
76
+
77
+ # Increment the counter at +depth+ and drop any deeper-depth counters.
78
+ # So the next time we descend to those depths they start fresh at 1.
79
+ def bump_counter(depth)
80
+ @counters[depth] = (@counters[depth] || 0) + 1
81
+ @counters.delete_if { |d, _| d > depth }
82
+ @counters[depth]
83
+ end
84
+ end
85
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MdxTex
4
- VERSION = '0.1.13'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/mdx_tex.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'mdx_tex/version'
4
4
  require 'mdx_tex/configuration'
5
5
  require 'mdx_tex/to_textile'
6
+ require 'mdx_tex/to_markdown'
6
7
 
7
8
  module MdxTex
8
9
  class << self
@@ -21,6 +22,10 @@ module MdxTex
21
22
  MdxTex::ToTextile.new(**merged).execute(markdown)
22
23
  end
23
24
 
25
+ def to_markdown(textile:)
26
+ MdxTex::ToMarkdown.new.execute(textile)
27
+ end
28
+
24
29
  def load_string_extension!
25
30
  require 'mdx_tex/core_ext/string'
26
31
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mdx-tex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.13
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gloria Budiman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-27 00:00:00.000000000 Z
11
+ date: 2026-05-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Converts Markdown syntax to Textile syntax, with configurable header
14
14
  levels and list depth.
@@ -24,6 +24,11 @@ files:
24
24
  - lib/mdx_tex/configuration.rb
25
25
  - lib/mdx_tex/core_ext/string.rb
26
26
  - lib/mdx_tex/railtie.rb
27
+ - lib/mdx_tex/to_markdown.rb
28
+ - lib/mdx_tex/to_markdown/bold.rb
29
+ - lib/mdx_tex/to_markdown/header.rb
30
+ - lib/mdx_tex/to_markdown/ordered_list.rb
31
+ - lib/mdx_tex/to_markdown/unordered_list.rb
27
32
  - lib/mdx_tex/to_textile.rb
28
33
  - lib/mdx_tex/to_textile/bold.rb
29
34
  - lib/mdx_tex/to_textile/errors.rb