Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ gem "plurimath", "~> 0.10"

# Local development: use bleeding-edge local checkouts when present.
# CI environments fall back to the published rubygems versions.
#
# Keep each local-checkout gem as single-line statement(s) whose text includes
# the gem name. metanorma/ci's dependent-repos job strips every Gemfile line
# containing the gem name before running `bundle add`; a multi-line if/else
# would drop its variable definition or leave a dangling `end` and break
# bundler on downstream CI.
repo_root = File.expand_path("../..", __dir__)

lutaml_local = File.join(repo_root, "lutaml/lutaml-model")
if File.exist?(lutaml_local)
gem "lutaml-model", path: lutaml_local
end
gem "lutaml-model", path: lutaml_local if File.exist?(lutaml_local)

moxml_local = File.join(repo_root, "lutaml/moxml")
if File.exist?(moxml_local)
gem "moxml", path: moxml_local
else
gem "moxml", ">= 0.1.15"
end
moxml_spec = File.exist?(moxml_local) ? { path: moxml_local } : ">= 0.1.15"
gem "moxml", moxml_spec

# Standard library gems that will be removed from default in Ruby 4.0
gem "benchmark"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def extract_heading_text(paragraph)
if paragraph.is_a?(Lutaml::Model::Serializable) && paragraph.class.attributes.key?(:text)
paragraph.text
elsif paragraph.is_a?(Lutaml::Model::Serializable) && paragraph.class.attributes.key?(:runs)
paragraph.runs.map(&:text).join
paragraph.runs.map(&:text_string).join
end
end
end
Expand Down
18 changes: 4 additions & 14 deletions lib/uniword/assembly/toc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,11 @@ def instruction_string
# @param paragraph [Wordprocessingml::Paragraph] Paragraph to check
# @return [Integer, nil] Heading level or nil
def heading_level(paragraph)
style_name = paragraph.properties&.style
return nil unless style_name
style = paragraph.style
return nil unless style

# Check for heading styles (Heading 1, Heading 2, etc.)
match = style_name.match(/^Heading\s*(\d+)$/i)
match = style.to_s.match(/^Heading\s*(\d+)$/i)
return match[1].to_i if match

# Check for outline level in properties
Expand All @@ -187,17 +187,7 @@ def heading_level(paragraph)
# @param paragraph [Wordprocessingml::Paragraph] Source paragraph
# @return [String] Extracted text
def extract_text(paragraph)
text_parts = []

paragraph.runs.each do |run|
run_text = run.text
next unless run_text

# Handle Text objects by converting to string
text_parts << run_text.to_s
end

text_parts.join
paragraph.runs.map(&:text_string).join
end

# Create title paragraph.
Expand Down
7 changes: 3 additions & 4 deletions lib/uniword/assembly/variable_substitutor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,9 @@ def substitute_paragraph(paragraph)
# @param run [Run] Run to process
# @return [void]
def substitute_run(run)
return unless run.text

content = run.text.to_s
run.text = substitute(content)
run.substitute_with_block(VARIABLE_PATTERN) do |match|
resolve_variable(match[1])
end
end

# Collect all keys from nested hash.
Expand Down
6 changes: 4 additions & 2 deletions lib/uniword/batch/stages/normalize_styles_stage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ def normalize_paragraph_styles(document)

# Detect style based on properties
detected_style = detect_paragraph_style(paragraph)
target = detected_style && @target_styles[detected_style]
next unless target

paragraph.properties.style = @target_styles[detected_style] if detected_style && @target_styles[detected_style]
paragraph.properties.style = target
end
end

Expand All @@ -102,7 +104,7 @@ def detect_paragraph_style(paragraph)

# Check style name
if paragraph.style
style_name = paragraph.style.downcase
style_name = paragraph.style.to_s.downcase
return :heading1 if style_name.include?("heading 1")
return :heading2 if style_name.include?("heading 2")
return :heading3 if style_name.include?("heading 3")
Expand Down
2 changes: 1 addition & 1 deletion lib/uniword/builder/paragraph_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def <<(element)
end

def style=(name)
ensure_properties.style = Properties::StyleReference.new(value: name)
ensure_properties.style = name
self
end

Expand Down
19 changes: 7 additions & 12 deletions lib/uniword/builder/run_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module RunUtils
# A run is empty only if it has no text content and no structural
# elements (breaks, tabs, drawings, field chars, etc.).
def empty_run?(run)
return false if run.break
return false if Array(run.break).any?
return false if run.tab
return false if run.drawings&.any?
return false if run.pictures&.any?
Expand All @@ -32,12 +32,7 @@ def empty_run?(run)
return false if run.separator_char
return false if run.continuation_separator_char

t = run.text
return true unless t

content = t.content if t.class.attributes.key?(:content)
content = t.value if content.nil? && t.class.attributes.key?(:value)
!content.is_a?(String) || content.empty?
run.text_string.empty?
end

# Whether a run contains only text (no structural elements).
Expand All @@ -53,7 +48,7 @@ def text_only_run?(run)
return false if run.field_char
return false if run.instr_text
return false if run.sym
return false if run.break
return false if Array(run.break).any?
return false if run.tab
return false if run.position_tab
return false if run.no_break_hyphen
Expand All @@ -77,7 +72,7 @@ def properties_match?(a, b)
# Runs can be merged if both are text-only with identical formatting.
def mergeable?(existing, incoming)
return false unless text_only_run?(existing) && text_only_run?(incoming)
return false unless incoming.text&.to_s && !incoming.text.to_s.empty?
return false if incoming.text_string.empty?

properties_match?(existing, incoming)
end
Expand All @@ -86,9 +81,9 @@ def mergeable?(existing, incoming)
#
# Handles xml:space="preserve" for whitespace-sensitive content.
def merge_text(target, source)
if source.text
existing = target.text&.to_s
appended = source.text.to_s
if Array(source.text).any?
existing = target.text_string
appended = source.text_string
combined = existing + appended
new_text = Wordprocessingml::Text.new(content: combined)
if Wordprocessingml::Text.preserve_whitespace?(combined)
Expand Down
2 changes: 1 addition & 1 deletion lib/uniword/diff/document_differ.rb
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ def format_value(props, method)

val = case method
when :alignment then props.alignment
when :style then props.style
when :style then Array(props.style).first
end
unwrap_value(val)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/uniword/docx/reconciler/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def document_fingerprint

texts = []
walk_body_paragraphs(body) do |para|
texts << (para.runs || []).map { |r| r.text.to_s }.join
texts << (para.runs || []).map(&:text_string).join
end
Digest::SHA256.hexdigest(texts.join("|"))
end
Expand Down
2 changes: 1 addition & 1 deletion lib/uniword/docx/reconciler/referential_integrity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ def ensure_rid_uniqueness
# -- Traversal helpers --

def reconcile_paragraph_style(para, defined_ids, style_names)
style_ref = para.properties&.style
style_ref = Array(para.properties&.style).first
return 0 unless style_ref&.value
return 0 if defined_ids.include?(style_ref.value)
return 0 if style_names.include?(style_ref.value)
Expand Down
2 changes: 1 addition & 1 deletion lib/uniword/mhtml/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Document < Lutaml::Model::Serializable
attribute :html_part, HtmlPart

# All MIME parts (including the HTML part)
attribute :parts, :array, default: -> { [] }
attribute :parts, MimePart, collection: true, default: -> { [] }

# Parsed metadata from HTML head XML blocks
attribute :document_properties, Metadata::DocumentProperties
Expand Down
13 changes: 13 additions & 0 deletions lib/uniword/properties/style_reference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ class StyleReference < Lutaml::Model::Serializable
map_attribute "val", to: :value
end

# Cast raw style-id strings into references (mirrors Text.cast), so
# `properties.style = "Heading1"` stores a proper StyleReference
def self.cast(value)
case value
when StyleReference, nil
value
when String
new(value: value)
else
super
end
end

# Compare with another StyleReference or a string
def ==(other)
if other.is_a?(StyleReference)
Expand Down
2 changes: 1 addition & 1 deletion lib/uniword/quality/rules/heading_hierarchy_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def check(document)
# @param paragraph [Paragraph] The paragraph to check
# @return [Integer, nil] Heading level (1-9) or nil if not a heading
def extract_heading_level(paragraph)
style_name = paragraph.properties&.style
style_name = paragraph.style
return nil unless style_name

match = style_name.to_s.match(HEADING_PATTERN)
Expand Down
2 changes: 1 addition & 1 deletion lib/uniword/quality/rules/style_consistency_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def check(document)
# @param paragraph [Paragraph] The paragraph to check
# @return [Boolean] true if has style
def has_style?(paragraph)
style = paragraph.properties&.style
style = paragraph.style
style&.value && !style.value.empty?
end

Expand Down
4 changes: 2 additions & 2 deletions lib/uniword/template/helpers/loop_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ def fill_variables(element)
# recursively process all text nodes
return unless element.is_a?(Uniword::Wordprocessingml::Run)

text = element.text
text = element.text_string
# Simple variable replacement ({{var}})
text.scan(/\{\{([^@].+?)\}\}/).each do |match|
var_name = match[0]
value = resolver.resolve(var_name)
element.text = text.gsub("{{#{var_name}}}", value.to_s)
element.substitute("{{#{var_name}}}", value.to_s)
end
end
end
Expand Down
17 changes: 3 additions & 14 deletions lib/uniword/toc/toc_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,7 @@ def heading_level(paragraph)
# @param paragraph [Wordprocessingml::Paragraph] Paragraph to inspect
# @return [String, nil] Style name or nil
def resolve_style_name(paragraph)
style_ref = paragraph.properties&.style
return nil unless style_ref

if style_ref.is_a?(Uniword::Properties::StyleReference)
style_ref.value
else
style_ref.to_s
end
paragraph.style&.value
end

