Skip to content

Instantly share code, notes, and snippets.

@Mijyuoon
Last active November 13, 2019 17:15
Show Gist options
  • Save Mijyuoon/e92ae390ac1dd821132eae5b9fabc03c to your computer and use it in GitHub Desktop.
Save Mijyuoon/e92ae390ac1dd821132eae5b9fabc03c to your computer and use it in GitHub Desktop.
dictgen
require 'optparse'
require 'parslet'
require 'erubis'
module DicGen
# Dummy interface for node values
module NodeValue
def nodes(*)
enum_for(:nodes) unless block_given?
end
def node(*)
nil
end
def str(*)
nil
end
def dict(*)
nil
end
def is_dict?
false
end
end
# Hackjob: add dummy NodeValue interface to `nil`
class ::NilClass
include NodeValue
end
# Hackjob: add dummy NodeValue interface to strings
class ::String
include NodeValue
end
class Dict
include NodeValue
def initialize(nodes)
@nodes = nodes.freeze
end
def nodes(dict = nil, name = nil)
if block_given?
@nodes.each do |n|
next unless name.nil? || n.name == name
next unless dict.nil? || n.dict? == dict
yield n
end
else
enum_for(:nodes, name, dict)
end
end
def node(name, dict = nil)
@nodes.find {|n| n.name == name && (dict.nil? || n.dict? == dict) }
end
def str(name, default = nil)
node(name, false)&.value || default
end
def dict(name)
node(name, true)&.value
end
def is_dict?
true
end
end
class Node
attr_accessor :name, :value
def initialize(name, value)
@name, @value = name, value
end
def dict?
@value.is_dict?
end
def to_ary
[@name, @value]
end
end
class Parser < Parslet::Parser
root(:file)
rule(:file) do
ws? >> pair.repeat.as(:dict)
end
rule(:dict) do
str('{') >> ws? >> (
pair.repeat.as(:dict)
) >> str('}') >> ws?
end
rule(:pair) do
(name | qstr).as(:key) >>
(qstr | mstr | null | dict).as(:val)
end
rule(:name) do
match['\p{L}\p{N}_'].repeat(1).as(:sym) >> ws?
end
rule(:qstr) do
str('"') >> (
(str('\\') >> any) |
(str('"').absent? >> any)
).repeat.as(:str) >>
str('"') >> ws?
end
rule(:mstr) do
str('[[') >> (
str(']]').absent? >> any
).repeat.as(:mstr) >>
str(']]') >> ws?
end
rule(:null) do
str('.').as(:null) >> ws?
end
rule(:ws?) do
match('\s').repeat
end
end
class Transform < Parslet::Transform
STR_ESCAPE = {
'\\*' => "\n",
}.freeze
rule(sym: simple(:x)) do
x.to_s.to_sym
end
rule(str: simple(:x)) do
str = x.to_s
str.gsub!(/\\./) {|f| STR_ESCAPE.fetch(f, f[-1]) }
str
end
rule(mstr: simple(:x)) do
str = x.to_s
str.gsub!(/^\n|\n[ \t]*$/, '')
indent = str.scan(/^[ \t]*(?=\S)/).min
str.gsub!(/^#{indent}/, '')
str
end
rule(null: simple(:_)) do
nil
end
rule(key: simple(:k), val: subtree(:v)) do
DicGen::Node.new(k, v)
end
rule(dict: subtree(:pp)) do
DicGen::Dict.new(pp)
end
end
end
options = {
tmpl: "#{File.dirname(__FILE__ )}/template.html.erb"
}
OptionParser.new do |opts|
opts.banner = 'Usage: dictgen.rb [options] input output'
opts.on('-t', '--template=FILENAME', 'Output template filename') do |v|
options[:tmpl] = v
end
opts.on('-h', '--help', 'Show this message') do
$stdout.puts(opts)
exit(0)
end
end.parse!
if ARGV.length < 2
$stderr.puts('Insufficient arguments provided')
exit(-1)
end
options[:input], options[:output] = ARGV.shift(2)
begin
tmpl = File.read(options[:tmpl])
input = File.read(options[:input])
erb = Erubis::Eruby.new(tmpl)
parser = DicGen::Parser.new
transform = DicGen::Transform.new
data = transform.apply(parser.parse(input))
output = erb.result(:@words => data)
File.write(options[:output], output)
end
<!doctype html>
<head>
<title>Dictionary</title>
<meta charset="utf-8"/>
<style>
html {
font-family: sans-serif;
font-size: 12pt;
}
body {
display: flex;
}
a[href^="#"] {
color: #7070ff;
text-decoration: none;
}
.word-list {
min-width: 12rem;
border-right: 1px solid black;
}
.word-list .header {
font-size: 18pt;
font-weight: bold;
}
.content {
margin-left: 0.80rem;
}
.list-block {
margin-top: 0.33rem;
font-size: 14pt;
list-style-type: "→ ";
}
.word-block .word {
font-size: 18pt;
font-weight: bold;
margin-bottom: 0.66rem;
}
.entry-block {
margin-bottom: 1.50rem;
margin-left: 0.66rem;
}
.entry-block .header {
font-weight: bold;
}
.entry-block .header .note {
font-weight: normal;
font-style: italic;
}
.entry-block .raw {
margin-top: 0.33rem;
margin-left: 1.25rem;
}
.trans-block {
margin: 0rem;
}
.trans-block > li {
margin-bottom: 0.66rem;
}
.trans-block .trans,
.trans-block .ex1 {
margin-top: 0.33rem;
}
.trans-block .note {
font-style: italic;
}
.trans-block .ex1 {
margin-left: 1.25rem;
}
.ipa-font {
font-family: "Charis SIL", "Doulos SIL", serif;
}
</style>
</head>
<body>
<% words = @words.nodes.sort {|x,y| x.name <=> y.name } %>
<div class="word-list">
<div class="header">Word list</div>
<ul class="list-block">
<% words.each do |word, _| %>
<li>
<a href="#<%= word %>"><%= word %></a>
</li>
<% end %>
</ul>
</div>
<div class="content">
<% words.each do |word, entries| %>
<div class="word-block" id="<%= word %>">
<div class="word">
<a href="#<%= word %>"><%= word %></a>
</div>
<% entries.nodes do |entry, data| %>
<% if entry == :word && data.is_dict? %>
<div class="entry-block">
<div class="header">
<span><%= data.str(:class, "Unknown") %></span>
<% if (note = data.str(:note)) %>
<span class="note">(<%= note %>)</span>
<% end %>
</div>
<% if (trlist = data.dict(:trans)) %>
<ol class="trans-block">
<% trlist.nodes do |trans, trinfo| %>
<li>
<div class="trans">
<% if (note = trinfo.str(:note)) %>
<span class="note">(<%= note %>)</span>
<% end %>
<span><%= trans %></span>
</div>
<% trinfo.nodes(false, :ex1) do |_, ex1| %>
<div class="ex1"><%= ex1 %></div>
<% end %>
</li>
<% end %>
</ol>
<% end %>
</div>
<% elsif entry == :etymo %>
<div class="entry-block">
<div class="header">Etymology</div>
<div class="raw"><%= data %></div>
</div>
<% elsif entry == :phono %>
<div class="entry-block">
<div class="header">Pronunciation</div>
<div class="raw">
<span>IPA: </span>
<span class="ipa-font">/<%= data %>/</span>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment