idml 0.10.3 → 0.10.5

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: 79a78b04b80af7bc0018d4fde7500d164d8bc8b587a425c2fba89c5be19a7bb8
4
- data.tar.gz: 0b835730671e71723e043372530b1fbb6765029c50da2025315408dd2caf4fa7
3
+ metadata.gz: 92cf9efcb8cb1f39e0047561634dd472186e817399f86ec0ffb1052e6de31db9
4
+ data.tar.gz: 1226c8e6cc234f21ed9f39ed79d371fb816bed45452eb28fba5cef62517b9dce
5
5
  SHA512:
6
- metadata.gz: 3f79b3ee1dbe8a3d5cb01346f55935196e10d21ca3cd7d477132aaedafc200df0880179b568b8c071d5dbce62e77b2fe6275fdedb1a6d8fbbeb868f61b16a70f
7
- data.tar.gz: 5d556bd0f53b5817051dd5ae419fab76c4e91534e3ff0f1415ff5e22fc10949743f2e6fb5992d0c2a669ea5e8aa4e3e2c20ebac7ddfe8ae991495267cd5a2671
6
+ metadata.gz: b369f0215c2e82053016ffd629bd14894d3d790c4dd053737a8645a919dc7c8054648a882fd6a1494df335e26d252a89c61e4c85f58f7dd7f630f54ed0686107
7
+ data.tar.gz: 79604135da4d6126775c9ae9ac044034180f89a034b1e56116c278b13a07a1b728839b4114cf0c317c8ff8af5b67150d1d62b111f02981796fea389767bbc758
data/Gemfile CHANGED
@@ -9,10 +9,12 @@ gem "lutaml-model", "~> 0.8.0"
9
9
  gem "pdfrb", "~> 0.7"
10
10
  gem "rake"
11
11
  gem "rspec"
12
- gem "rubocop"
13
- gem "rubocop-performance"
14
- gem "rubocop-rake"
15
- gem "rubocop-rspec"
12
+ # Pinned: new rubocop majors introduce cops that fail CI
13
+ # unexpectedly (1.90 did on 2026-08-25). Bump deliberately.
14
+ gem "rubocop", "1.90.0"
15
+ gem "rubocop-performance", "1.27.0"
16
+ gem "rubocop-rake", "0.7.1"
17
+ gem "rubocop-rspec", "3.10.2"
16
18
  gem "simplecov", require: false
17
19
  gem "thor"
18
20
  gem "yard"
@@ -1,7 +1,6 @@
1
1
  # TODO PDF 117: Endnote support
2
2
 
3
- ## Status: PARTIAL (modeling + reference markers COMPLETE
4
- 2026-08-18/19; endnote TEXT rendering OPEN pending a real fixture)
3
+ ## Status: COMPLETE implemented 2026-08-26
5
4
 
6
5
  ## Problem
7
6
 
@@ -27,7 +26,7 @@ story flagged `IsEndnoteStory="true"`. Documents with endnotes
27
26
  references instead of dropping them. `StoryInner` also parses
28
27
  `StoryPreference` (vertical writing support).
29
28
 
30
- ## Design for rendering (needs a real InDesign fixture to validate)
29
+ ## End-of-story text rendering (2026-08-26)
31
30
 
32
31
  1. ~~Marker~~ DONE 2026-08-19: CSR-level EndnoteRange emits a
33
32
  superscript marker run at the CSR's position (same pattern as
@@ -48,3 +47,22 @@ The marker-position machinery (text-range resolution) cannot be
48
47
  validated without a real fixture; blind implementation risks wrong
49
48
  anchor placement on production files. Schema modeling + round-trip
50
49
  is complete and verified.
50
+
51
+ ## Final implementation notes
52
+
53
+ When a main story carries EndnoteRange markers, the package's
54
+ endnote stories (IsEndnoteStory="true", package order) append
55
+ after the main flow via the chain state — end-of-story placement
56
+ per the design above. Each endnote story's first run is prefixed
57
+ with its ordinal ("1 …", "2 …") matching the body markers'
58
+ sequential numbering. Verified by an end-to-end synthetic-package
59
+ spec (3 text blocks with the marker, 1 without).
60
+
61
+ ## Known limitations
62
+
63
+ - Multi-story linkage is not modeled: ALL endnote stories follow
64
+ the marker-bearing story (correct for single-article documents;
65
+ the EndnoteTextRange chain remains the fixture-gated refinement
66
+ for multi-article layouts).
67
+ - EndnoteOption (Scope / StartAt / Prefix / Suffix) is not yet
68
+ modeled in Preferences; numbering is ordinal.
@@ -1,8 +1,9 @@
1
1
  # TODO PDF 13: CJK text layout
2
2
 
3
- ## Status: PARTIALvertical writing mode implemented 2026-08-19
4
- (horizontal-mode CJK was complete 2026-08-17: glyph measurement,
5
- kinsoku shori, ruby annotations).
3
+ ## Status: COMPLETEfinal mojikumi pair compression landed
4
+ 2026-08-26. Vertical writing (2026-08-19), kinsoku shori, ruby,
5
+ tate-chu-yoko, script spacing, and line-end compression were
6
+ already in.
6
7
 
7
8
  ## Vertical mode (2026-08-19)
8
9
 
@@ -22,10 +23,12 @@ kinsoku shori, ruby annotations).
22
23
 
23
24
  2026-08-20 completions: tate-chu-yoko (TODO 124), Latin rotation
24
25
  and vertical ruby (TODO 122), CJK/Latin automatic script spacing
25
- (TODO 125, a mojikumi subset wired into Shaper). The remaining
26
- stretch goal is full class-based mojikumi (per-pair punctuation
27
- compression tables); line-end punctuation compression landed
28
- 2026-08-23 as TODO 132. See `lib/idml/text_engine/cjk_layout.rb` and
26
+ (TODO 125, a mojikumi subset wired into Shaper). Final piece (2026-08-26): class-based pair compression — a
27
+ full-width closing/middle glyph followed by another full-width
28
+ punctuation compresses to half advance within the line (。」、
29
+ ー)… pairs), completing the three real mojikumi behaviors of
30
+ InDesign's default sets alongside script spacing (TODO 125) and
31
+ line-end compression (TODO 132). See `lib/idml/text_engine/cjk_layout.rb` and
29
32
  `lib/idml/text_engine/vertical_text_layout.rb`.
30
33
 
31
34
  ## Progress 2026-08-17
@@ -27,9 +27,9 @@ this list reflects the current state.
27
27
  the box; side-aware narrowing is not implemented.
28
28
 
29
29
  ### CJK
30
- - **Mojikumi**: CJK/Latin auto script spacing (TODO 125) and
31
- line-end punctuation compression (TODO 132) are applied; full
32
- class-based compression tables are not.
30
+ - **Mojikumi**: script spacing (TODO 125), line-end compression
31
+ (TODO 132), and class-based pair compression (2026-08-26) are
32
+ applied; named mojikumi sets (詳細 etc.) are not modeled.
33
33
  - **Vertical mode**: Latin rotation is per glyph (not run-grouped);
34
34
  ruby placement is beside-column; keep options do not apply.
35
35
 
data/TODO.pdf/README.md CHANGED
@@ -12,7 +12,10 @@ without InDesign Server. The pipeline has three layers:
12
12
  output together. IDML Package → typed model → render plan →
13
13
  pdfrb Document → PDF file.
14
14
 
15
- All entries below are functionally DONE (basic CJK in TODO 13 is
16
- the only PARTIAL vertical writing mode, kinsoku, ruby, and
17
- tate-chu-yoko remain stretch goals). Each file carries a
18
- `## Status:` header with a brief implementation summary.
15
+ All entries are DONE except: TODO 117 (endnote TEXT rendering —
16
+ gated on a real fixture to resolve the EndnoteTextRange reference
17
+ chain) and TODO 13's final stretch goal (full class-based mojikumi
18
+ tables; script spacing and line-end compression are in). Vertical
19
+ writing, kinsoku, ruby, and tate-chu-yoko all landed (TODOs
20
+ 122-125). TODO 61 is the live known-limitations index. Each file
21
+ carries a `## Status:` header with a brief implementation summary.
data/idml.gemspec CHANGED
@@ -12,7 +12,14 @@ Gem::Specification.new do |spec|
12
12
  spec.authors = ["Ribose"]
13
13
  spec.email = ["open.source@ribose.com"]
14
14
 
15
- spec.summary = "Parse, manipulate, and round-trip Adobe IDML files."
15
+ spec.summary = "Parse, validate, compose, and render Adobe IDML files to PDF."
16
+ spec.description = "Pure-Ruby toolkit for Adobe InDesign IDML: " \
17
+ "schema-faithful models with byte-faithful " \
18
+ "round-trip, RelaxNG validation, composition " \
19
+ "operations, and a full " \
20
+ "IDML-to-PDF rendering pipeline (text layout, " \
21
+ "tables, CJK " \
22
+ "vertical writing, text wrap, footnotes, PDF/A)."
16
23
  spec.homepage = "https://github.com/metanorma/idml"
17
24
  spec.license = "BSD-2-Clause"
18
25
  spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
@@ -77,6 +77,7 @@ module Idml
77
77
  style_lookup: context.style_lookup,
78
78
  footnote_option: Footnote.option(context.package)
79
79
  )
80
+ paragraphs.concat(endnote_paragraphs(story, context))
80
81
  tables = tables_in_story(story).map { |table| [table, 0] }
81
82
  return nil if paragraphs.empty? && tables.empty?
82
83
 
@@ -114,6 +115,55 @@ module Idml
114
115
  end
115
116
  private_class_method :render_inline_tables
116
117
 
118
+ # End-of-story endnote text (TODO 117): when the main
119
+ # story carries endnote markers, the package's endnote
120
+ # stories (IsEndnoteStory="true", in designmap StoryList
121
+ # order) append after the main flow, each prefixed with
122
+ # its ordinal so it reads as numbered. Multi-story linkage
123
+ # (which endnotes belong to which main story) is not
124
+ # modeled — all endnote stories follow the marker-bearing
125
+ # story, which matches single-article documents.
126
+ def self.endnote_paragraphs(story, context)
127
+ return [] unless endnotes_marked?(story, context)
128
+
129
+ number = 0
130
+ context.package.stories.filter_map do |endnote_story|
131
+ next unless endnote_story.inner&.is_endnote_story
132
+
133
+ number += 1
134
+ paragraphs = StyleResolver.extract_paragraphs(
135
+ endnote_story,
136
+ condition_filter: context.condition_filter,
137
+ style_lookup: context.style_lookup,
138
+ footnote_option: Footnote.option(context.package),
139
+ )
140
+ prefix_endnote_number(paragraphs, number)
141
+ paragraphs
142
+ end.flatten
143
+ end
144
+ private_class_method :endnote_paragraphs
145
+
146
+ def self.endnotes_marked?(story, context)
147
+ return false unless context.package
148
+ return false unless story_csrs(story).any? do |csr|
149
+ !csr.endnote_range.empty?
150
+ end
151
+
152
+ context.package.stories.any? do |candidate|
153
+ candidate.inner&.is_endnote_story
154
+ end
155
+ end
156
+ private_class_method :endnotes_marked?
157
+
158
+ def self.prefix_endnote_number(paragraphs, number)
159
+ return if paragraphs.empty?
160
+ return if paragraphs.first.runs.empty?
161
+
162
+ first_run = paragraphs.first.runs.first
163
+ first_run.text = "#{number} #{first_run.text}"
164
+ end
165
+ private_class_method :prefix_endnote_number
166
+
117
167
  def self.tables_in_story(story)
118
168
  story_csrs(story).flat_map(&:table).compact
119
169
  end
data/lib/idml/render.rb CHANGED
@@ -50,14 +50,13 @@ module Idml
50
50
 
51
51
  # Render an IDML package to a PDF file. All options except
52
52
  # `package:` and `to:` are keyword-only and optional.
53
- # rubocop:disable Metrics/ParameterLists
53
+ # rubocop:disable-next Metrics/ParameterLists
54
54
  def self.render(package:, to:, font_search_paths: nil, compliance: nil,
55
55
  tagged: false, subset_fonts: true, compress: false)
56
56
  Pipeline.new(package, to, font_search_paths,
57
57
  compliance: compliance, tagged: tagged,
58
58
  subset_fonts: subset_fonts, compress: compress).call
59
59
  end
60
- # rubocop:enable Metrics/ParameterLists
61
60
  end
62
61
  end
63
62
 
@@ -74,6 +74,68 @@ module Idml
74
74
  TRAILING_COMPRESSION_CODEPOINTS.include?(codepoint)
75
75
  end
76
76
 
77
+ # Class-based mojikumi pair compression: a full-width
78
+ # closing/middle glyph followed by another full-width
79
+ # punctuation glyph compresses to half advance within the
80
+ # line (。」、ー) … pairs), matching InDesign's default
81
+ # mojikumi behavior for consecutive 約物.
82
+ OPENING_CODEPOINTS = [
83
+ 0x300C, # 「
84
+ 0x300E, # 『
85
+ 0x3008, # 〈
86
+ 0x300A, # 《
87
+ 0x3010, # 【
88
+ 0x3014, # 〔
89
+ 0xFF08, # (fullwidth left paren
90
+ 0xFF3B, # [
91
+ 0xFF5B, # {
92
+ ].freeze
93
+
94
+ MIDDLE_CODEPOINTS = [
95
+ 0x30FB, # ・ fullwidth middle dot
96
+ 0x30FC, # ー prolonged sound mark
97
+ 0x2025, # ‥ two-dot leader
98
+ 0x2026, # … ellipsis
99
+ ].freeze
100
+
101
+ def apply_pair_compression(lines)
102
+ lines.each do |line|
103
+ glyphs = line.glyphs
104
+ (0...(glyphs.length - 1)).each do |index|
105
+ leading = glyphs[index]
106
+ following = glyphs[index + 1]
107
+ next unless pair_compressible?(leading, following)
108
+
109
+ leading.width /= 2.0
110
+ end
111
+ line.width = glyphs.sum(&:width)
112
+ end
113
+ lines
114
+ end
115
+
116
+ # Closing/middle glyph followed by full-width punctuation
117
+ # compresses the leading glyph to half advance.
118
+ def pair_compressible?(leading, following)
119
+ return false unless fullwidth_punct?(following.codepoint)
120
+
121
+ closing?(leading.codepoint) ||
122
+ (middle?(leading.codepoint) &&
123
+ !OPENING_CODEPOINTS.include?(following.codepoint))
124
+ end
125
+
126
+ def closing?(codepoint)
127
+ TRAILING_COMPRESSION_CODEPOINTS.include?(codepoint)
128
+ end
129
+
130
+ def middle?(codepoint)
131
+ MIDDLE_CODEPOINTS.include?(codepoint)
132
+ end
133
+
134
+ def fullwidth_punct?(codepoint)
135
+ closing?(codepoint) || middle?(codepoint) ||
136
+ OPENING_CODEPOINTS.include?(codepoint)
137
+ end
138
+
77
139
  CJK_RANGES = [
78
140
  0x3000..0x303F, # CJK Symbols and Punctuation
79
141
  0x3040..0x309F, # Hiragana
@@ -15,6 +15,7 @@ module Idml
15
15
  return lines unless cjk_run?(glyphs)
16
16
 
17
17
  lines = CjkLayout.apply_kinsoku(lines)
18
+ CjkLayout.apply_pair_compression(lines)
18
19
  CjkLayout.apply_line_end_compression(lines)
19
20
  end
20
21
 
data/lib/idml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.10.3"
4
+ VERSION = "0.10.5"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.3
4
+ version: 0.10.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-24 00:00:00.000000000 Z
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -94,7 +94,10 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
- description:
97
+ description: 'Pure-Ruby toolkit for Adobe InDesign IDML: schema-faithful models with
98
+ byte-faithful round-trip, RelaxNG validation, composition operations, and a full
99
+ IDML-to-PDF rendering pipeline (text layout, tables, CJK vertical writing, text
100
+ wrap, footnotes, PDF/A).'
98
101
  email:
99
102
  - open.source@ribose.com
100
103
  executables:
@@ -1149,5 +1152,5 @@ requirements: []
1149
1152
  rubygems_version: 3.5.22
1150
1153
  signing_key:
1151
1154
  specification_version: 4
1152
- summary: Parse, manipulate, and round-trip Adobe IDML files.
1155
+ summary: Parse, validate, compose, and render Adobe IDML files to PDF.
1153
1156
  test_files: []