# Extract text content from a paragraph.
Expand Down Expand Up @@ -197,9 +190,7 @@ def build_toc_sdt(toc_entries, max_level:)
def build_title_paragraph
para = Wordprocessingml::Paragraph.new
para.properties = Wordprocessingml::ParagraphProperties.new
para.properties.style = Uniword::Properties::StyleReference.new(
value: "TOCHeading",
)
para.properties.style = "TOCHeading"

run = Wordprocessingml::Run.new
run.text = "Table of Contents"
Expand Down Expand Up @@ -251,9 +242,7 @@ def build_toc_field_paragraph(max_level: MAX_LEVEL)
def build_entry_paragraph(entry)
para = Wordprocessingml::Paragraph.new
para.properties = Wordprocessingml::ParagraphProperties.new
para.properties.style = Uniword::Properties::StyleReference.new(
value: "TOC#{entry.level}",
)
para.properties.style = "TOC#{entry.level}"

run = Wordprocessingml::Run.new
run.text = entry.text
Expand Down
24 changes: 14 additions & 10 deletions lib/uniword/transformation/mhtml_element_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ def heading_tag_for_style(style)
# Convert OOXML Run to HTML
def run_to_html(run)
# Handle breaks and drawings first (these never get style wrapping)
return break_to_html(run.break) if run.break
# (parsed runs hold [] for absent collections, so check emptiness)
return break_to_html(run.break) if Array(run.break).any?
if run.drawings && !run.drawings.empty?
return drawing_to_html(run.drawings.first)
end
Expand Down Expand Up @@ -105,7 +106,8 @@ def run_to_html(run)
# Skip wrapping for internal OOXML styles that shouldn't produce
# visible HTML class wrappers
is_internal_style = %w[stem hyperlink].include?(style_id)
is_stem_spacer = (style_id == "stem") && run.text.to_s.strip.empty?
is_stem_spacer = (style_id == "stem") &&
run.text_string.strip.empty?
unless is_internal_style || is_stem_spacer
style_val = run_style_to_class(props.style.value)
content = %(<span class="#{style_val}">#{content}</span>)
Expand All @@ -125,7 +127,7 @@ def render_run_content(run)
elsif run.tab
tab_to_html(run)
else
text = run.text.to_s
text = run.text_string
return "" if text.empty?

escape_html(text)
Expand All @@ -142,13 +144,15 @@ def endnote_reference_to_html(_run)
%(<span class="MsoEndnoteReference"><span style="mso-special-character:endnote"></span></span>)
end

# Convert OOXML Break to HTML
def break_to_html(brk)
if brk.type == "page"
%(<br clear="all" style="mso-special-character:line-break;page-break-before:always" />)
else
%(<br />)
end
# Convert OOXML Breaks to HTML, one <br> per Break element
def break_to_html(breaks)
Array(breaks).map do |brk|
if brk.type == "page"
%(<br clear="all" style="mso-special-character:line-break;page-break-before:always" />)
else
%(<br />)
end
end.join
end

# Convert OOXML FieldChar to HTML span
Expand Down
2 changes: 1 addition & 1 deletion lib/uniword/transformation/mhtml_metadata_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def calculate_document_stats
para_chars = 0

element.runs.each do |run|
text = run.text.to_s
text = run.text_string
characters += text.length
para_chars += text.length
words += text.split.length
Expand Down
8 changes: 3 additions & 5 deletions lib/uniword/transformation/ooxml_to_html_converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def self.paragraph_to_html(paragraph)
# @param run [Uniword::Wordprocessingml::Run]
# @return [String] HTML text content with inline formatting
def self.run_to_html(run)
text = escape_html(run.text || "")
text = escape_html(run.text_string)
return text if text.empty?

props = run.properties
Expand Down Expand Up @@ -135,12 +135,10 @@ def self.wrap_html(body_html, document)
# @param paragraph [Uniword::Wordprocessingml::Paragraph]
# @return [String] HTML class/style attribute or empty string
def self.paragraph_style(paragraph)
return "" unless paragraph.properties

style = paragraph.properties.style
style = paragraph.style
return "" unless style

" class=\"#{escape_html(style)}\""
" class=\"#{escape_html(style.to_s)}\""
end

# Convert OOXML font size (half-points) to HTML font size
Expand Down
5 changes: 2 additions & 3 deletions lib/uniword/validation/checkers/internal_link_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,9 @@ def extract_heading_bookmarks(document)
return [] unless document.is_a?(Uniword::Wordprocessingml::DocumentRoot)

document.paragraphs.select do |p|
p.is_a?(Lutaml::Model::Serializable) && p.class.attributes.key?(:style) && p.style&.match?(/^Heading/)
p.style&.to_s&.match?(/^Heading/)
end.filter_map do |p|
# Generate bookmark from heading text
p.is_a?(Lutaml::Model::Serializable) && p.class.attributes.key?(:text) ? heading_to_bookmark(p.text) : nil
heading_to_bookmark(p.text)
end
end

Expand Down
Loading
Loading