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 +7 -0
- data/.gitignore +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +23 -0
- data/Rakefile +1 -0
- data/bin/opmac2html +5 -0
- data/lib/opmac2html/cli.rb +28 -0
- data/lib/opmac2html/converter.rb +58 -0
- data/lib/opmac2html/html_builder.rb +102 -0
- data/lib/opmac2html/list_builder.rb +54 -0
- data/lib/opmac2html/macro_parser.rb +176 -0
- data/lib/opmac2html/par_builder.rb +47 -0
- data/lib/opmac2html/paragraph_parser.rb +158 -0
- data/lib/opmac2html/preprocessor.rb +26 -0
- data/lib/opmac2html/table_builder.rb +38 -0
- data/lib/opmac2html/text_cutter.rb +54 -0
- data/lib/opmac2html/version.rb +4 -0
- data/lib/opmac2html.rb +15 -0
- data/opmac2html.gemspec +27 -0
- metadata +108 -0
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
data/Gemfile
ADDED
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,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&\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 << "„#{quote}“"
|
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 '…'
|
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 = { '<' => '<', '>' => '>', '&' => '&' }
|
6
|
+
|
7
|
+
TEXT_SUBST = { '~' => ' ', '--' => '–', '---' => '&mdash',
|
8
|
+
'\\,' => ' ', '\\-' => '', '\\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
|
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
|
data/opmac2html.gemspec
ADDED
@@ -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:
|