opmac2html 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: