Skip to content

Commit b7ed2f6

Browse files
committed
docs: rewrite man_xref as treeprocessor instead of HTML postprocessor
Rewrite manpage cross-references to link: macros on the parsed AST before conversion, so verbatim blocks, monospace, passthroughs and existing links are excluded by node context instead of HTML regex splitting. The backend is checked via document.backend.
1 parent ef7ad6c commit b7ed2f6

1 file changed

Lines changed: 93 additions & 56 deletions

File tree

docs/src/extensions/man_xref.rb

Lines changed: 93 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
# docs/src/extensions/man_xref.rb
22
#
3-
# Asciidoctor postprocessor that turns manpage cross-references in the
4-
# conventional name(section) form (e.g. "halcmd(1)") into clickable links to
5-
# the sibling HTML page ../man<section>/<name>.<section>.html.
3+
# Asciidoctor treeprocessor that turns manpage cross-references in the
4+
# conventional name(section) form (e.g. "halcmd(1)") into link: macros
5+
# pointing at the sibling HTML page man<section>/<name>.<section>.html.
6+
# Running on the parsed AST (before conversion) rewrites text in AsciiDoc
7+
# source space, so code blocks, monospace spans, passthroughs and existing
8+
# links are excluded by node context instead of by guessing at tag
9+
# boundaries in finished HTML.
610
#
711
# * Index-gated: a token is linked only when a page <name>.<section> exists
812
# in the troff tree, so false positives never resolve and stay plain text
913
# ("feed(2)"/"arc(3)" enum values in motion(9); external open(2)/udev(8);
1014
# typo'd or renamed API names).
1115
# * Never links a page to itself.
12-
# * Skips text inside <a> <code> <pre> <script> <style> <head> <title> <h1>,
13-
# so code samples, tag attributes and existing links are left untouched.
16+
# * Skips verbatim blocks; inside inline text skips monospace spans,
17+
# pass:[] passthroughs, link:/xref:/image: macros, URLs and <<xrefs>>.
1418
# * Case-insensitive match ("AXIS(1)" => axis.1); visible text kept verbatim.
1519
#
1620
# Used for both the HTML manpages (sibling links under man/) and the narrative
@@ -27,23 +31,28 @@
2731
require 'asciidoctor/extensions'
2832

2933
module LinuxCNCDocs
30-
class ManXref < Asciidoctor::Extensions::Postprocessor
31-
# Manpage sections LinuxCNC ships and cross-references between.
34+
class ManXref < Asciidoctor::Extensions::Treeprocessor
3235
SECTIONS = %w[1 3 9].freeze
3336

34-
# Elements whose text content must never be rewritten.
35-
PROTECTED = %w[a code pre script style head title h1].freeze
36-
3737
# One name(section) token. Name starts with a letter/underscore so
3838
# version-like "3.5(1)" never matches; the section is a single digit.
3939
TOKEN = /\b([A-Za-z_][A-Za-z0-9_.\-]*)\((\d)\)/.freeze
4040

41-
# Cache the per-root index across the many pages of one asciidoctor run.
41+
# Inline source spans that must never be rewritten. Note compat-mode
42+
# legacy 'quotes' are emphasis (<em>), not code, so they stay linkable.
43+
PROTECTED_SPAN = %r{(
44+
`[^`\n]*`
45+
| pass:\[[^\]\n]*\]
46+
| (?:link|xref|image):[^\s\[]*\[[^\]\n]*\]
47+
| <<[^>\n]*>>
48+
| https?://[^\s\[]+(?:\[[^\]\n]*\])?
49+
)}x.freeze
50+
4251
@index_cache = {}
4352
class << self; attr_reader :index_cache; end
4453

45-
# Build "name-downcased\tsection" => "man<N>/<name>.html" from the
46-
# troff man tree. Filenames are the authoritative existence list.
54+
# Build "name-downcased\tsection" => "man<N>/<name>.<N>.html" from
55+
# the troff man tree. Filenames are the authoritative existence list.
4756
def self.build_index(root)
4857
key = File.expand_path(root)
4958
cached = index_cache[key]
@@ -58,68 +67,96 @@ def self.build_index(root)
5867
next unless fn.end_with?(suffix)
5968
name = fn[0...-suffix.length]
6069
next if name.empty?
61-
# Rendered HTML keeps the section in the filename: the troff page
62-
# man<N>/<name>.<N> becomes man<N>/<name>.<N>.html.
6370
idx["#{name.downcase}\t#{sec}"] = "man#{sec}/#{fn}.html"
6471
end
6572
end
6673
index_cache[key] = idx
6774
end
6875

69-
# Split HTML into an alternating stream of text runs and tags, tracking a
70-
# stack of PROTECTED elements; yield only text runs that are safe to edit.
71-
def self.each_editable_text(html)
72-
depth = 0
73-
pos = 0
74-
out = +''
75-
html.scan(/([^<]+)|(<[^>]*>)/) do
76-
text, tag = Regexp.last_match(1), Regexp.last_match(2)
77-
if text
78-
out << (depth.zero? ? yield(text) : text)
79-
else
80-
out << tag
81-
m = /\A<\s*(\/?)\s*([A-Za-z][A-Za-z0-9]*)/.match(tag)
82-
if m && PROTECTED.include?(m[2].downcase) && !tag.end_with?('/>')
83-
depth += (m[1] == '/' ? -1 : 1)
84-
depth = 0 if depth < 0
85-
end
86-
end
87-
pos += 1
88-
end
89-
out
90-
end
76+
def process(document)
77+
return unless document.backend.start_with?('html')
9178

92-
def process(document, output)
9379
root = document.attr('manxref-root')
94-
return output if root.nil? || root.empty?
80+
return if root.nil? || root.empty?
9581

9682
idx = self.class.build_index(root)
97-
return output if idx.empty?
83+
return if idx.empty?
9884

99-
self_name = (document.attr('mantitle') || '').downcase
100-
self_vol = (document.attr('manvolnum') || '').to_s
101-
102-
# Relative path from this page to the man<N>/ dirs. Manpages sit beside
103-
# each other under man/, so the default reaches a sibling section dir;
104-
# narrative pages pass their own depth-adjusted base (../man/, ../../man/).
10585
base = document.attr('manxref-linkbase')
10686
base = '../' if base.nil? || base.empty?
10787

108-
self.class.each_editable_text(output) do |text|
109-
text.gsub(TOKEN) do
110-
whole = Regexp.last_match(0)
111-
name = Regexp.last_match(1)
112-
sec = Regexp.last_match(2)
113-
# Never link a page to itself.
114-
next whole if name.downcase == self_name && sec == self_vol
115-
href = idx["#{name.downcase}\t#{sec}"]
116-
href ? %(<a class="man-xref" href="#{base}#{href}">#{whole}</a>) : whole
88+
ctx = {
89+
idx: idx,
90+
base: base,
91+
self_name: (document.attr('mantitle') || '').downcase,
92+
self_vol: (document.attr('manvolnum') || '').to_s,
93+
}
94+
document.blocks.each { |blk| rewrite_block(blk, ctx) }
95+
nil
96+
end
97+
98+
private
99+
100+
# ListItem, ListTerm and Table::Cell #text getters apply inline
101+
# substitutions; read and write the raw text to avoid double substitution.
102+
def raw_text(node)
103+
node.instance_variable_get(:@text)
104+
end
105+
106+
def set_raw_text(node, text)
107+
node.instance_variable_set(:@text, text)
108+
end
109+
110+
def rewrite_block(blk, ctx)
111+
case blk.context
112+
when :list_item
113+
set_raw_text(blk, rewrite_line(raw_text(blk), ctx)) if blk.text?
114+
when :table
115+
rewrite_table(blk, ctx)
116+
else
117+
blk.lines.map! { |line| rewrite_line(line, ctx) } if blk.content_model == :simple
118+
end
119+
return unless blk.blocks?
120+
blk.blocks.each do |child|
121+
if child.is_a?(Array)
122+
# dlist entry: [terms, description]
123+
terms, desc = child
124+
terms.each { |t| set_raw_text(t, rewrite_line(raw_text(t), ctx)) } if terms
125+
rewrite_block(desc, ctx) if desc
126+
else
127+
rewrite_block(child, ctx)
128+
end
129+
end
130+
end
131+
132+
def rewrite_table(tbl, ctx)
133+
(tbl.rows.head + tbl.rows.body + tbl.rows.foot).each do |row|
134+
row.each do |cell|
135+
next if cell.style == :asciidoc
136+
set_raw_text(cell, rewrite_line(raw_text(cell), ctx))
117137
end
118138
end
119139
end
140+
141+
# Link tokens only in unprotected text (protected spans land on odd
142+
# indices after the split).
143+
def rewrite_line(line, ctx)
144+
line.split(PROTECTED_SPAN).each_with_index.map do |part, i|
145+
i.odd? ? part : part.gsub(TOKEN) { link_token(Regexp.last_match, ctx) }
146+
end.join
147+
end
148+
149+
def link_token(match, ctx)
150+
whole = match[0]
151+
return whole if match[1].downcase == ctx[:self_name] && match[2] == ctx[:self_vol]
152+
href = ctx[:idx]["#{match[1].downcase}\t#{match[2]}"]
153+
# compat-mode swallows link-macro attributes; put the styling role
154+
# on a wrapping span instead of on the anchor.
155+
href ? %([.man-xref]#link:#{ctx[:base]}#{href}[#{whole}]#) : whole
156+
end
120157
end
121158
end
122159

123160
Asciidoctor::Extensions.register do
124-
postprocessor LinuxCNCDocs::ManXref
161+
treeprocessor LinuxCNCDocs::ManXref
125162
end

0 commit comments

Comments
 (0)