Skip to content

Instantly share code, notes, and snippets.

@mackuba
Created June 6, 2020 00:55
Show Gist options
  • Save mackuba/2f67a565d287f7833d6c5378c9b8ce0a to your computer and use it in GitHub Desktop.
Save mackuba/2f67a565d287f7833d6c5378c9b8ce0a to your computer and use it in GitHub Desktop.
Code for formatting hand-written plain text notes into formatted HTML
class NoteCompiler
def build(text, info)
note = NoteContent.new(text, info)
note.compile_html
end
class NoteContent
def initialize(text, info)
@text = text
@info = info
end
def compile_html
@html = ''
@code_block = nil
@text_block = ''
@current_list = nil
@text.lines.each do |line|
line.chomp!('')
content = line.strip
if @code_block
if content == '```'
close_code_block
else
@code_block << line + "\n"
end
else
case content
when '```'
close_text_block
open_code_block
when ''
close_text_block
when /^\-\>? (.*)$/
open_list('ul')
@text_block << "<li>#{process_line($1)}</li>\n"
when /^\d+\. (.*)$/
open_list('ol')
@text_block << "<li>#{process_line(content)}</li>\n" # todo
when /^\=+\s*(\w.*)$/
close_text_block
@html << %(<h3>#{$1}</h3>\n)
else
close_list
@text_block << "<p>#{process_line(content)}</p>\n"
end
end
end
if @code_block
raise "Unfinished code block"
else
close_text_block
end
@html
end
private
def open_code_block
@html << %(<pre class="brush: swift">)
@code_block = ''
end
def close_code_block
@html << process_code_block(@code_block.chop) + %(</pre>\n)
@code_block = nil
end
def process_code_block(code)
code.gsub(/\</, '&lt;').gsub(/\>/, '&gt;')
end
def close_text_block
close_list
if @text_block.length > 0
@html << %(<div class="block">\n#{@text_block}</div>\n)
@text_block = ''
end
end
def open_list(list_type)
if @current_list.nil?
@text_block << "<#{list_type}>\n"
elsif @current_list != list_type
@text_block << "</#{@current_list}>\n"
@text_block << "<#{list_type}>\n"
end
@current_list = list_type
end
def close_list
if @current_list
@text_block << "</#{@current_list}>\n"
@current_list = nil
end
end
def process_line(line)
line = line.clone
line.gsub!(/\&/, '&amp;')
line.gsub!(/\</, '&lt;')
line.gsub!(/\>/, '&gt;')
line.gsub!(/^(\p{Pi}.*\p{Pf})$/, "<em>\\1</em>")
pieces = line.split(/`/, -1)
raise "Unmatched code block mark: #{line.inspect}" if pieces.length.even?
pieces.each_with_index.map { |piece, i|
if i.even?
process_line_piece(piece)
else
%(<code>#{piece}</code>)
end
}.join
end
def process_line_piece(line)
line = line.clone
code_blocks = find_code_blocks(line)
updated_code_blocks = apply_code_blocks(code_blocks, line)
replacements = [
[/ - /, '&nbsp;– '],
[/&lt;-&gt;/, '&nbsp;⭤&nbsp;'],
[/-&gt;/, '&nbsp;⭢&nbsp;'],
]
replacements.each do |pattern, new_text|
line.gsub!(pattern) do
range = Regexp.last_match.offset(0)
if updated_code_blocks.any? { |s, f| s < range[1] && f > range[0] }
line[range[0]...range[1]]
else
new_text
end
end
end
# don't break line before emoticons
[':)', ';)', ':>', ':]', ':(', ':\\', ':/', ':|', 'o_O'].each do |emot|
regexp = emot.split(//).map { |ch| "\\" + ch }.join
line.gsub!(%r(\s#{regexp}), "&nbsp;#{emot}")
end
line
end
def find_code_blocks(line)
code_blocks = []
non_code_identifiers = %w(SwiftUI e.g. e.g iPhone iPhones iPad iPads WatchConnectivity)
non_code_patterns = [/^\w+Kit$/, /^Core[A-Z]\w+$/, /^[a-z]+OS$/, /^[A-Z]+$/, /^[A-Z]+s$/, /^[A-Z]+Ses$/]
code_regexps = [
# x(), x.y()
[/(\B(\.|@))?\b(\w[\w\.\*]*\w\([^)]*\))\B/, 0],
# x { ... }, x() { ... }
[/(\B(\.|@))?\b(\w[\w\.\*]*\w(\([^)]*\))?\s*\{[^}]*\})\B/, 0],
# x -> y
[/\b([a-z][\w\.\*]*[A-Z][\w\.\*]*\w\s*-&gt;\s*\w*(\?\B|\*\B|\w\b))/, 0],
# x() -> y
[/\b(\w[\w\.\*]*\w\([^)]*\)\s*-&gt;\s*\w*(\?\B|\*\B|\w\b))/, 0],
# x:y: -> y
[/\b(\w[\w\*]*\:(\w[\w\*]*\:)+\s*-&gt;\s*\w*(\?\B|\*\B|\w\b))/, 0],
# x.y
[/\b([\w\*]+\.([\w\*]+\*\B|[\w\*]+\b|\*\B))/, 0],
# X<Y>
[/\b([\w\*]+&lt;[\w\*]+&gt;)\B/, 0],
# @zxc
[/\B@([\w\*]+)(\*\B|\b)/, 0],
# identifier
[/(\B\.)?\b(\w[\w\*]*[A-Z][\w\*]*)(\*\B|\?\B|\b)/, 0],
# identifier:
[/\b([a-z][\w\*]*[A-Z][\w\*]*)\:\B/, 1],
# foo:will:bar:
[/\b\w[\w\*]*\:(\w[\w\*]*\:)+\B/, 0]
]
code_regexps.each do |pattern, i|
line.scan(pattern) do |names|
match = Regexp.last_match
name = match[i]
unless non_code_identifiers.include?(name) || non_code_patterns.any? { |pt| name =~ pt }
code_blocks << match.offset(0)
end
end
end
code_blocks
end
def apply_code_blocks(code_blocks, line)
code_blocks.sort_by(&:first).reduce([]) { |list, block|
if list.empty?
[block]
elsif block[0] <= list.last[1]
prev = list.pop
new_end = [prev[1], block[1]].max
new_block = [prev[0], new_end]
list + [new_block]
else
list + [block]
end
}.each_with_index.map { |block, i|
start = block[0] + 13 * i
finish = block[1] + 13 * i
line[start...finish] = "<code>" + line[start...finish] + "</code>"
[start, finish + 13]
}
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment