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
2731require 'asciidoctor/extensions'
2832
2933module 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
121158end
122159
123160Asciidoctor ::Extensions . register do
124- postprocessor LinuxCNCDocs ::ManXref
161+ treeprocessor LinuxCNCDocs ::ManXref
125162end
0 commit comments