opmac2html 0.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b9625bef7744ab49e28dbc304b224a87f913baef
4
+ data.tar.gz: 16f7373d2b2cdcfffb60fa7f6f445a22df8ca7a0
5
+ SHA512:
6
+ metadata.gz: b25cba818ed4691231c8f8886d208673801f820e18f0b23ab50318a560ab3b26035f8a3145c5cbb92cb17f638aa042df1c3eb2878051943efef6aa3eef8827d4
7
+ data.tar.gz: 884e077bd888e403d3480971512ff7dd566f9dd121bf5fcb78e1cb8129e37d303cdcae99fb7061a9311a5f3062c84f3f515e3fde91508c4646a94814b20755e2
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in opmac2html.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Martin Kinčl
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # opmac2html
2
+
3
+ opmac2html is a document markup converter from OPmac plainTeX macro set markup to HTML5
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'opmac2html'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install opmac2html
20
+
21
+ ## Usage
22
+
23
+ $ opmac2html -i input.tex -o output.html
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/opmac2html ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'opmac2html/cli'
4
+
5
+ Opmac2html::CLI.new
@@ -0,0 +1,28 @@
1
+ require 'slop'
2
+ require 'opmac2html'
3
+ require 'opmac2html/converter'
4
+
5
+ module Opmac2html
6
+ # Command line option parser and runner
7
+ class CLI
8
+ def initialize
9
+ opts = Slop.parse(help: true) do |o|
10
+ o.banner = 'Usage: opmac2html -i <input.tex> -o <output.html>'
11
+ o.string '-i', '--input', 'Input OPmac file'
12
+ o.string '-o', '--output', 'Output HTML file'
13
+ o.on '-h', '--help', 'Shows this message'
14
+ o.on '-v', '--version', 'Shows application version'
15
+ end
16
+ run opts
17
+ end
18
+
19
+ def run(opts)
20
+ puts "opmac2html, version: #{Opmac2html.version}" if opts[:version]
21
+ if opts[:input] && opts[:output]
22
+ Converter.new(opts[:input], opts[:output]).convert
23
+ else
24
+ puts opts
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,58 @@
1
+ require 'opmac2html/html_builder'
2
+ require 'opmac2html/list_builder'
3
+ require 'opmac2html/par_builder'
4
+ require 'opmac2html/table_builder'
5
+ require 'opmac2html/preprocessor'
6
+ require 'opmac2html/text_cutter'
7
+ require 'opmac2html/paragraph_parser'
8
+ require 'opmac2html/macro_parser'
9
+
10
+ module Opmac2html
11
+ # Converter from OPmac to html markup
12
+ class Converter
13
+ include TextCutter
14
+ include ParagraphParser
15
+ include MacroParser
16
+
17
+ def initialize(input_file, output_file)
18
+ @input = read_input input_file
19
+ @preproc = Preprocessor.new
20
+ @input = @preproc.run @input
21
+ @builder = HtmlBuilder.new
22
+ @output_file = output_file
23
+ @ttchar = '"'
24
+ end
25
+
26
+ def read_input(filename)
27
+ File.open(filename, 'r') { |input| input.readlines.join }
28
+ end
29
+
30
+ def write_output(output)
31
+ File.open(@output_file, 'w') { |file| file << output }
32
+ end
33
+
34
+ def convert
35
+ until @input.empty?
36
+ if @input.start_with? '%', "\n"
37
+ cut_at "\n"
38
+ else
39
+ parse
40
+ end
41
+ @input.lstrip!
42
+ end
43
+ write_output @builder.to_s
44
+ end
45
+
46
+ def parse
47
+ if @input.start_with? '\\'
48
+ parse_macro
49
+ else
50
+ parse_par
51
+ end
52
+ end
53
+
54
+ def err(text)
55
+ puts "Unsupported control sequence: #{text}"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,102 @@
1
+ module Opmac2html
2
+ # Builder for the resulting document
3
+ class HtmlBuilder
4
+ attr_reader :anchors
5
+
6
+ MATH_JAX = '<meta charset="UTF-8">
7
+ <script type="text/x-mathjax-config">
8
+ MathJax.Hub.Config({tex2jax: {inlineMath: [[\'$\',\'$\']]}});
9
+ </script>
10
+ <script type="text/javascript"
11
+ src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?' \
12
+ 'config=TeX-AMS-MML_HTMLorMML">
13
+ </script>'
14
+
15
+ TAIL = "</body>\n</html>\n"
16
+
17
+ def initialize
18
+ @document = []
19
+ @fnotes = ListBuilder.new 'n'
20
+ @fnote_count = 0
21
+ @anchors = []
22
+ end
23
+
24
+ def head(title)
25
+ "<!DOCTYPE html>\n<head>\n<title>#{title}</title>\n" \
26
+ "#{MATH_JAX}\n</head>\n<body>\n"
27
+ end
28
+
29
+ def elem(name, text)
30
+ "<#{name}>#{text}</#{name.partition(' ')[0]}>\n\n"
31
+ end
32
+
33
+ def header(number, title)
34
+ elem "h#{number}", title
35
+ end
36
+
37
+ def add_title(title)
38
+ @title ||= title[1]
39
+ @document << [title[0], title[1]]
40
+ end
41
+
42
+ def add_par(text)
43
+ @document << ['p', text]
44
+ end
45
+
46
+ def add_verbatim(text)
47
+ @document << ['pre', text]
48
+ end
49
+
50
+ def add_table(text)
51
+ @document << ['table', text]
52
+ end
53
+
54
+ def add_list(text)
55
+ @document << [nil, text]
56
+ end
57
+
58
+ def add_fnote(text)
59
+ @fnote_count += 1
60
+ @fnotes.add_item(text, @fnote_count.to_s)
61
+ @fnote_count
62
+ end
63
+
64
+ def add_img(filename)
65
+ elem = "<img src=\"#{filename}\" " \
66
+ "alt=\"#{filename[0...filename.rindex('.')]}\">\n"
67
+ @document << [nil, elem]
68
+ end
69
+
70
+ def add_figure(filename, caption)
71
+ img = "<img src=\"#{filename}\" " \
72
+ "alt=\"#{caption}\">"
73
+ cap = "<figcaption>#{caption}</figcaption>"
74
+ @document << ['figure', img + "\n" + cap]
75
+ end
76
+
77
+ def add_anchor(id)
78
+ @anchors << id
79
+ @document << ["span id=\"#{id}\"", '']
80
+ end
81
+
82
+ def header?(element)
83
+ element[0].is_a? Numeric
84
+ end
85
+
86
+ def doc_to_s
87
+ @document.map do |e|
88
+ if header?(e)
89
+ header(*e)
90
+ elsif !e[0]
91
+ e[1]
92
+ else
93
+ elem(*e)
94
+ end
95
+ end.join + "<hr>\n" + @fnotes.to_s
96
+ end
97
+
98
+ def to_s
99
+ head(@title) + doc_to_s + TAIL
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,54 @@
1
+ module Opmac2html
2
+ # Builder for lists (items)
3
+ class ListBuilder
4
+ def initialize(style)
5
+ @list = []
6
+ @list_stack = []
7
+ begitems style
8
+ end
9
+
10
+ def get_type(style)
11
+ case style
12
+ when 'n', 'N'
13
+ 'ol type="1"'
14
+ when 'i', 'I', 'a', 'A'
15
+ "ol type=\"#{style}\""
16
+ else
17
+ 'ul'
18
+ end
19
+ end
20
+
21
+ def start_tag(text)
22
+ @list << "<#{text}>\n"
23
+ @list_stack << text.partition(' ')[0]
24
+ end
25
+
26
+ def end_tag
27
+ @list << "</#{@list_stack.pop}>\n"
28
+ end
29
+
30
+ def begitems(style)
31
+ start_tag get_type style
32
+ @first = true
33
+ end
34
+
35
+ def enditems
36
+ 2.times { end_tag }
37
+ end
38
+
39
+ def add_item(text, id = nil)
40
+ if @first
41
+ @first = false
42
+ else
43
+ end_tag
44
+ end
45
+ start_tag(id ? "li id=\"#{id}\"" : 'li')
46
+ @list << text
47
+ end
48
+
49
+ def to_s
50
+ enditems until @list_stack.empty?
51
+ @list.join
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,176 @@
1
+ module Opmac2html
2
+ # Mixin providing parsing of macro calls
3
+ module MacroParser
4
+ TITLES = %w(\\tit \\chap \\sec \\secc)
5
+ IN_PAR_MACROS = %w(\\TeX \\LaTeX \\csplain)
6
+
7
+ def parse_macro
8
+ title_index = TITLES.index { |t| @input.start_with? t }
9
+ if title_index
10
+ parse_title title_index
11
+ elsif @input.start_with? '\\begtt'
12
+ parse_verbatim
13
+ elsif @input.start_with? '\\verbinput'
14
+ verbinput
15
+ elsif @input.start_with? '\\begitems'
16
+ parse_list
17
+ elsif @input.start_with? '\\activettchar'
18
+ parse_ttchar
19
+ elsif IN_PAR_MACROS.any? { |m| @input.start_with? m }
20
+ parse_par
21
+ else
22
+ parse_other
23
+ end
24
+ end
25
+
26
+ def parse_title(index)
27
+ @min_index ||= index
28
+ title_text = @preproc.process_text(cut_at("\n\n").partition(' ')[2])
29
+ @builder.add_title([title_level(index), title_text])
30
+ end
31
+
32
+ def title_level(index)
33
+ index + 1 - @min_index
34
+ end
35
+
36
+ def parse_verbatim
37
+ cut_at "\n"
38
+ @builder.add_verbatim cut_at '\\endtt'
39
+ end
40
+
41
+ def verbinput
42
+ beg_line, end_line = *verbinput_range
43
+ file = File.open(cut_at("\n").strip, 'r') { |input| input.readlines }
44
+ @builder.add_verbatim(file[beg_line - 1..end_line - 1].join)
45
+ end
46
+
47
+ def verbinput_range
48
+ cut_at '('
49
+ beg_line = cut_at('-').to_i
50
+ end_line = cut_at(')').to_i
51
+ end_line = -1 if end_line == 0
52
+ [beg_line, end_line]
53
+ end
54
+
55
+ def parse_list
56
+ list = parse_list_items
57
+ builder = ListBuilder.new(list[0].partition('\style ')[2][0])
58
+ list[1..-1].each do |line|
59
+ process_list_item line, builder
60
+ end
61
+ @builder.add_list builder.to_s
62
+ end
63
+
64
+ def parse_list_items
65
+ list, @input = *(cut_at_matching(@input, '\\begitems', '\\enditems'))
66
+ list.split("\n").reduce([]) do |a, e|
67
+ if /\*|\\begitems|\\enditems/.match(e) || a.empty?
68
+ a << e
69
+ else
70
+ a[-1].concat "\n" + e
71
+ a
72
+ end
73
+ end
74
+ end
75
+
76
+ def process_list_item(line, builder)
77
+ if line.include? '\\begitems'
78
+ builder.begitems line.partition('\style ')[2][0]
79
+ elsif line.include? '\\enditems'
80
+ builder.enditems
81
+ else
82
+ builder.add_item parse_par_macros(line.partition(/\*\s/)[2])
83
+ end
84
+ end
85
+
86
+ def parse_ttchar
87
+ cut_at 'r'
88
+ @preproc.ttchar = @ttchar = @input[0]
89
+ @input = @input[1..-1]
90
+ end
91
+
92
+ def parse_other
93
+ part_line = @input.partition("\n")
94
+ if %w(\\table \\caption/t).any? { |s| part_line[0].include? s }
95
+ parse_table
96
+ elsif part_line[0].include?('\\inspic')
97
+ parse_image
98
+ elsif part_line[0].include?('\\def')
99
+ part = cut_at_match_with_start(@input, '{', '}')
100
+ err part[0] + part[1]
101
+ @input = part[2]
102
+ elsif part_line[0].start_with?('\\noindent')
103
+ err cut_at(/\s/)
104
+ elsif part_line[0].start_with? '\\label'
105
+ parse_label
106
+ elsif part_line[0].start_with? '\\centerline'
107
+ text = cut_at_matching(part_line[0], '{', '}')[0]
108
+ @builder.add_par parse_par_macros text
109
+ @input = part_line[2]
110
+ else
111
+ err part_line[0]
112
+ @input = part_line[2]
113
+ end
114
+ end
115
+
116
+ def parse_table
117
+ tb = TableBuilder.new
118
+ parse_table_caption(@input.partition("\n")[0], tb)
119
+
120
+ build_table tb
121
+
122
+ parse_table_caption(cut_at("\n\n"), tb)
123
+
124
+ @builder.add_table tb.to_s
125
+ end
126
+
127
+ def parse_table_caption(line, tb)
128
+ return unless line.include? '\\caption/t'
129
+ caption = line.partition('\\caption/t ')[2].partition("\n")[0]
130
+ tb.add_caption(parse_par_macros(caption))
131
+ end
132
+
133
+ def parse_table_cells
134
+ text = @input.partition(/\\table\s*\{[^\{]*/)[2]
135
+ part = cut_at_matching(text, '{', '}')
136
+ @input = part[1]
137
+
138
+ part[0].split(/\\cr.*/).map { |r| r.split(/\s&amp;\s/).map(&:strip) }
139
+ .reject(&:empty?)
140
+ end
141
+
142
+ def build_table(tb)
143
+ parse_table_cells.each do |row|
144
+ tb.add_row(row.map do |cell|
145
+ if cell.start_with? '\\multispan'
146
+ cellpart = cell.partition(/\d+/)
147
+ cellpart[0] + cellpart[1] + parse_par_macros(cellpart[2])
148
+ else
149
+ parse_par_macros cell
150
+ end
151
+ end)
152
+ end
153
+ end
154
+
155
+ def parse_image
156
+ img = cut_at_matching(@input, '\\inspic ', "\n")[0]
157
+ .partition(/ [\n\}]/)[0]
158
+ part = cut_at("\n\n")
159
+ if part.include? '\\label'
160
+ @builder.add_anchor(cut_at_matching(part, '\\label[', ']')[0])
161
+ end
162
+ if part.include? '\\caption/f'
163
+ @builder.add_figure img, cut_at_matching(
164
+ part, '\\caption/f ', "\n\n")[0]
165
+ else
166
+ @builder.add_img img
167
+ end
168
+ end
169
+
170
+ def parse_label
171
+ part = cut_at_matching @input, '[', ']'
172
+ @builder.add_anchor part[0]
173
+ @input = part[1]
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,47 @@
1
+ module Opmac2html
2
+ # Paragraph builder
3
+ class ParBuilder
4
+ ELEM = -> (name, text) { "<#{name}>#{text}</#{name}>" }
5
+ ELEM_WITH_ATT = lambda do |name, attname, attval, text|
6
+ "<#{name} #{attname}=\"#{attval}\">#{text}</#{name}>"
7
+ end
8
+ LINK_SUBS = { '\\%' => '%', '\\#' => '#' }
9
+
10
+ def initialize
11
+ @par = []
12
+ end
13
+
14
+ def add_word(word)
15
+ @par << word
16
+ end
17
+
18
+ def add_code(code)
19
+ @par << ELEM.call('code', code)
20
+ end
21
+
22
+ def add_quote(quote)
23
+ @par << "&bdquo;#{quote}&ldquo;"
24
+ end
25
+
26
+ def add_em(text)
27
+ @par << ELEM.call('em', text)
28
+ end
29
+
30
+ def add_strong(text)
31
+ @par << ELEM.call('strong', text)
32
+ end
33
+
34
+ def add_link(address, text = nil)
35
+ address.gsub!(/\\[%#]/) { |c| LINK_SUBS[c] }
36
+ @par << ELEM_WITH_ATT.call('a', 'href', address, text ? text : address)
37
+ end
38
+
39
+ def add_verbatim(text)
40
+ @par << ELEM.call('pre', text)
41
+ end
42
+
43
+ def to_s
44
+ @par.join
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,158 @@
1
+ module Opmac2html
2
+ # Mixin providing parsing of paragraphs and other text elements
3
+ module ParagraphParser
4
+ def parse_par
5
+ slice = cut_at_with_sep(/\n\n|\n\\begtt|\n\\.*skip|\\par[\\ ]/)
6
+ @builder.add_par(parse_par_macros(slice.gsub(/^%.*/, '')))
7
+ end
8
+
9
+ def parse_par_macros(text)
10
+ par_builder = ParBuilder.new
11
+ until text.empty?
12
+ index = text.index(/#{@ttchar}|\\\w|\{|\$/) || text.length
13
+ par_builder.add_word @preproc.process_text text[0...index]
14
+ text = par_special text[index..-1], par_builder
15
+ end
16
+ par_builder.to_s
17
+ end
18
+
19
+ def par_special(text, par_builder)
20
+ case text[0]
21
+ when @ttchar
22
+ parse_code text, par_builder
23
+ when '$'
24
+ parse_math text, par_builder
25
+ else
26
+ par_macro text, par_builder
27
+ end
28
+ end
29
+
30
+ def dump_all(text)
31
+ err text unless text.empty?
32
+ ''
33
+ end
34
+
35
+ def dump_to_space(text)
36
+ part = text.partition(/\s/)
37
+ err part[0]
38
+ part[2]
39
+ end
40
+
41
+ def parse_code(text, par_builder)
42
+ part = text[1..-1].partition(@ttchar)
43
+ par_builder.add_code part[0]
44
+ part[2]
45
+ end
46
+
47
+ def parse_math(text, par_builder)
48
+ separator = text.start_with?('$$') ? '$$' : '$'
49
+ part = text[separator.length..-1].partition(separator)
50
+ par_builder.add_word separator + part[0] + part[1]
51
+ part[2]
52
+ end
53
+
54
+ def par_macro(text, par_builder)
55
+ if %w(\\url \\ulink \\fnote \\ref \\pgref).any? { |p| text.start_with? p }
56
+ parse_clickable text, par_builder
57
+ elsif text.start_with? '\\begtt'
58
+ par_verbatim text, par_builder
59
+ elsif text.start_with? '\\dots'
60
+ par_builder.add_word '&hellip;'
61
+ text.partition('s')[2]
62
+ elsif text.index(/(\\\w+)?\{/) == 0
63
+ part = cut_at_match_with_start(text, '{', '}')
64
+ if %w(\\TeX \\LaTeX \\csplain).any? { |p| text.start_with? p }
65
+ par_builder.add_word(@preproc.process_text(part[0] + part[1]))
66
+ else
67
+ parse_format_block part[0], par_builder
68
+ end
69
+ part[2]
70
+ elsif %w(\\it \\em \\bf \\tt).any? { |p| text.start_with? p }
71
+ parse_format_block text, par_builder
72
+ ''
73
+ elsif text.index(/\\\w*\s/) == 0
74
+ dump_to_space text
75
+ else
76
+ dump_all text
77
+ end
78
+ end
79
+
80
+ def parse_clickable(text, par_builder)
81
+ if %w(\\url \\ulink).any? { |p| text.start_with? p }
82
+ parse_link text, par_builder
83
+ elsif text.start_with? '\\fnote'
84
+ parse_fnote text, par_builder
85
+ elsif %w(\\ref \\pgref).any? { |p| text.start_with? p }
86
+ part = cut_at_matching(text, '[', ']')
87
+ par_builder.add_link("##{part[0]}", part[0])
88
+ part[1]
89
+ end
90
+ end
91
+
92
+ def par_verbatim(text, par_builder)
93
+ part = cut_at_matching(text, '\\begtt', '\\endtt')
94
+ par_builder.add_verbatim(part[0])
95
+ part[1]
96
+ end
97
+
98
+ def parse_link(text, par_builder)
99
+ part = text.partition('}')
100
+ if text.start_with? '\\url'
101
+ parse_url part[0], par_builder
102
+ else
103
+ parse_ulink part[0], par_builder
104
+ end
105
+ part[2]
106
+ end
107
+
108
+ def parse_url(text, par_builder)
109
+ par_builder.add_link(text.partition('{')[2])
110
+ end
111
+
112
+ def parse_ulink(text, par_builder)
113
+ add = text[text.index('[') + 1...text.index(']')]
114
+ txt = text[text.index('{') + 1..-1]
115
+ par_builder.add_link(add, txt)
116
+ end
117
+
118
+ def parse_format_block(text, par_builder)
119
+ t = parse_par_macros(text[4..-1]).lstrip if text[4]
120
+ if text.start_with? '\\uv{'
121
+ par_builder.add_quote t
122
+ else
123
+ parse_format text, par_builder, t
124
+ end
125
+ end
126
+
127
+ def parse_format(text, par_builder, t)
128
+ case text[0..3]
129
+ when '\\it ', '\\em ', '{\\it', '{\\em'
130
+ par_builder.add_em t
131
+ when '\\bf ', '{\\bf'
132
+ par_builder.add_strong t
133
+ when '\\tt ', '{\\tt'
134
+ par_builder.add_code t
135
+ else
136
+ extract_from_braces text, par_builder
137
+ end
138
+ end
139
+
140
+ def extract_from_braces(text, par_builder)
141
+ if text.index(/\{\\\w/) == 0
142
+ par_special dump_to_space(text), par_builder
143
+ elsif text.start_with? '{'
144
+ par_special cut_at_matching(text, '{', '}')[0], par_builder
145
+ else
146
+ dump_all text
147
+ end
148
+ end
149
+
150
+ def parse_fnote(text, par_builder)
151
+ part = cut_at_match_with_start(text, '{', '}')
152
+ fnote = parse_par_macros(part[0][part[0].index('{') + 1..-1])
153
+ num = @builder.add_fnote(fnote)
154
+ par_builder.add_link("##{num}", "<sup>#{num}</sup>")
155
+ part[2]
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,26 @@
1
+ module Opmac2html
2
+ # A preprocessor for HTML and TeX special characters
3
+ class Preprocessor
4
+ attr_accessor :ttchar
5
+ G_SUBST = { '<' => '&lt;', '>' => '&gt;', '&' => '&amp;' }
6
+
7
+ TEXT_SUBST = { '~' => '&nbsp;', '--' => '&ndash;', '---' => '&mdash',
8
+ '\\,' => '&#8239;', '\\-' => '', '\\TeX{}' => 'TeX',
9
+ '\\LaTeX{}' => 'LaTeX', '\\csplain{}' => 'CSplain' }
10
+
11
+ TS_REG = Regexp.new('~|---?|([^\\\\][%].*)|^%.*|\s%.*|\\\\[,-]|' \
12
+ '\\\\(La)?TeX\{\}|\\\\csplain\{\}|^\{|\s\{|^\}|\s\}')
13
+
14
+ def initialize
15
+ @ttchar = '"'
16
+ end
17
+
18
+ def run(text)
19
+ text.gsub(/[<>&]/) { |c| G_SUBST[c] }
20
+ end
21
+
22
+ def process_text(text)
23
+ text.gsub(TS_REG) { |c| TEXT_SUBST[c] }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ module Opmac2html
2
+ # Builder for tables
3
+ class TableBuilder
4
+ SPAN = '\\multispan'
5
+
6
+ def initialize
7
+ @header = true
8
+ @table = ["\n"]
9
+ end
10
+
11
+ def add_row(cells)
12
+ @table << "<tr>\n"
13
+ cells.each do |cell|
14
+ span_index = cell.index(SPAN)
15
+ span = cell[span_index + SPAN.length] if span_index
16
+ part = cell.partition SPAN
17
+ newcell = part[0] + (span_index ? part[2][1..-1] : '')
18
+ @table << cell_to_s([@header, newcell, span])
19
+ end
20
+ @table << "</tr>\n"
21
+ @header = false
22
+ end
23
+
24
+ def add_caption(text)
25
+ @table.insert 1, "<caption>#{text}</caption>\n"
26
+ end
27
+
28
+ def cell_to_s(cell)
29
+ tag = cell[0] ? 'th' : 'td'
30
+ attr = cell[2] ? " colspan=\"#{cell[2]}\"" : ''
31
+ "<#{tag}#{attr}>#{cell[1]}</#{tag}>\n"
32
+ end
33
+
34
+ def to_s
35
+ @table.join
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,54 @@
1
+ module Opmac2html
2
+ # Mixin providing text partitioning
3
+ module TextCutter
4
+ def cut_at(separator)
5
+ part = @input.partition separator
6
+ @input = part[2]
7
+ part[0]
8
+ end
9
+
10
+ def cut_at_with_sep(separator)
11
+ part = @input.partition separator
12
+ @input = part[1] + part[2]
13
+ part[0]
14
+ end
15
+
16
+ def cut_at_match_with_start(text, beg_sep, end_sep)
17
+ return ['', '', ''] if text.empty?
18
+ index = matching_separator_index text, beg_sep, end_sep
19
+ el = end_sep.length
20
+ [text[0...index], text[index, el] || '', text[index + el..-1] || '']
21
+ end
22
+
23
+ def cut_at_matching(text, beg_sep, end_sep)
24
+ return ['', ''] if text.empty?
25
+ index = matching_separator_index text, beg_sep, end_sep
26
+ bi, bl, el = text.index(beg_sep), beg_sep.length, end_sep.length
27
+ [text[bi + bl...index], text[index + el..-1] || '']
28
+ end
29
+
30
+ protected
31
+
32
+ def matching_separator_start(text, beg_sep)
33
+ index = text.index beg_sep
34
+ if index
35
+ index + beg_sep.length
36
+ else
37
+ 0
38
+ end
39
+ end
40
+
41
+ def matching_separator_index(text, beg_sep, end_sep)
42
+ level = 1
43
+ (matching_separator_start(text, beg_sep)...text.length).each do |i|
44
+ if text[i, beg_sep.length] == beg_sep
45
+ level += 1
46
+ elsif text[i, end_sep.length] == end_sep
47
+ level -= 1
48
+ end
49
+ return i if level == 0
50
+ end
51
+ text.length
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ # Converter from OPmac TeX markup to HTML5
2
+ module Opmac2html
3
+ VERSION = '0.0.2'
4
+ end
data/lib/opmac2html.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'opmac2html/version'
2
+ require 'opmac2html/converter'
3
+
4
+ module Opmac2html
5
+ # Opmac2html root class/facade
6
+ class Opmac2html
7
+ def initialize(input, output)
8
+ Converter.new(input, output).convert
9
+ end
10
+
11
+ def self.version
12
+ VERSION
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'opmac2html/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "opmac2html"
8
+ spec.version = Opmac2html::VERSION
9
+ spec.authors = ["Martin Kinčl"]
10
+ spec.summary = %q{Converter from OPmac TeX markup to HTML}
11
+ spec.description = %q{A converter of TeX documents written using OPmac macro set
12
+ to HTML5 pages}
13
+ spec.homepage = "https://github.com/kinclma1/opmac2html"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = '>= 2.0'
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+
26
+ spec.add_dependency "slop", "~> 4.0"
27
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opmac2html
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Martin Kinčl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-06-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: slop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
55
+ description: |-
56
+ A converter of TeX documents written using OPmac macro set
57
+ to HTML5 pages
58
+ email:
59
+ executables:
60
+ - opmac2html
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - Gemfile
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - bin/opmac2html
70
+ - lib/opmac2html.rb
71
+ - lib/opmac2html/cli.rb
72
+ - lib/opmac2html/converter.rb
73
+ - lib/opmac2html/html_builder.rb
74
+ - lib/opmac2html/list_builder.rb
75
+ - lib/opmac2html/macro_parser.rb
76
+ - lib/opmac2html/par_builder.rb
77
+ - lib/opmac2html/paragraph_parser.rb
78
+ - lib/opmac2html/preprocessor.rb
79
+ - lib/opmac2html/table_builder.rb
80
+ - lib/opmac2html/text_cutter.rb
81
+ - lib/opmac2html/version.rb
82
+ - opmac2html.gemspec
83
+ homepage: https://github.com/kinclma1/opmac2html
84
+ licenses:
85
+ - MIT
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.4.5
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Converter from OPmac TeX markup to HTML
107
+ test_files: []
108
+ has_rdoc: