├── javascript └── test.js ├── files └── test_include.md ├── scripts ├── env.rb ├── bear-pro ├── mindnode ├── github-pre ├── github-pro ├── lib │ ├── helpdocs.rb │ ├── settings.rb │ ├── util.rb │ ├── knowledge.rb │ ├── porterstemmer.rb │ ├── helppdfbuilder.rb │ ├── pdfbuilder.rb │ ├── string.rb │ └── helpbuilder.rb ├── bear ├── jekyll-pre ├── taskpaper ├── obsidian-callouts ├── obsidian-md-filter ├── jekyll-post └── bunch-post ├── README.md ├── tracks.yaml └── css └── callouts.css /javascript/test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/test_include.md: -------------------------------------------------------------------------------- 1 | This is some included Markdown 2 | -------------------------------------------------------------------------------- /scripts/env.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | warn ENV 4 | 5 | puts 'NOCUSTOM' 6 | -------------------------------------------------------------------------------- /scripts/bear-pro: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat | cmark-gfm -e table -e footnotes -e strikethrough -e autolink -e tasklist 4 | -------------------------------------------------------------------------------- /scripts/mindnode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | input = $stdin.read 5 | puts "" 6 | puts 7 | puts input 8 | -------------------------------------------------------------------------------- /scripts/github-pre: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | input = $stdin.read 5 | puts '' 6 | puts 7 | puts input 8 | -------------------------------------------------------------------------------- /scripts/github-pro: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | puts 'NOCUSTOM' 5 | Process.exit 0 6 | require 'shellwords' 7 | 8 | input = $stdin.read.force_encoding('utf-8') 9 | 10 | puts `echo #{Shellwords.escape(input)} | /Users/ttscoff/.asdf/shims/rdiscount` 11 | -------------------------------------------------------------------------------- /scripts/lib/helpdocs.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | %w[shellwords erb yaml rubygems fileutils nokogiri json cgi time pp].each do |filename| 4 | require filename 5 | end 6 | 7 | require 'util.rb' 8 | require 'settings.rb' 9 | require 'porterstemmer.rb' 10 | require 'knowledge.rb' 11 | require 'string.rb' 12 | require 'helpbuilder.rb' 13 | require 'helppdfbuilder.rb' 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample config for Marked Conductor 2 | 3 | This repo is my actual setup for [Conductor](https://brettterpstra.com/projects/conductor). It's a work in progress; I'm still dissecting the jekyll-pre script that built up over the last 10 years and contains a bunch of conditional statements that need to be moved into individual scripts triggered by Conductor conditions. But this should give you an idea how to set it up. 4 | -------------------------------------------------------------------------------- /scripts/lib/settings.rb: -------------------------------------------------------------------------------- 1 | DEFAULT_SETTINGS = { 2 | :should_deploy => false, 3 | :should_build_index => true, 4 | :debug => '', 5 | :pdfext => '.pdf', # empty or ".test" 6 | :status => STDERR, 7 | :baseurl => 'http://dev.nvultra/help/', 8 | :skipwords => ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "one", "two", "three", "four", "five", "about", "actually", "always", "even", "given", "into", "just", "not", "Im", "thats", "its", "arent", "weve", "ive", "didnt", "dont", "the", "of", "to", "and", "a", "in", "is", "it", "you", "that", "he", "was", "for", "on", "are", "with", "as", "I", "his", "they", "be", "at", "one", "have", "this", "from", "or", "had", "by", "hot", "but", "some", "what", "there", "we", "can", "out", "were", "all", "your", "when", "up", "use", "how", "said", "an", "each", "she", "which", "do", "their", "if", "will", "way", "many", "then", "them", "would", "like", "so", "these", "her", "see", "him", "has", "more", "could", "go", "come", "did", "my", "no", "get", "me", "say", "too", "here", "must", "such", "try", "us", "own", "oh", "any", "youll", "youre", "also", "than", "those", "though", "thing", "things"] 9 | } 10 | -------------------------------------------------------------------------------- /scripts/bear: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'cgi' 5 | 6 | input = $stdin.read.force_encoding('utf-8') 7 | 8 | # Handle [[links]] 9 | input.gsub!(/\[\[(?.*?)\]\]/) do 10 | m = Regexp.last_match 11 | # Test for link|title matchup 12 | if m['content'] =~ /(?.+)\|(?.+)/ 13 | l = Regexp.last_match 14 | title = l['title'] 15 | link = l['link'] 16 | else 17 | title = m['content'] 18 | link = m['content'] 19 | end 20 | 21 | # Test for note/header matchup 22 | if link =~ %r{(?<page>.+)/(?<header>.+)} 23 | l = Regexp.last_match 24 | link = l['page'] 25 | header = l['header'] 26 | else 27 | header = nil 28 | end 29 | 30 | header = header.nil? ? '' : "&header=#{CGI.escape(header).gsub(/\+/, '%20')}" 31 | 32 | url = CGI.escape(link).gsub(/\+/, '%20') 33 | callback = CGI.escape("bear://x-callback-url/create?title=#{url}").gsub(/\+/, '%20') 34 | "[#{title}](bear://x-callback-url/open-note?title=#{url}#{header}&x-error=#{callback})" 35 | end 36 | 37 | # Handle ==highlight== 38 | input.gsub!(/==(.*?)==/, '<mark>\1</mark>') 39 | 40 | # Handle ~~deletion~~ 41 | input.gsub!(/~~(.*?)~~/, '<del>\1</del>') 42 | 43 | # Handle ~underline~ 44 | input.gsub!(/~(.*?)~/, '<span class="underline">\1</span>') 45 | 46 | # Handle tags, linking to tag pages in Bear 47 | input.gsub!(%r{(?!<#)#(?<tag>[^\s#,?.!]+)}i) do 48 | m = Regexp.last_match 49 | tag = CGI.escape(m['tag']) 50 | %(<span class="mkstyledtag"><a href="bear://x-callback-url/open-tag?name=#{tag}">##{m['tag']}</a></span>) 51 | end 52 | puts '<!--marked style: bear-->' 53 | puts 54 | puts input 55 | -------------------------------------------------------------------------------- /scripts/lib/util.rb: -------------------------------------------------------------------------------- 1 | module Utils 2 | 3 | def colorize(string) 4 | unless ENV["TERM_PROGRAM"] =~ /(iTerm.app|Apple_Terminal)/ 5 | return string 6 | end 7 | colors = { 8 | 'red' => "\033[1;31m", 9 | 'green' => "\033[32m", 10 | 'yellow' => "\033[33m", 11 | 'blue' => "\033[1;34m", 12 | 'magenta' => "\033[1;35m", 13 | 'cyan' => "\033[1;36m", 14 | 'white' => "\033[1;37m", 15 | 'r' => "\033[0;39m" 16 | } 17 | 18 | string.sub!(/^(\[[\d:]+\]: )?([^a-z0-9 ]+)?(.*)/i, %Q{%%green%%\\1%%yellow%%\\2%%white%%\\3%%r%%}) 19 | string.gsub!(/([a-z]+ing|[a-z]+[it]ed)\b/i,%Q{%%magenta%%\\1%%r%%}) 20 | string.gsub!(/\b((?:keyword )?index|sidebar|(?:home )?page|images?|css|changelog|search|js)/i,%Q{%%cyan%%\\1%%r%%}) 21 | string.gsub!(/(error:?)/i,%Q{%%red%%\\1%%r%%}) 22 | string.gsub!(/%%(\w+?)%%/) {|m| 23 | colors[$1] 24 | } 25 | end 26 | module_function :colorize 27 | 28 | def load_config (config_file) 29 | File.open(config_file) { |yf| YAML::load(yf) } 30 | end 31 | module_function :load_config 32 | 33 | def dump_config (config) 34 | File.open(config_file, 'w') { |yf| YAML::dump(config, yf) } 35 | end 36 | module_function :dump_config 37 | 38 | def load_template (template) 39 | return IO.read(template) 40 | end 41 | module_function :load_template 42 | 43 | def find_headers(lines) 44 | in_headers = false 45 | lines.each_with_index {|line, i| 46 | if line =~ /^\S[^\:]+\:( .*?)?$/ 47 | in_headers = true 48 | elsif in_headers === true 49 | return i 50 | else 51 | return false 52 | end 53 | } 54 | end 55 | module_function :find_headers 56 | 57 | def remove_todos(text) 58 | text.gsub!(/(^(TODO|FIX(ME)?):.*?$|\s*\((TODO|FIX(ME)?):.*?\))/,'') 59 | out = text.split(/(<!-- *NOTES *-->|__(NOTES|END|TODO)__)/i)[0] 60 | out 61 | end 62 | module_function :remove_todos 63 | 64 | def update_status(update,options = {}) 65 | 66 | last = options[:last] || false 67 | 68 | unless ENV['TERM'] 69 | ENV['TERM'] = "xterm" 70 | end 71 | 72 | # Get the terminal width using *nix `tput` command running every time to try to handle resizing windows 73 | begin 74 | cols = %x{tput cols}.strip.to_i - 17 75 | rescue 76 | cols = 68 77 | end 78 | # trim output so it doesn't break to a second line 79 | update = update.slice(0,cols) if update.length > cols 80 | # if it's not the last output, use a carriage return instead of a newline as terminator 81 | terminator = last ? "\n" : "\r" 82 | # add date 83 | t = Time.now.strftime('%H:%M:%S') 84 | update = colorize(%Q{[#{t}]: #{update}}) 85 | # Print to STDERR 86 | DEFAULT_SETTINGS[:status].printf("\033[K%s%s",update,terminator) 87 | 88 | DEFAULT_SETTINGS[:status].flush if last 89 | end 90 | module_function :update_status 91 | 92 | end 93 | 94 | -------------------------------------------------------------------------------- /scripts/jekyll-pre: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -W1 2 | # frozen_string_literal: true 3 | 4 | # Version 2 (02-03-2015) 5 | # 6 | # Example custom processor for use with Marked <http://markedapp.com> and Jekyll _posts 7 | # It's geared toward my personal set of plugins and tags, but you'll get the idea. 8 | # It turns 9 | # {% img alignright /images/heythere.jpg 100 100 "Hey there" "hi" %} 10 | # into 11 | # <img src="../images/heythere.jpg" alt="Hey there" class="alignright" title="hi" /> 12 | # 13 | # replaces alignleft and alignright classes with appropriate style attribute 14 | # --- 15 | # Replaces {% gist XXXXX filename.rb %} with appropriate script tag 16 | # 17 | # Replace various other OctoPress, Jekyll and custom tags 18 | # 19 | # Processes final output with /usr/bin/kramdown (install kramdown as system gem: `sudo gem install kramdown`) 20 | # 21 | # Be sure to run *without* stripping YAML headers in Marked Behavior preferences. 22 | 23 | require 'rubygems' 24 | require 'shellwords' 25 | require 'kramdown' 26 | require 'uri' 27 | require 'cgi' 28 | require 'erb' 29 | require 'logger' 30 | require 'nokogiri/nokogiri' 31 | 32 | $LOAD_PATH.unshift File.join("#{File.dirname(__FILE__)}/lib") 33 | require 'helpdocs' 34 | 35 | @logger = Logger.new(File.expand_path('~/logs/jekyllpre.log')) 36 | 37 | def class_exists?(class_name) 38 | klass = Module.const_get(class_name) 39 | klass.is_a?(Class) 40 | rescue NameError 41 | false 42 | end 43 | 44 | if class_exists? 'Encoding' 45 | Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external') 46 | Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal') 47 | end 48 | 49 | begin 50 | content = $stdin.read.force_encoding('utf-8') 51 | rescue StandardError 52 | content = $stdin.read 53 | end 54 | 55 | class String 56 | def inject_meta(string) 57 | keys = `echo #{Shellwords.escape(self)}|multimarkdown -m`.strip 58 | if keys.empty? 59 | "#{string}\n\n#{self}" 60 | else 61 | "#{string}\n#{self}" 62 | end 63 | end 64 | 65 | def inject_meta!(string) 66 | replace inject_meta(string) 67 | end 68 | end 69 | 70 | def process_docs(content) 71 | content = content.render_liquid(pdf: 'marked') 72 | content = Utils.remove_todos(content) 73 | if ENV['MARKED_ORIGIN'] =~ /nvultra/ 74 | content.inject_meta!('Marked CSS: nvUltra') 75 | elsif ENV['MARKED_ORIGIN'] =~ /HelpDocs/ 76 | title = File.basename(ENV['MARKED_PATH'], '.md').gsub(/_/, ' ') 77 | content.sub!(/<%= @title %>/, title) 78 | content.inject_meta!('Marked CSS: Marked Help') 79 | end 80 | puts content 81 | end 82 | 83 | if ENV['MARKED_ORIGIN'] =~ /(HelpDocs|nvultra)/i 84 | warn('Looks like help docs') 85 | process_docs(content) 86 | else 87 | source = File.dirname(ENV['MARKED_PATH']) 88 | content.gsub!(/\[\[(.*?)\]\]/) do 89 | title = Regexp.last_match(1) 90 | "[#{title}](#{File.join(source, ERB::Util.url_encode(title).sub(/(\.md)?$/, '.md'))})" 91 | end 92 | style = ENV['MARKED_ORIGIN'] =~ %r{/bunch/} ? 'Bunch' : 'brettterpstra-2023' 93 | content.sub!(/^---\n/m, "---\nmarked style: #{style}\n") 94 | puts content 95 | end 96 | -------------------------------------------------------------------------------- /scripts/taskpaper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'kramdown' 5 | 6 | def process_taskpaper(content) 7 | header = content.scan(/Format: .*$/) || [] 8 | output = '' 9 | prevlevel = 0 10 | begin 11 | content.split("\n").each do |line| 12 | if line =~ /^(\t+)?(.*?):(\s.*)?$/ 13 | m = Regexp.last_match 14 | tabs = m[1] 15 | project = m[2] 16 | if tabs.nil? 17 | output += "\n## #{project} ##\n\n" 18 | prevlevel = 0 19 | else 20 | output += "#{tabs.gsub(/^\t/, '')}* **#{project.gsub(/^\s*-\s*/, '')}**{:.project}\n" 21 | prevlevel = tabs.length 22 | end 23 | elsif line =~ /^(\t+)?- (.*)$/ 24 | m = Regexp.last_match 25 | task = m[2] 26 | tabs = m[1].nil? ? '' : m[1] 27 | task.gsub!(/(@[^ \n\r(]+)((\()([^)]+)(\)))?/, '*\1*{:.tag}') 28 | 29 | task = if task =~ /@done/ 30 | "- [x] <del>#{task}</del>" 31 | else 32 | "- [ ] #{task}" 33 | end 34 | if tabs.length - prevlevel > 1 35 | tabs = "\t" 36 | prevlevel.times { tabs += "\t" } 37 | end 38 | tabs = '' if prevlevel.zero? && tabs.length.positive? 39 | output += "#{tabs.gsub(/^\t/, '')}#{task.strip}\n" 40 | prevlevel = tabs.length 41 | else 42 | next if line =~ /^\s*$/ 43 | 44 | tabs = '' 45 | (prevlevel - 1).times { tabs += "\t" } unless prevlevel.zero? 46 | output += "- #{tabs}*#{line.strip}*{:.note}\n" 47 | end 48 | end 49 | final = header.nil? ? '' : "#{header.join("\n")}\n\n" 50 | final = final.gsub(/\|/, '\|') 51 | style = <<~EOSTYLE 52 | <style> 53 | li.project { 54 | list-style: none; 55 | font-size: 1.2rem; 56 | } 57 | li.project::before { 58 | content: '>'; 59 | color: #aaa; 60 | margin-left: -1.25rem; 61 | position: absolute; } 62 | del { 63 | color: #aaa; } 64 | .tag strong { 65 | font-weight: normal; 66 | color: #555 } 67 | .tag a { 68 | text-decoration: none; 69 | border: none; 70 | color:#777 } 71 | li.note { 72 | list-style: none; 73 | text-indent: 2em; 74 | font-size: .85em } 75 | </style> 76 | 77 | EOSTYLE 78 | 79 | final += style 80 | # title = File.basename(ENV['MARKED_PATH'],'.taskpaper') || "TaskPaper Preview" 81 | final += output 82 | 83 | final.gsub!(/\[\[(.*?)\]\]/) do 84 | note = Regexp.last_match(1) 85 | escaped = ERB::Util.url_encode(note) 86 | %([#{note}](nvalt://find/#{escaped})) 87 | end 88 | 89 | script = <<~ENDSCRIPT 90 | <script>(function($){ 91 | $('em.note').closest('li').addClass('note'); 92 | $('strong.project').closest('li').addClass('project'); 93 | })(jQuery);</script> 94 | ENDSCRIPT 95 | Kramdown::Document.new(final).to_html + "\n\n#{script}" 96 | rescue StandardError => e 97 | warn e 98 | warn e.backtrace 99 | raise 100 | end 101 | end 102 | 103 | puts process_taskpaper($stdin.read.force_encoding('utf-8')) 104 | -------------------------------------------------------------------------------- /tracks.yaml: -------------------------------------------------------------------------------- 1 | tracks: 2 | - title: Preprocessing 3 | condition: phase is pre 4 | tracks: 5 | - title: VoiceOver test 6 | condition: filename is wrap_em.md 7 | script: wrap_em.sh 8 | - title: Test File 9 | condition: filename ends with test.md 10 | sequence: 11 | # - filter: insertCSS(https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css) 12 | # - filter: insertScript(https://unpkg.com/mermaid@10/dist/mermaid.min.js) 13 | - filter: autoLink() 14 | - filter: insertTitle(yes) 15 | - filter: fixHeaders 16 | # - filter: insertScript(test.js) 17 | # - filter: prependFile(test_include.md) 18 | # - filter: insertTOC(3, h1) 19 | - filter: insertCSS(callouts) 20 | # - filter: insertTOC() 21 | - filter: setMeta(processor, gfm) 22 | - title: MindNode map 23 | condition: filename ends with mindnode 24 | filter: setStyle(ink) 25 | - title: Obsidian Markdown document 26 | condition: tree contains .obsidian AND (extension is md OR extension is markdown) 27 | sequence: 28 | - script: obsidian-md-filter 29 | - script: obsidian-callouts 30 | - filter: insertStylesheet(callouts) 31 | - title: GitHub README 32 | condition: filename is README.md 33 | filter: setStyle(github) 34 | - title: Bear Preview 35 | condition: "text contains <!-- source: Bear.app -->" 36 | sequence: 37 | - script: bear 38 | - title: Blog post or Bunch docs 39 | condition: (path contains _drafts OR path contains _posts OR path contains /bunch/) AND has yaml 40 | script: jekyll-pre 41 | - title: Help docs 42 | condition: path contains HelpDocs OR path contains Sites/dev/bunch OR path contains Code/nvultra-docs 43 | script: jekyll-pre 44 | - title: Processing 45 | condition: phase is pro 46 | tracks: 47 | # - title: Test File including linktest.md 48 | # condition: includes contains file linktest.md 49 | # command: echo NOCUSTOM 50 | - title: Test file 51 | condition: filename ends with test.md 52 | sequence: 53 | - filter: fixHeaders() 54 | - filter: removeMeta() 55 | # - command: cmark-gfm -e table -e footnotes -e strikethrough -e tasklist 56 | - command: kramdown 57 | - title: Obsidian Markdown document 58 | condition: tree contains .obsidian AND (extension is md OR extension is markdown) 59 | command: echo "MMD" 60 | - title: Confluence conversion 61 | condition: path contains Confluence 62 | command: markdown_py -x extra 63 | - title: TaskPaper file 64 | condition: ext is taskpaper OR text contains @taskpaper 65 | script: taskpaper 66 | - title: GitHub README 67 | condition: filename is README.md 68 | script: github-pro 69 | - title: Blog post 70 | condition: (path contains _drafts OR path contains _posts) AND has yaml 71 | script: jekyll-post 72 | - title: Bunch docs 73 | condition: extension is md AND path contains /bunch/ AND has yaml 74 | script: bunch-post 75 | - title: nvUltra or Marked docs 76 | condition: path contains HelpDocs OR path contains Code/nvultra-docs 77 | sequence: 78 | - command: echo MMD 79 | -------------------------------------------------------------------------------- /scripts/lib/knowledge.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | class KnowlegeBase 4 | include Utils 5 | KB_APIKEY = "adc3d3d5cbbb264835c2a288df46f546c0778310" 6 | KB_SITE = "marked" 7 | KB_EXPORTDIR = "/Users/ttscoff/Desktop/Code/marked/HelpDocs/content" 8 | 9 | def get_kb_response(url) 10 | cmd = %Q{curl -sS -H "Accept: application/vnd.tender-v1+json" -H "X-Tender-Auth: #{KB_APIKEY}" #{url}} 11 | res = %x{#{cmd}} 12 | JSON.parse(res) 13 | end 14 | 15 | def retrieve_kb 16 | kbsections = [] 17 | # main_index = File.join(KB_EXPORTDIR,"Knowledgebase.md") 18 | 19 | get_kb_response("http://api.tenderapp.com/#{KB_SITE}/sections")['sections'].each do |section| 20 | body = '' 21 | toc = '' 22 | unless section['faqs_count'] > 0 23 | next 24 | end 25 | faqs = get_kb_response("#{section['href']}/faqs")['faqs'] 26 | sec_title = section['title'] 27 | sec_folder = sec_title.sanitized 28 | sec_index = File.join(KB_EXPORTDIR, "#{sec_folder}.md") 29 | # %w(beta draft).each {|sub| # create beta and draft folders for section 30 | # FileUtils.mkdir_p(File.join(KB_EXPORTDIR, sub)) 31 | # } 32 | # sec_toc = "# #{sec_title}\n\n" 33 | # toc << "\n\n## [#{sec_title}](kb_#{sec_folder}_index.hml)\n\n" 34 | # kbpages = [] 35 | faqs.each do |faq| 36 | title = faq['title'] 37 | toc << "* [#{title}](##{title.sanitized.downcase})\n" 38 | # filename = sec_folder + '_' + title.sanitized + ".md" 39 | # file = File.join(KB_EXPORTDIR, filename) 40 | # if faq['beta'] 41 | # file = File.join(KB_EXPORTDIR, 'beta', filename) 42 | # elsif Time.parse(faq['published_at']) > Time.now 43 | # file = File.join(KB_EXPORTDIR, 'draft', filename) 44 | # else 45 | # toc << "* [#{title}](##{title.sanitize})\n" 46 | # toc << "* [#{title}](#{filename.sub(/md$/,'html')})\n" 47 | # kbpages << { 48 | # 'title' => title, 49 | # 'file' => title.sanitized # File.join('knowledgebase', sec_folder, title.sanitized) 50 | # } 51 | # end 52 | # classes << 'beta' if faq['beta'] 53 | # classes << 'important' if faq['important'] 54 | # classes << 'draft' if Time.parse(faq['published_at']) > Time.now 55 | body << "## #{title} [#{title.sanitized.downcase}]\n\n" 56 | body << faq['body']. 57 | gsub(/\/assets/, "http://#{KB_SITE}.tenderapp.com/help/assets"). 58 | # gsub(/\/help\/assets/, "http://#{KB_SITE}.tenderapp.com/help/assets"). 59 | gsub(/^#/,'##') 60 | body << "\n\n[Table of contents](#toc)\n\n" 61 | # File.open(file, "w") { |f| f.write body } 62 | 63 | Utils.update_status ("retrieved #{sec_title}/#{title}") 64 | end 65 | kbsections << { 66 | 'title' => sec_title, 67 | 'file' => sec_folder 68 | } 69 | output = "# <%= @title %> [toc]\n\n" + body # + toc + "\n\n" + body 70 | File.open(sec_index, "w") { |f| f.write output } 71 | Utils.update_status("@@@ index for #{sec_title} written to #{sec_index}",{:last=>true}) 72 | end 73 | # File.open(main_index, "w") { |f| f.write toc } 74 | # $stderr.puts("@@@ knowledge base index written to #{main_index}") 75 | return kbsections 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /scripts/lib/porterstemmer.rb: -------------------------------------------------------------------------------- 1 | module Text # :nodoc: 2 | module PorterStemming 3 | 4 | STEP_2_LIST = { 5 | 'ational' => 'ate', 'tional' => 'tion', 'enci' => 'ence', 'anci' => 'ance', 6 | 'izer' => 'ize', 'bli' => 'ble', 7 | 'alli' => 'al', 'entli' => 'ent', 'eli' => 'e', 'ousli' => 'ous', 8 | 'ization' => 'ize', 'ation' => 'ate', 9 | 'ator' => 'ate', 'alism' => 'al', 'iveness' => 'ive', 'fulness' => 'ful', 10 | 'ousness' => 'ous', 'aliti' => 'al', 11 | 'iviti' => 'ive', 'biliti' => 'ble', 'logi' => 'log' 12 | } 13 | 14 | STEP_3_LIST = { 15 | 'icate' => 'ic', 'ative' => '', 'alize' => 'al', 'iciti' => 'ic', 16 | 'ical' => 'ic', 'ful' => '', 'ness' => '' 17 | } 18 | 19 | SUFFIX_1_REGEXP = /( 20 | ational | 21 | tional | 22 | enci | 23 | anci | 24 | izer | 25 | bli | 26 | alli | 27 | entli | 28 | eli | 29 | ousli | 30 | ization | 31 | ation | 32 | ator | 33 | alism | 34 | iveness | 35 | fulness | 36 | ousness | 37 | aliti | 38 | iviti | 39 | biliti | 40 | logi)$/x 41 | 42 | SUFFIX_2_REGEXP = /( 43 | al | 44 | ance | 45 | ence | 46 | er | 47 | ic | 48 | able | 49 | ible | 50 | ant | 51 | ement | 52 | ment | 53 | ent | 54 | ou | 55 | ism | 56 | ate | 57 | iti | 58 | ous | 59 | ive | 60 | ize)$/x 61 | 62 | C = "[^aeiou]" # consonant 63 | V = "[aeiouy]" # vowel 64 | CC = "#{C}(?>[^aeiouy]*)" # consonant sequence 65 | VV = "#{V}(?>[aeiou]*)" # vowel sequence 66 | 67 | MGR0 = /^(#{CC})?#{VV}#{CC}/o # [cc]vvcc... is m>0 68 | MEQ1 = /^(#{CC})?#{VV}#{CC}(#{VV})?$/o # [cc]vvcc[vv] is m=1 69 | MGR1 = /^(#{CC})?#{VV}#{CC}#{VV}#{CC}/o # [cc]vvccvvcc... is m>1 70 | VOWEL_IN_STEM = /^(#{CC})?#{V}/o # vowel in stem 71 | 72 | def self.stem(word) 73 | 74 | # make a copy of the given object and convert it to a string. 75 | word = word.dup.to_str 76 | 77 | return word if word.length < 3 78 | 79 | # now map initial y to Y so that the patterns never treat it as vowel 80 | word[0] = 'Y' if word[0] == ?y 81 | 82 | # Step 1a 83 | if word =~ /(ss|i)es$/ 84 | word = $` + $1 85 | elsif word =~ /([^s])s$/ 86 | word = $` + $1 87 | end 88 | 89 | # Step 1b 90 | if word =~ /eed$/ 91 | word.chop! if $` =~ MGR0 92 | elsif word =~ /(ed|ing)$/ 93 | stem = $` 94 | if stem =~ VOWEL_IN_STEM 95 | word = stem 96 | case word 97 | when /(at|bl|iz)$/ then word << "e" 98 | when /([^aeiouylsz])\1$/ then word.chop! 99 | when /^#{CC}#{V}[^aeiouwxy]$/o then word << "e" 100 | end 101 | end 102 | end 103 | 104 | if word =~ /y$/ 105 | stem = $` 106 | word = stem + "i" if stem =~ VOWEL_IN_STEM 107 | end 108 | 109 | # Step 2 110 | if word =~ SUFFIX_1_REGEXP 111 | stem = $` 112 | suffix = $1 113 | # print "stem= " + stem + "\n" + "suffix=" + suffix + "\n" 114 | if stem =~ MGR0 115 | word = stem + STEP_2_LIST[suffix] 116 | end 117 | end 118 | 119 | # Step 3 120 | if word =~ /(icate|ative|alize|iciti|ical|ful|ness)$/ 121 | stem = $` 122 | suffix = $1 123 | if stem =~ MGR0 124 | word = stem + STEP_3_LIST[suffix] 125 | end 126 | end 127 | 128 | # Step 4 129 | if word =~ SUFFIX_2_REGEXP 130 | stem = $` 131 | if stem =~ MGR1 132 | word = stem 133 | end 134 | elsif word =~ /(s|t)(ion)$/ 135 | stem = $` + $1 136 | if stem =~ MGR1 137 | word = stem 138 | end 139 | end 140 | 141 | # Step 5 142 | if word =~ /e$/ 143 | stem = $` 144 | if (stem =~ MGR1) || 145 | (stem =~ MEQ1 && stem !~ /^#{CC}#{V}[^aeiouwxy]$/o) 146 | word = stem 147 | end 148 | end 149 | 150 | if word =~ /ll$/ && word =~ MGR1 151 | word.chop! 152 | end 153 | 154 | # and turn initial Y back to y 155 | word[0] = 'y' if word[0] == ?Y 156 | 157 | word 158 | end 159 | 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /scripts/lib/helppdfbuilder.rb: -------------------------------------------------------------------------------- 1 | 2 | class HelpPDFBuilder 3 | include Utils 4 | attr_accessor :config, :outfolder, :basefolder, :version 5 | def initialize 6 | 7 | @cat = [] 8 | 9 | @settings = DEFAULT_SETTINGS.dup 10 | @internal = '' 11 | @libdir = File.dirname(__FILE__) 12 | @basefolder = File.expand_path(File.join(@libdir,'..')) 13 | @resourcefolder = File.join(@basefolder, 'resources') 14 | 15 | Dir.chdir(@basefolder) 16 | config_file = File.expand_path(@basefolder + "/config.yaml") 17 | 18 | @config = Utils.load_config(config_file) 19 | 20 | @main_title = @config["Title"] 21 | @main_logo = @config["Logo"] 22 | 23 | @version = @config['Version'].to_s 24 | 25 | @outfolder = File.expand_path(@basefolder+"/"+@config["Title"]+".pdf") 26 | 27 | @outpdf = File.join(@basefolder,'nvUltraHelp.pdf') 28 | 29 | @searchindex = [] 30 | 31 | # Make storage directory if needed 32 | FileUtils.mkdir_p(@outfolder,:mode => 0755) unless File.exists? @outfolder 33 | 34 | # titleblock =<<ENDTITLEBLOCK 35 | 36 | # ![](images/logo_large.png) 37 | 38 | # ENDTITLEBLOCK 39 | 40 | # @cat.push(titleblock) 41 | 42 | build_help 43 | 44 | copy_dependencies 45 | 46 | run_pandoc 47 | end 48 | 49 | # I forced most of the headings in the document to be globally unique so 50 | # that when I linked between pages I could always include a hash target, and 51 | # when I generate the PDF I can just remove everything *but* the hash target 52 | # and the same intra-doc links will work in both web and PDF scenarios. 53 | def fix_local_links(input) 54 | input.gsub(/\]\((?!http).*?(#[^\)]+)\)/i, '](\1)') 55 | end 56 | 57 | def run_pandoc 58 | 59 | compiled = @cat.join("\n\n").symbolify_symbols 60 | 61 | compiled = fix_local_links(compiled) 62 | # Most fonts are missing the right triangle character, 63 | # but menlo has it, so we make sure it's always in a code span 64 | compiled.gsub!(/▸/,'`▸`') 65 | 66 | comp_target = File.join(@outfolder,'compiled.md') 67 | 68 | 69 | File.open(comp_target,'w') do |f| 70 | f.puts compiled 71 | Utils.update_status("Compiled Markdown to #{comp_target}") 72 | end 73 | 74 | Dir.chdir(@outfolder) 75 | 76 | # Pandoc, and even more so LaTeX, are black magic to me. I can make them 77 | # do wondrous things by poking at them with different size sticks, but 78 | # I have no consistency. Thus some things are passed as metadata, some as 79 | # variables, some imported into the head from external files, some passed 80 | # on the command line. This will improve with time. 81 | 82 | vars = [ 83 | 'geometry:margin=2cm', 84 | 'fontsize=12pt', 85 | 'title="nvUltra Documentation"', 86 | 'author="Fletcher Penney and Brett Terpstra"', 87 | 'colorlinks' 88 | ] 89 | args = [ 90 | '--top-level-division=section', 91 | '-f markdown_mmd', 92 | "--include-in-header \"#{File.join(@resourcefolder,'chapterbreak.tex')}\"", 93 | '--toc', 94 | '--toc-depth=2', 95 | '--pdf-engine=xelatex', 96 | '--highlight-style=zenburn', 97 | "--template \"#{File.join(@resourcefolder,'pdf_template.template')}\"", 98 | "--metadata=date:#{Time.now.strftime('%Y-%m-%d')}", 99 | "--data-dir=resources" 100 | ] 101 | 102 | Utils.update_status("Compiling PDF with Pandoc") 103 | %x{pandoc compiled.md #{args.join(" ")} -V #{vars.join(" -V ")} -o \"#{@outpdf}\"} 104 | Utils.update_status("Compiled PDF to #{@outpdf}",{:last => true}) 105 | %x{open #{@outpdf}} 106 | end 107 | 108 | def generate_page(page) 109 | Utils.update_status("Generating page: #{page['title']}") 110 | @subtitle = page['title'] 111 | 112 | infile = "#{@basefolder}/content#{@settings[:debug]}/#{page['file']}.md" 113 | 114 | @title = page["title"] 115 | 116 | section_folder = '.' 117 | prefix = '../' 118 | 119 | text = ERB.new(Utils.load_template(infile)).result(binding) 120 | text = text.render_liquid(pdf: true) 121 | 122 | # remove all @2x images for PDF generation 123 | text.gsub!(/@2x(\.(?:png|jpe?g|gif))/,'\1') 124 | 125 | @cat.push(Utils.remove_todos(text)) 126 | end 127 | 128 | def build_help 129 | 130 | @config["Pages"].each_with_index do |page, i| 131 | generate_page(page) unless page['pdf_ignore'] 132 | end 133 | 134 | Utils.update_status("Copying images to #{@outfolder}") 135 | FileUtils.copy_entry("#{@basefolder}/content#{@settings[:debug]}/images","#{@outfolder}/images") 136 | end 137 | 138 | def copy_dependencies 139 | @config["Dependencies"].each do |dep| 140 | src = File.join(@basefolder,@config["DependenciesBase"],dep) 141 | if File.directory?(src) 142 | FileUtils.cp_r(src, @outfolder) 143 | else 144 | FileUtils.copy(src, @outfolder) if dep =~ /(css|jpg|png|gif|json)$/ 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /scripts/lib/pdfbuilder.rb: -------------------------------------------------------------------------------- 1 | class PDFBuilder 2 | include Utils 3 | attr_accessor :config, :filelist, :basefolder, :norepeat, :norepeatlinks, :output 4 | def initialize 5 | 6 | @basefolder = File.join(File.dirname(__FILE__),'..') 7 | Dir.chdir(@basefolder) 8 | config_file = File.expand_path(@basefolder + "/config.yaml") 9 | 10 | @config = Utils.load_config(config_file) 11 | 12 | @filelist = file_list 13 | 14 | @norepeat = [] 15 | @norepeatlinks = [] 16 | 17 | build_help 18 | generate_compiled 19 | end 20 | 21 | def generate_compiled 22 | Utils.update_status("Building Compiled Document",{:last => true}) 23 | 24 | template = ERB.new(Utils.load_template("#{@basefolder}/compile_template.html")) 25 | 26 | section_template = ERB.new <<-SECTIONTEMPLATE 27 | ## <%= @section_title %> 28 | 29 | <%= @lessons %> 30 | 31 | SECTIONTEMPLATE 32 | 33 | lesson_template = ERB.new <<-LESSONTEMPLATE 34 | 35 | --- 36 | 37 | <<[<%= @filename %>.md] 38 | LESSONTEMPLATE 39 | 40 | @main_title = @config["Title"] 41 | 42 | @sections = "" 43 | @config["Sections"].each do |section| 44 | @section_title = section["title"] 45 | @section_folder = section["folder"] 46 | @section_class = section_folder.downcase.gsub(/[-_]/,'') 47 | @lessons = "" 48 | section["pages"].each do |page| 49 | @title = page["title"] 50 | @filename = page["file"] 51 | @page_class = filename.downcase.gsub(/[-_]/,'') 52 | @lessons += lesson_template.result(binding) 53 | end 54 | @sections += section_template.result(binding) 55 | end 56 | 57 | outfile = File.new(@basefolder+'/content'+@settings[:pdfext]+'/Compiled.md', "w+") 58 | outfile.write(template.result(binding)) 59 | # outfile.write(content) 60 | outfile.close 61 | end 62 | 63 | def convert_links(text) 64 | output = [] 65 | links = text.scan(/\]\(([^\)]+)\)/) 66 | refs = text.scan(/^\s{0,3}\[([^\]]+)\]: (\S+)( .*)?$/) 67 | lines = text.split("\n") 68 | bottom = lines[0..-1].join("\n").gsub(/^\s{0,3}\[([^\]]+)\]: (\S+)( .*\n)?$/,'') 69 | 70 | refs.each {|ref| 71 | name = ref[0] 72 | next if @norepeatlinks.include? ref[1] 73 | while @norepeat.include? name 74 | if name =~ / ?[0-9]$/ 75 | name.next! 76 | else 77 | name = name + " 2" 78 | end 79 | end 80 | tail = ref[2].nil? ? '' : ref[2].to_s 81 | output << {'orig' => ref[0], 'title' => name, 'link' => ref[1]+tail} 82 | @norepeat.push name 83 | @norepeatlinks.push ref[1] 84 | } 85 | 86 | links.each {|url| 87 | next if @norepeatlinks.include? url[0] 88 | if url[0] =~ /^http/ 89 | domain = url[0].match(/https?:\/\/([^\/]+)/) 90 | parts = domain[1].split('.') 91 | name = case parts.length 92 | when 1 93 | parts[0] 94 | when 2 95 | parts[0] 96 | else 97 | parts[1] 98 | end 99 | elsif url[0] !~ /^[\/~]/ 100 | name = File.basename(url[0]).split('.')[0] 101 | else 102 | name = 'Unknown' 103 | end 104 | while @norepeat.include? name 105 | if name =~ / ?[0-9]$/ 106 | name.next! 107 | else 108 | name = name + " 2" 109 | end 110 | end 111 | output << {'orig' => url[0], 'title' => name, 'link' => url[0] } 112 | @norepeat.push name 113 | @norepeatlinks.push url[0] 114 | } 115 | output = output.sort {|a,b| a['title'] <=> b['title']} 116 | o = [] 117 | output.each_with_index { |x,i| 118 | o.push("[#{x['title']}]: #{x['link']}") 119 | bottom = bottom.gsub(/\((#{x['orig']}|#{x['link']})\)/,"[#{x['title']}]").gsub(/\[#{x['orig']}\]/,"[#{x['title']}]") 120 | } 121 | return bottom + "\n\n#{o.join("\n")}\n" 122 | end 123 | 124 | def generate_page(section_folder,page) 125 | 126 | Utils.update_status("Processing page: ------------------------------#{page['title']}") 127 | 128 | 129 | infile = "#{@basefolder}/content#{@settings[:debug]}/#{page['file']}.md" 130 | outfile = "#{@basefolder}/content#{@settings[:pdfext]}/#{page['file']}.md" 131 | 132 | title = page["title"] + " [" + page["file"].gsub(/_/,'').downcase + "]" 133 | text = Utils.remove_todos(Utils.load_template(infile).gsub(/<%= @title %>/,title)) 134 | 135 | # Move headers two units down the hierarchy 136 | lines = text.split("\n") 137 | first_headline = "" 138 | first_headline_level = 0 139 | line_counter = 0 140 | while first_headline.empty? && line_counter < lines.length 141 | if lines[line_counter] =~ /^#+\s/ 142 | first_headline = lines[line_counter] 143 | first_headline_level = first_headline.split(' ')[0].length 144 | end 145 | line_counter += 1 146 | end 147 | text.gsub!(/^#/,'###') if first_headline_level == 1 && first_headline =~ /^#+\s/ 148 | 149 | # Fix wiki links 150 | Utils.update_status("Fixing wiki links") 151 | text.gsub!(/\]\(\[\[(.*?)(#.*?)?\]\]\)/) {|match| 152 | if match =~ /\]\(\[\[(.*?)(#.*?)?\]\]\)/ 153 | "](#{$1}.html#{$2})" 154 | end 155 | } 156 | text.gsub!(/: \[\[(.*?)(#.*?)?\]\]/) {|match| 157 | if match =~ /: \[\[(.*?)(#.*?)?\]\]/ 158 | ": #{$1}.html#{$2}" 159 | end 160 | } 161 | 162 | # Fix image references 163 | change_counter = 0 164 | text.scan(/\[(\d+)\]:\s+(.*?)\.(jpg|png)/).each { |match| 165 | change_counter += 1 166 | text.gsub!(/\[#{match[0]}\]/,"[#{match[1].gsub(/images\//,'')}]") 167 | } 168 | Utils.update_status("-- #{change_counter} image changes") 169 | 170 | if text.scan(/\[(.*?)\](:\s+(.*)$|\(.*?\))/).length > 0 171 | # convert inline links to refs 172 | text = convert_links(text) 173 | 174 | # Fix url references 175 | 176 | change_counter = 0 177 | text.scan(/^\s*\[(\d+)\]:\s+(.*)$/).each { |match| 178 | change_counter += 1 179 | 180 | if match[1] =~ /^http/ 181 | domain = match[1].match(/https?:\/\/([^\/]+)/) 182 | parts = domain[1].split('.') 183 | name = case parts.length 184 | when 1 185 | parts[0] 186 | when 2 187 | parts[0] 188 | else 189 | parts[1] 190 | end 191 | elsif match[1] =~ /\/?([^\/]+)\/?$/ 192 | name = $1.gsub(/([^\.]+)\.[^\.]*$/,"\\1") 193 | else 194 | name = "Unknown" 195 | end 196 | 197 | while @norepeat.include? name 198 | if name =~ / ?[0-9]$/ 199 | name.next! 200 | else 201 | name = name + " 2" 202 | end 203 | end 204 | @norepeat.push(name) 205 | text.gsub!(/\[#{match[0]}\]/,"[#{name}]") 206 | } 207 | 208 | Utils.update_status("-- #{change_counter} url reference changes") 209 | else 210 | Utils.update_status("-- NO LINKS FOUND") 211 | end 212 | 213 | # Fix internal urls 214 | text.gsub!(/\](\(|: ).*?\.html(#.*?)?(\)|$)/) { |match| 215 | if match =~ /\]: (.*?)\.html(#.*)?$/ 216 | if $2.nil? 217 | "]: ##{$1.gsub(/_/,'').downcase}" 218 | else 219 | "]: #{$2}" 220 | end 221 | elsif match =~ /\]\((.*?)\.html(#.*?)?\)/ 222 | "](#{$1}.html#{$2})" 223 | end 224 | } 225 | 226 | Utils.update_status("Adding sizes to images") 227 | text.scan(/\[(.*?)\]:\s*(?!http)(.*?\.(jpg|png|gif|svg))\s*$/).each {|image_match| 228 | image_id = image_match[0] 229 | image_path = image_match[1] 230 | if image_path =~ /^[\/~]/ # absolute path 231 | image_path = File.expand_path(image_path) 232 | else 233 | image_path = File.expand_path("#{@basefolder}/content#{@settings[:debug]}/#{image_path}") 234 | end 235 | width = %x{sips -g pixelWidth "#{image_path}"|tail -n1|awk '{print $2}'}.strip 236 | height = %x{sips -g pixelHeight "#{image_path}"|tail -n1|awk '{print $2}'}.strip 237 | text.gsub!(/\[#{image_id}\]:.*?\.(jpg|png|gif|svg)/,"[#{image_id}]: #{image_match[1]} width=#{width}px height=#{height}px") 238 | } 239 | 240 | fh = File.new(outfile, "w+") 241 | fh.puts(text) 242 | fh.close 243 | end 244 | 245 | def build_help 246 | @config["Sections"].each do |section| 247 | folder = section["folder"] 248 | section["pages"].each do |page| 249 | generate_page(folder,page) 250 | end 251 | end 252 | Utils.update_status("Copying images to content#{@settings[:pdfext]}/images") 253 | FileUtils.copy_entry("#{@basefolder}/content#{@settings[:debug]}/images","#{@basefolder}/content#{@settings[:pdfext]}/images") 254 | FileUtils.copy_entry("#{@basefolder}/marked_icon_lg.png","#{@basefolder}/content#{@settings[:pdfext]}/images/marked_icon_lg.png") 255 | end 256 | 257 | def file_list 258 | list = {} 259 | # # debugging 260 | # return {'Exporting' => "Exporting"} 261 | @config["Sections"].each do |section| 262 | folder = section['folder'] 263 | section['pages'].each do |page| 264 | list[page['file']] = "#{folder}/#{page['file']}" 265 | end 266 | end 267 | list 268 | end 269 | 270 | end 271 | -------------------------------------------------------------------------------- /scripts/obsidian-callouts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'erb' 5 | 6 | module Callouts 7 | SVG = { 8 | fold: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"></path></svg>', 9 | tip: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>', 10 | info: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>', 11 | note: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>', 12 | success: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-check"><path d="M20 6 9 17l-5-5"></path></svg>', 13 | question: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><path d="M12 17h.01"></path></svg>', 14 | todo: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-check-circle-2"><circle cx="12" cy="12" r="10"></circle><path d="m9 12 2 2 4-4"></path></svg>', 15 | abstract: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-clipboard-list"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>', 16 | warning: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-alert-triangle"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>', 17 | failure: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-x"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>', 18 | danger: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>', 19 | bug: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-bug"><path d="m8 2 1.88 1.88"></path><path d="M14.12 3.88 16 2"></path><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"></path><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"></path><path d="M12 20v-9"></path><path d="M6.53 9C4.6 8.8 3 7.1 3 5"></path><path d="M6 13H2"></path><path d="M3 21c0-2.1 1.7-3.9 3.8-4"></path><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"></path><path d="M22 13h-4"></path><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"></path></svg>', 20 | example: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>', 21 | quote: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-quote"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>' 22 | } 23 | 24 | FOLDABLE_HTML = <<~ENDFOLDHTML 25 | <!-- foldable, content --> 26 | <div data-callout="<%= @type %>" class="callout is-collapsible"> 27 | <div class="callout-title"> 28 | <div class="callout-icon"><%= @svg %></div> 29 | <div class="callout-title-inner"><%= @title %></div> 30 | <div class="callout-fold"><%= SVG[:fold] %></div> 31 | </div> 32 | <div class="callout-content" style=""> 33 | <p><%= @content %></p> 34 | </div> 35 | </div> 36 | ENDFOLDHTML 37 | 38 | CONTENT_HTML = <<~ENDCONTENTHTML 39 | <!-- content --> 40 | <div data-callout="<%= @type %>" class="callout"> 41 | <div class="callout-title"> 42 | <div class="callout-icon"><%= @svg %></div> 43 | <div class="callout-title-inner"><%= @title %></div> 44 | </div> 45 | <div class="callout-content"> 46 | <p><%= @content %></p> 47 | </div> 48 | </div> 49 | ENDCONTENTHTML 50 | 51 | TITLE_HTML = <<~ENDTITLEHTML 52 | <!-- title only --> 53 | <div data-callout="<%= @type %>" class="callout"> 54 | <div class="callout-title"> 55 | <div class="callout-icon"><%= @svg %></div> 56 | <div class="callout-title-inner"><%= @title %></div> 57 | </div> 58 | </div> 59 | ENDTITLEHTML 60 | end 61 | 62 | include Callouts 63 | 64 | ## 65 | ## Process callouts in input 66 | ## 67 | ## @param input [String] The input 68 | ## 69 | ## @return [String] Processed callouts 70 | ## 71 | def process(input) 72 | callout_rx = /(?mix) 73 | ^(?<indent>(?:>\s*)+)\[! 74 | (?<type>note|abstract|summary|tldr|info|todo|tip|hint|important|success| 75 | check|done|question|help|faq|warning|caution|attention|failure|fail| 76 | missing|danger|error|bug|example|quote|cite|) 77 | \](?<foldable>-?)(\s+(?<title>[\s\S]*?))?\s*(\n|\Z) 78 | (?<content>(>\s[\s\S]*?(?:\n|\Z))*)/ 79 | content = input.force_encoding('utf-8') 80 | match_datas = content.to_enum(:scan, callout_rx).map { Regexp.last_match } 81 | match_datas.each do |m| 82 | orig = m[0] 83 | res = template_input(m) 84 | content.sub!(/#{Regexp.escape(orig)}/, res) 85 | end 86 | 87 | content 88 | end 89 | 90 | def template_input(m) 91 | title = m['title'] || m['type'].capitalize 92 | 93 | content = m['content'] ? process(m['content']) : m['content'] 94 | 95 | @type = case m['type'].downcase 96 | when /(abstract|summary|tldr)/ 97 | 'abstract' 98 | when /info/ 99 | 'info' 100 | when /todo/ 101 | 'todo' 102 | when /(tip|hint|important)/ 103 | 'tip' 104 | when /(success|check|done)/ 105 | 'success' 106 | when /(question|help|faq)/ 107 | 'question' 108 | when /(warning|caution|attention)/ 109 | 'warning' 110 | when /(failure|fail|missing)/ 111 | 'failure' 112 | when /(danger|error)/ 113 | 'danger' 114 | when /bug/ 115 | 'bug' 116 | when /example/ 117 | 'example' 118 | when /(quote|cite)/ 119 | 'quote' 120 | else 121 | 'note' 122 | end 123 | 124 | content.gsub!(/^ *>(.*?)(\n *>(.*?))+$/) do |mtch| 125 | mtch.split(/\n/).join("\n<br>\n") 126 | end 127 | content.gsub!(/^\s*> +/, '') 128 | 129 | @svg = SVG[@type.to_sym] 130 | 131 | @content = content 132 | @title = title 133 | 134 | template = if m['foldable'] == '-' 135 | ERB.new(FOLDABLE_HTML.dup) 136 | elsif m['content'] && !m['content'].empty? 137 | ERB.new(CONTENT_HTML.dup) 138 | else 139 | ERB.new(TITLE_HTML.dup) 140 | end 141 | template.result 142 | end 143 | 144 | puts process($stdin.read) 145 | -------------------------------------------------------------------------------- /css/callouts.css: -------------------------------------------------------------------------------- 1 | body { 2 | --bold-weight: 600; 3 | --bold-color: inherit; 4 | 5 | /* Relative font sizes */ 6 | --font-smallest: 0.8em; 7 | --font-smaller: 0.875em; 8 | --font-small: 0.933em; 9 | /* UI font sizes */ 10 | --font-ui-smaller: 12px; 11 | --font-ui-small: 13px; 12 | --font-ui-medium: 15px; 13 | --font-ui-large: 20px; 14 | /* Font weights */ 15 | --font-thin: 100; 16 | --font-extralight: 200; 17 | --font-light: 300; 18 | --font-normal: 400; 19 | --font-medium: 500; 20 | --font-semibold: 600; 21 | --font-bold: 700; 22 | --font-extrabold: 800; 23 | --font-black: 900; 24 | } 25 | 26 | body { 27 | color-scheme: light; 28 | --highlight-mix-blend-mode: darken; 29 | --mono-rgb-0: 255, 255, 255; 30 | --mono-rgb-100: 0, 0, 0; 31 | --color-red-rgb: 233, 49, 71; 32 | --color-red: #e93147; 33 | --color-orange-rgb: 236, 117, 0; 34 | --color-orange: #ec7500; 35 | --color-yellow-rgb: 224, 172, 0; 36 | --color-yellow: #e0ac00; 37 | --color-green-rgb: 8, 185, 78; 38 | --color-green: #08b94e; 39 | --color-cyan-rgb: 0, 191, 188; 40 | --color-cyan: #00bfbc; 41 | --color-blue-rgb: 8, 109, 221; 42 | --color-blue: #086ddd; 43 | --color-purple-rgb: 120, 82, 238; 44 | --color-purple: #7852ee; 45 | --color-pink-rgb: 213, 57, 132; 46 | --color-pink: #d53984; 47 | --color-base-00: #ffffff; 48 | --color-base-05: #fcfcfc; 49 | --color-base-10: #fafafa; 50 | --color-base-20: #f6f6f6; 51 | --color-base-25: #e3e3e3; 52 | --color-base-30: #e0e0e0; 53 | --color-base-35: #d4d4d4; 54 | --color-base-40: #bdbdbd; 55 | --color-base-50: #ababab; 56 | --color-base-60: #707070; 57 | --color-base-70: #5c5c5c; 58 | --color-base-100: #222222; 59 | --color-accent-hsl: var(--accent-h), 60 | var(--accent-s), 61 | var(--accent-l); 62 | --color-accent: hsl(var(--accent-h), var(--accent-s), var(--accent-l)); 63 | --color-accent-1: hsl(calc(var(--accent-h) - 1), calc(var(--accent-s) * 1.01), calc(var(--accent-l) * 1.075)); 64 | --color-accent-2: hsl(calc(var(--accent-h) - 3), calc(var(--accent-s) * 1.02), calc(var(--accent-l) * 1.15)); 65 | --background-secondary-alt: var(--color-base-05); 66 | --background-modifier-box-shadow: rgba(0, 0, 0, 0.1); 67 | --background-modifier-cover: rgba(220, 220, 220, 0.4); 68 | --input-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12), 69 | 0 2px 3px 0 rgba(0,0,0,.05), 70 | 0 1px 1.5px 0 rgba(0,0,0,.03), 71 | 0 1px 2px 0 rgba(0,0,0,.04), 72 | 0 0 0 0 transparent; 73 | --input-shadow-hover: inset 0 0 0 1px rgba(0, 0, 0, 0.17), 74 | 0 2px 3px 0 rgba(0,0,0,.1), 75 | 0 1px 1.5px 0 rgba(0,0,0,.03), 76 | 0 1px 2px 0 rgba(0,0,0,.04), 77 | 0 0 0 0 transparent; 78 | --shadow-s: 0px 1px 2px rgba(0, 0, 0, 0.028), 79 | 0px 3.4px 6.7px rgba(0, 0, 0, .042), 80 | 0px 15px 30px rgba(0, 0, 0, .07); 81 | --shadow-l: 0px 1.8px 7.3px rgba(0, 0, 0, 0.071), 82 | 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), 83 | 0px 30px 90px rgba(0, 0, 0, 0.2); 84 | } 85 | 86 | .inverted { 87 | color-scheme: dark; 88 | --highlight-mix-blend-mode: lighten; 89 | --mono-rgb-0: 0, 0, 0; 90 | --mono-rgb-100: 255, 255, 255; 91 | --color-red-rgb: 251, 70, 76; 92 | --color-red: #fb464c; 93 | --color-orange-rgb: 233, 151, 63; 94 | --color-orange: #e9973f; 95 | --color-yellow-rgb: 224, 222, 113; 96 | --color-yellow: #e0de71; 97 | --color-green-rgb: 68, 207, 110; 98 | --color-green: #44cf6e; 99 | --color-cyan-rgb: 83, 223, 221; 100 | --color-cyan: #53dfdd; 101 | --color-blue-rgb: 2, 122, 255; 102 | --color-blue: #027aff; 103 | --color-purple-rgb: 168, 130, 255; 104 | --color-purple: #a882ff; 105 | --color-pink-rgb: 250, 153, 205; 106 | --color-pink: #fa99cd; 107 | --color-base-00: #1e1e1e; 108 | --color-base-05: #212121; 109 | --color-base-10: #242424; 110 | --color-base-20: #262626; 111 | --color-base-25: #2a2a2a; 112 | --color-base-30: #363636; 113 | --color-base-35: #3f3f3f; 114 | --color-base-40: #555555; 115 | --color-base-50: #666666; 116 | --color-base-60: #999999; 117 | --color-base-70: #b3b3b3; 118 | --color-base-100: #dadada; 119 | --color-accent-hsl: var(--accent-h), 120 | var(--accent-s), 121 | var(--accent-l); 122 | --color-accent: hsl(var(--accent-h), var(--accent-s), var(--accent-l)); 123 | --color-accent-1: hsl(calc(var(--accent-h) - 3), calc(var(--accent-s) * 1.02), calc(var(--accent-l) * 1.15)); 124 | --color-accent-2: hsl(calc(var(--accent-h) - 5), calc(var(--accent-s) * 1.05), calc(var(--accent-l) * 1.29)); 125 | --background-modifier-form-field: var(--color-base-25); 126 | --background-secondary-alt: var(--color-base-30); 127 | --interactive-normal: var(--color-base-30); 128 | --interactive-hover: var(--color-base-35); 129 | --text-accent: var(--color-accent-1); 130 | --interactive-accent: var(--color-accent); 131 | --interactive-accent-hover: var(--color-accent-1); 132 | --background-modifier-box-shadow: rgba(0, 0, 0, 0.3); 133 | --background-modifier-cover: rgba(10, 10, 10, 0.4); 134 | --text-selection: hsla(var(--interactive-accent-hsl), 0.25); 135 | --input-shadow: inset 0 0.5px 0.5px 0.5px rgba(255, 255, 255, 0.09), 136 | 0 2px 4px 0 rgba(0,0,0,.15), 137 | 0 1px 1.5px 0 rgba(0,0,0,.1), 138 | 0 1px 2px 0 rgba(0,0,0,.2), 139 | 0 0 0 0 transparent; 140 | --input-shadow-hover: inset 0 0.5px 1px 0.5px rgba(255, 255, 255, 0.16), 141 | 0 2px 3px 0 rgba(0,0,0,.3), 142 | 0 1px 1.5px 0 rgba(0,0,0,.2), 143 | 0 1px 2px 0 rgba(0,0,0,.4), 144 | 0 0 0 0 transparent; 145 | --shadow-s: 0px 1px 2px rgba(0, 0, 0, 0.121), 146 | 0px 3.4px 6.7px rgba(0, 0, 0, 0.179), 147 | 0px 15px 30px rgba(0, 0, 0, 0.3); 148 | --shadow-l: 0px 1.8px 7.3px rgba(0, 0, 0, 0.071), 149 | 0px 6.3px 24.7px rgba(0, 0, 0, 0.112), 150 | 0px 30px 90px rgba(0, 0, 0, 0.2); 151 | --pdf-shadow: 0 0 0 1px var(--background-modifier-border); 152 | --pdf-thumbnail-shadow: 0 0 0 1px var(--background-modifier-border); 153 | } 154 | 155 | body { 156 | /* Layout sizing - for padding and margins */ 157 | --size-2-1: 2px; 158 | --size-2-2: 4px; 159 | --size-2-3: 6px; 160 | --size-4-1: 4px; 161 | --size-4-2: 8px; 162 | --size-4-3: 12px; 163 | --size-4-4: 16px; 164 | --size-4-5: 20px; 165 | --size-4-6: 24px; 166 | --size-4-8: 32px; 167 | --size-4-9: 36px; 168 | --size-4-10: 40px; 169 | --size-4-12: 48px; 170 | --size-4-16: 64px; 171 | --size-4-18: 72px; 172 | /* Radiuses */ 173 | --radius-s: 4px; 174 | --radius-m: 8px; 175 | --radius-l: 12px; 176 | --radius-xl: 16px; 177 | /* Callouts */ 178 | --callout-border-width: 0px; 179 | --callout-border-opacity: 0.25; 180 | --callout-padding: var(--size-4-3) var(--size-4-3) var(--size-4-3) var(--size-4-6); 181 | --callout-radius: var(--radius-s); 182 | --callout-blend-mode: var(--highlight-mix-blend-mode); 183 | --callout-title-color: inherit; 184 | --callout-title-padding: 0; 185 | --callout-title-size: inherit; 186 | --callout-content-padding: 0; 187 | --callout-content-background: transparent; 188 | --callout-bug: var(--color-red-rgb); 189 | --callout-default: var(--color-blue-rgb); 190 | --callout-error: var(--color-red-rgb); 191 | --callout-example: var(--color-purple-rgb); 192 | --callout-fail: var(--color-red-rgb); 193 | --callout-important: var(--color-cyan-rgb); 194 | --callout-info: var(--color-blue-rgb); 195 | --callout-question: var(--color-orange-rgb); 196 | --callout-success: var(--color-green-rgb); 197 | --callout-summary: var(--color-cyan-rgb); 198 | --callout-tip: var(--color-cyan-rgb); 199 | --callout-todo: var(--color-blue-rgb); 200 | --callout-warning: var(--color-orange-rgb); 201 | --callout-quote: 158, 158, 158; 202 | } 203 | 204 | .callout { 205 | --callout-color: var(--callout-default); 206 | --callout-icon: lucide-pencil; 207 | } 208 | .callout[data-callout="abstract"], 209 | .callout[data-callout="summary"], 210 | .callout[data-callout="tldr"] { 211 | --callout-color: var(--callout-summary); 212 | --callout-icon: lucide-clipboard-list; 213 | } 214 | .callout[data-callout="info"] { 215 | --callout-color: var(--callout-info); 216 | --callout-icon: lucide-info; 217 | } 218 | .callout[data-callout="todo"] { 219 | --callout-color: var(--callout-todo); 220 | --callout-icon: lucide-check-circle-2; 221 | } 222 | .callout[data-callout="important"] { 223 | --callout-color: var(--callout-important); 224 | --callout-icon: lucide-flame; 225 | } 226 | .callout[data-callout="tip"], 227 | .callout[data-callout="hint"] { 228 | --callout-color: var(--callout-tip); 229 | --callout-icon: lucide-flame; 230 | } 231 | .callout[data-callout="success"], 232 | .callout[data-callout="check"], 233 | .callout[data-callout="done"] { 234 | --callout-color: var(--callout-success); 235 | --callout-icon: lucide-check; 236 | } 237 | .callout[data-callout="question"], 238 | .callout[data-callout="help"], 239 | .callout[data-callout="faq"] { 240 | --callout-color: var(--callout-question); 241 | --callout-icon: help-circle; 242 | } 243 | .callout[data-callout="warning"], 244 | .callout[data-callout="caution"], 245 | .callout[data-callout="attention"] { 246 | --callout-color: var(--callout-warning); 247 | --callout-icon: lucide-alert-triangle; 248 | } 249 | .callout[data-callout="failure"], 250 | .callout[data-callout="fail"], 251 | .callout[data-callout="missing"] { 252 | --callout-color: var(--callout-fail); 253 | --callout-icon: lucide-x; 254 | } 255 | .callout[data-callout="danger"], 256 | .callout[data-callout="error"] { 257 | --callout-color: var(--callout-error); 258 | --callout-icon: lucide-zap; 259 | } 260 | .callout[data-callout="bug"] { 261 | --callout-color: var(--callout-bug); 262 | --callout-icon: lucide-bug; 263 | } 264 | .callout[data-callout="example"] { 265 | --callout-color: var(--callout-example); 266 | --callout-icon: lucide-list; 267 | } 268 | .callout[data-callout="quote"], 269 | .callout[data-callout="cite"] { 270 | --callout-color: var(--callout-quote); 271 | --callout-icon: quote-glyph; 272 | } 273 | .callout { 274 | overflow: hidden; 275 | border-style: solid; 276 | border-color: rgba(var(--callout-color), var(--callout-border-opacity)); 277 | border-width: var(--callout-border-width); 278 | border-radius: var(--callout-radius); 279 | margin: 1em 0; 280 | mix-blend-mode: var(--callout-blend-mode); 281 | background-color: rgba(var(--callout-color), 0.1); 282 | padding: var(--callout-padding); 283 | } 284 | .callout.is-collapsible .callout-title { 285 | cursor: var(--cursor); 286 | } 287 | .callout-title { 288 | padding: var(--callout-title-padding); 289 | display: flex; 290 | gap: var(--size-4-1); 291 | font-size: 16px; 292 | color: rgb(var(--callout-color)); 293 | line-height: var(--line-height-tight); 294 | align-items: flex-start; 295 | } 296 | .callout-content { 297 | overflow-x: auto; 298 | padding: var(--callout-content-padding); 299 | background-color: var(--callout-content-background); 300 | } 301 | .callout-icon { 302 | flex: 0 0 auto; 303 | display: flex; 304 | align-items: center; 305 | } 306 | .callout-icon .svg-icon { 307 | color: rgb(var(--callout-color)); 308 | } 309 | .callout-fold .svg-icon { 310 | color: rgb(var(--callout-color)); 311 | } 312 | .callout-icon svg { 313 | height: 1em; 314 | width: 1em; 315 | } 316 | .callout-icon::after { 317 | content: "\200B"; 318 | } 319 | .callout-title-inner { 320 | font-weight: var(--bold-weight); 321 | color: var(--callout-title-color); 322 | } 323 | .callout-fold { 324 | display: flex; 325 | align-items: center; 326 | padding-right: var(--size-4-2); 327 | } 328 | .callout-fold::after { 329 | content: "\200B"; 330 | } 331 | .callout-fold .svg-icon { 332 | transition: transform 100ms ease-in-out; 333 | } 334 | .callout-fold.is-collapsed .svg-icon { 335 | transform: rotate(-90deg); 336 | } 337 | -------------------------------------------------------------------------------- /scripts/lib/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # TODO: Generate TOC automatically if there are more than 3 h2s 4 | 5 | # String helpers 6 | class String 7 | def sanitized 8 | filename = dup 9 | filename.strip! 10 | filename.gsub!(/ +/, '_') 11 | filename.gsub!(%r{^.*(\\|/)}, '') 12 | filename.gsub!(/[^0-9A-Za-z.\-_]/, '') 13 | filename 14 | end 15 | 16 | def symbolify_symbols 17 | content = dup 18 | content.gsub!(/←/, '←') 19 | content.gsub!(/↑/, '↑') 20 | content.gsub!(/→/, '→') 21 | content.gsub!(/↓/, '↓') 22 | content.gsub!(/↖/, '↖') 23 | content.gsub!(/↘/, '↘') 24 | content.gsub!(/⇞/, '⇞') 25 | content.gsub!(/⇟/, '⇟') 26 | content.gsub!(/⇥/, '⇥') 27 | content.gsub!(/⇧/, '⇧') 28 | content.gsub!(/⌃/, '⌃') 29 | content.gsub!(/⌘/, '⌘') 30 | content.gsub!(/⌤/, '⌅') 31 | content.gsub!(/⌥/, '⌥') 32 | content.gsub!(/⌦/, '⌦') 33 | content.gsub!(/⌫/, '⌫') 34 | content.gsub!(/⎋/, '⎋') 35 | content.gsub!(/⏎/, '↩') 36 | content 37 | end 38 | 39 | def symbolify_symbols! 40 | replace symbolify_symbols 41 | end 42 | 43 | def textify_symbols 44 | content = dup 45 | content.gsub!(/(⌘|⌘)/, 'Cmd-') 46 | content.gsub!(/(⌥|⌥)/, 'Opt-') 47 | content.gsub!(/(⌃|⌃)/, 'Ctrl-') 48 | content.gsub!(/(⇧|⇧)/, 'Shift-') 49 | content.gsub!(/(⎋|⎋)/, 'Esc') 50 | content.gsub!(/(↑|↑)/, 'Up Arrow') 51 | content.gsub!(/(↓|↓)/, 'Down Arrow') 52 | content.gsub!(/(←|←)/, 'Left Arrow') 53 | content.gsub!(/(→|→)/, 'Right Arrow') 54 | content.gsub!(/▸/, '>') 55 | content 56 | end 57 | 58 | def textify_symbols! 59 | replace textify_symbols 60 | end 61 | 62 | def replace_with_entity 63 | case strip.downcase 64 | when /^apple$/ 65 | '' 66 | when /^(comm(and)?|cmd|clover)$/ 67 | '⌘' 68 | when /^(cont(rol)?|ctl|ctrl)$/ 69 | '⌃' 70 | when /^(opt(ion)?|alt)$/ 71 | '⌥' 72 | when /^shift$/ 73 | '⇧' 74 | when /^tab$/ 75 | '⇥' 76 | when /^caps(lock)?$/ 77 | '⇪' 78 | when /^eject$/ 79 | '⏏' 80 | when /^return$/ 81 | '⏎' 82 | when /^enter$/ 83 | '⌤' 84 | when /^(del(ete)?|back(space)?)$/ 85 | '⌫' 86 | when /^fwddel(ete)?$/ 87 | '⌦' 88 | when /^(esc(ape)?)$/ 89 | '⎋' 90 | when /^r(ight)?$/ 91 | '→' 92 | when /^l(eft)?$/ 93 | '←' 94 | when /^up?$/ 95 | '↑' 96 | when /^d(own)?$/ 97 | '↓' 98 | when /^pgup$/ 99 | '⇞' 100 | when /^pgdn$/ 101 | '⇟' 102 | when /^home$/ 103 | '↖' 104 | when /^end$/ 105 | '↘' 106 | when /^clear$/ 107 | '⌧' 108 | when /^gear$/ 109 | '⚙' 110 | else 111 | "{{#{self}}}" 112 | end 113 | end 114 | 115 | def modifier? 116 | self =~ /\{\{(comm(and)?|cmd|clover|shift|cont(rol)?|ctl|ctrl|opt(ion)?|alt)\}\}/i 117 | end 118 | 119 | # separate a key combination into separate kbd tags 120 | def format_kbd 121 | if self =~ /(\{\{[a-z]+\}\})+[A-Z0-9[:punct:]=]/i # modifier combo 122 | keys = scan(/(\{\{[a-z]+\}\}|[a-z0-9[:punct:]=])/i) 123 | keys.map!.with_index do |key, i| 124 | if key[0] =~ %r{^[/-]$} && i < (keys.length - 1) 125 | key[0] 126 | elsif key[0].modifier? 127 | %(<kbd class="modifierkey">#{key[0]}</kbd>) 128 | else 129 | %(<kbd>#{key[0].upcase}</kbd>) 130 | end 131 | end 132 | %(<span class="keycombo">#{keys.join('')}</span>) 133 | else 134 | classes = 'single' 135 | # classes += " modifierkey" if self.modifier? 136 | %(<kbd class="#{classes}">#{self}</kbd>) 137 | end 138 | end 139 | 140 | def format_kbd_pdf 141 | if self =~ /(\{\{[a-z]+\}\})+[A-Z0-9[:punct:]=]/i # modifier combo 142 | keys = scan(/(\{\{[a-z]+\}\}|[a-z0-9[:punct:]=])/i) 143 | keys.map!.with_index do |key, i| 144 | if key[0] =~ %r{^[/-]$} && i < (keys.length - 1) 145 | key[0] 146 | elsif key[0].modifier? 147 | key[0] 148 | else 149 | key[0].upcase 150 | end 151 | end 152 | %(`#{keys.join('')}`) 153 | else 154 | %(`#{self}`) 155 | end 156 | end 157 | 158 | def table_of_contents(opts = {}) 159 | opts[:force] ||= false 160 | opts[:level] ||= 2 161 | 162 | headers = scan(/^(\#{#{opts[:level]}}(?!#))\s*(.*?)(\s*#+)?$/) 163 | return unless headers.length > 3 || opts[:force] 164 | 165 | output = [] 166 | 167 | min = if opts[:level] =~ /(\d),(\d)/ 168 | Regexp.last_match(1).to_i 169 | else 170 | opts[:level].to_i 171 | end 172 | 173 | headers.each do |h| 174 | title = h[1] 175 | id = '' 176 | 177 | hlevel = h[0].length - min 178 | 179 | title.gsub!(/#+/, '') 180 | title.strip! 181 | if title =~ /\[(.*?)\]$/ 182 | id = Regexp.last_match(1).strip 183 | title.sub!(/\s*\[.*?\]$/, '') 184 | else 185 | id = title.gsub(/[^a-z0-9\-.]/i, '').downcase 186 | end 187 | output.push(%(#{"\t" * hlevel}* [#{title}](##{id}))) 188 | end 189 | %(\n<nav id="sectiontoc" aria-label="Page contents" class="uk-width-full">\n\n#{output.join("\n")}\n\n</nav>\n\n) 190 | end 191 | 192 | def render_liquid(pdf: false) 193 | if pdf.to_s == 'marked' 194 | marked = true 195 | pdf = false 196 | end 197 | out = dup 198 | # Remove CriticMarkup Comments 199 | out.gsub!(/\{>>.*?<<\}/m, '') 200 | 201 | # Replace {% block [params] %}content{% endblock %} 202 | out.gsub!(/(?mi)\{%\s*(\S+)\s*(.*?)\s*%\}(.*?)\{%\s*end\1\s*%\}/m) do 203 | m = Regexp.last_match 204 | directive = m[1].strip 205 | params = m[2] 206 | content = m[3] 207 | 208 | output = '' 209 | 210 | # {% apponly p %}A paragraph that will show up only in the in-app 211 | # browser{% endapponly %} 212 | if directive =~ /(apponly|browseronly|class)/i 213 | padding = '' 214 | if directive == 'class' 215 | tag = 'span' 216 | classes = params.strip 217 | else 218 | tag = params.length.positive? ? params.strip : 'span' 219 | classes = directive 220 | padding = "\n\n" if tag =~ /div/ 221 | end 222 | output = %(<#{tag} class="#{classes}">#{padding}#{content}#{padding}</#{tag}>) 223 | # {% notes %}some notes about this section{% endnotes %} 224 | elsif directive =~ /(notes?|comment|todo|fixme)/ 225 | output = '' 226 | else 227 | output = content 228 | end 229 | 230 | output 231 | end 232 | 233 | # Replace single {% tag [params] %} directives 234 | out.gsub!(/\{% *(\S+) +(.*?) *%\}/) do 235 | m = Regexp.last_match 236 | directive = m[1] 237 | args = m[2].strip 238 | # {% prefspane General %} 239 | if directive =~ /prefs?pane/i && (pdf || marked) 240 | %(<span class="appmenu">**Preferences**▸[**#{args}**](preferences-#{args.downcase.gsub(/ /, '-')}.html) pane</span>) 241 | elsif directive =~ /prefs?pane/i && !pdf && !marked 242 | %(<span class="appmenu">**Preferences**▸[**#{args}**](#prefs#{args.downcase.gsub(/ /, '-')}) pane</span>) 243 | # {% kbd {{cmd}}S %} 244 | elsif directive =~ /kbd/i 245 | pdf ? args.format_kbd_pdf : args.format_kbd 246 | # {% appmenu File,Save ({{cmd}}S) %} 247 | elsif directive =~ /(app)?menu/ 248 | kbd = '' 249 | if args =~ /\s+\((.*?)\)$/ 250 | m = Regexp.last_match 251 | kbd = pdf ? " (#{m[1].format_kbd_pdf})" : " (#{m[1].format_kbd})" 252 | args.sub!(/\s+\((.*?)\)$/, '') 253 | end 254 | 255 | segs = args.split(/,/) 256 | toplevel = segs.slice!(0) 257 | 258 | res = %(<span class="appmenu">**#{toplevel}**) 259 | if !pdf 260 | segs.each do |seg| 261 | res += %(▸#{seg.strip}) 262 | end 263 | else 264 | segs.each do |seg| 265 | res += %( *#{seg.strip}*) 266 | end 267 | end 268 | res + "</span>#{kbd}" 269 | 270 | elsif directive =~ /(notes?|comment|todo|fixme)/ 271 | '' 272 | else 273 | m[0] 274 | end 275 | end 276 | 277 | if !pdf && !marked 278 | out.gsub!(/\{\{ *toc(?: [\d,]+)? *\}\}/i, '') 279 | elsif out =~ /\{\{ *toc( [1-6](,[2-6])?)? *\}\}/i 280 | out.gsub!(/\{\{ *toc(?: ([1-6](,[2-6])?))? *\}\}/i) do 281 | m = Regexp.last_match 282 | lvl = m[1] || 2 283 | out.table_of_contents({ force: true, level: lvl }) 284 | end 285 | else 286 | toc = out.table_of_contents 287 | out.sub!(/^## /, "#{toc}\n\n## ") unless toc.nil? 288 | end 289 | 290 | # Replace {{insertions}} 291 | out.gsub!(/\{\{(.*?)\}\}/) do 292 | Regexp.last_match(1).strip.replace_with_entity 293 | end 294 | out 295 | end 296 | end 297 | 298 | if __FILE__ == $PROGRAM_NAME 299 | input = DATA.read 300 | puts input.render_liquid 301 | end 302 | 303 | __END__ 304 | # <%= @title %> 305 | 306 | {>>A critic comment BT - 2019-05-19<<} 307 | 308 | {>>A multiline 309 | 310 | critic comment BT - 2019-05-19<<} 311 | 312 | A little bit of intro text. 313 | 314 | ## Section one 315 | 316 | Choose "Validate all links" (shortcut {% kbd {{ctrl}}{{cmd}}L %}) from the Gear menu or the right click menu. All remote links in the document will be checked, and the results are displayed in a popup. Clicking a link in the popup will scroll to and highlight its respective link in the document. 317 | 318 | {% todo test stuff %} 319 | 320 | {% fixme %} 321 | - some other stuff 322 | - I need to do 323 | - when I get a chance 324 | {% endfixme %} 325 | 326 | You can quickly re-open the last file you were viewing with {%kbd {{shift}}{{cmd}}R%}. There are a lot of other keyboard shortcuts, too. If you care to learn them, you can find a chart by clicking the Special Features link in the sidebar. 327 | 328 | Save HTML with {% appmenu File,Export,Save HTML %} 329 | 330 | ## Section two [secttwo] 331 | 332 | {% apponly div %} 333 | *A paragraph to appear only [when run](https://brettterpstra.com) native.* 334 | {% endapponly %} 335 | 336 | {% browseronly %}A paragraph to appear only when run in a browser.{% endbrowseronly %} 337 | 338 | ### Subsection Uno ### [subsecuno] 339 | 340 | {% note %} 341 | The above should only have rendered as a span. 342 | {% endnote %} 343 | 344 | A paragraph containing {% apponly b %}a section for app only{% endapponly %}{% browseronly strong %}a section for browser only.{% endbrowseronly %} 345 | 346 | ### Subsection dos ### 347 | 348 | {% class class1 class2 %}Valid urls may be hidden from the popup with the "Hide Valid" button at the top of it. This will show only urls that have returned an error status.{% endclass %} 349 | 350 | #### Let's go real deep here 351 | 352 | Pressing {% class class1 class2 %}Escape{%endclass%} will hide the validation results. They can be revealed again using {% kbd {{ctrl}}{{cmd}}L %} or the Gear menu. 353 | 354 | ## Validating Notes 355 | 356 | Validating automatically {%note this should be removed completely %} 357 | 358 | Turn on "Automatically validate URLs on update" in the Preview preferences (or at the bottom of the link validation popup). When the document loads, contained links will be tested in background. A dialog will only show if there are errors. 359 | 360 | To disable the popup, turn it off in preferences, or uncheck the box at the bottom of the popup window. 361 | 362 | ## Validating Notes 2 363 | 364 | Trigger autoscroll by pressing {% kbd s %}. This will begin scrolling forward through the document at the default speed. 365 | 366 | An indicator at the bottom left will show you the current speed. The speed can be adjusted with the up and down arrows, and {% kbd {{shift}}{{up}}/{{down}} %} will speed it up and slow it down in larger increments. 367 | 368 | {% kbd Space %} will pause and play as you scroll. 369 | 370 | Pressing {% kbd S %} (Shift-s) while scrolling will reverse the scroll direction. 371 | 372 | hold down Option-Command ({% kbd {{opt}}{{cmd}} %}) to open 373 | -------------------------------------------------------------------------------- /scripts/obsidian-md-filter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | require 'singleton' 3 | require 'yaml' 4 | require 'erb' 5 | 6 | Encoding.default_internal = Encoding::UTF_8 7 | Encoding.default_external = Encoding::UTF_8 8 | 9 | class ::String 10 | def slugify 11 | if Vault.instance.mmd? 12 | gsub(/%20/, '').gsub(/[^a-z0-9%]/i, '') 13 | else 14 | downcase.gsub(/%20/, '-').gsub(/[^a-z0-9%]/i, '-').gsub(/-+/, '-') 15 | end 16 | end 17 | end 18 | 19 | class Vault 20 | include Singleton 21 | 22 | def valid? 23 | !ENV['MARKED_PATH'].nil? && !root.nil? 24 | end 25 | 26 | def resolve(reference) 27 | Dir.chdir(root) do 28 | files = Dir.glob("**/#{reference}*") 29 | return File.join(root, files[0]) if files.size == 1 30 | 31 | nil 32 | end 33 | end 34 | 35 | def strip_emojis? 36 | settings[:strip_emojis] 37 | end 38 | 39 | def add_title? 40 | settings[:add_title] 41 | end 42 | 43 | def convert_tags? 44 | settings[:convert_tags] 45 | end 46 | 47 | def obsidian_links? 48 | settings[:obsidian_links] 49 | end 50 | 51 | def convert_markdown_links? 52 | settings[:convert_markdown_links] 53 | end 54 | 55 | def vault 56 | @vault ||= ERB::Util.url_encode(File.basename(root)) 57 | end 58 | 59 | def mmd 60 | @mmd ||= settings[:marked_processor] =~ /^(mult|mmd)/ 61 | end 62 | 63 | def mmd? 64 | mmd 65 | end 66 | 67 | private 68 | 69 | def root 70 | @root ||= resolve_root(ENV['MARKED_PATH']) 71 | end 72 | 73 | def resolve_root(path) 74 | dir = File.dirname(path) 75 | if Dir.exist?(File.join(dir, '.obsidian')) 76 | dir 77 | elsif [Dir.home, '/'].include?(dir) 78 | nil 79 | else 80 | resolve_root(dir) 81 | end 82 | end 83 | 84 | def settings 85 | @settings ||= load_settings(File.join(root, '.obsidian-md-filter')) 86 | end 87 | 88 | def load_settings(path) 89 | config = if File.exist?(path) 90 | YAML.load_file(path) 91 | else 92 | {} 93 | end 94 | { 95 | strip_emojis: config['strip_emojis'] || false, 96 | add_title: config['add_title'] || false, 97 | convert_tags: config['convert_tags'] || false, 98 | obsidian_links: config['obsidian_links'] || false, 99 | convert_markdown_links: config['convert_markdown_links'] || false, 100 | marked_processor: config['marked_processor']&.downcase || 'discount' 101 | } 102 | end 103 | end 104 | 105 | # Don't process if the file is not in an Obsidian vault. 106 | unless Vault.instance.valid? 107 | puts 'NOCUSTOM' 108 | return 109 | end 110 | 111 | def obsidian_links(line, vault) 112 | replacements = {} 113 | line.scan(/(\[\[(.*?)\]\])/) do |match| 114 | wikilink = match[0] 115 | text = match[1] 116 | link, label = text.split('|') 117 | page, anchor = link.split('#') 118 | 119 | replacements[wikilink] = if !label.nil? 120 | # [[Internal link|Alias]] -> [Alias](obsidian url) 121 | "[#{label}](obsidian://vault/#{vault}/#{ERB::Util.url_encode(link)})" 122 | elsif !anchor.nil? 123 | if page.empty? 124 | # [[#A Reference]] -> [A Reference](#a-reference) 125 | "[#{anchor}](##{anchor.slugify})" 126 | else 127 | # [[Internal link#Reference]] -> [Internal link > Reference](obsidian url to page#anchor) 128 | "[#{page} > #{anchor}](obsidian://vault/#{vault}/#{ERB::Util.url_encode("#{page}##{anchor}")})" 129 | end 130 | else 131 | page = anchor.nil? ? page : "#{page} > #{anchor}" 132 | "[#{page}](obsidian://vault/#{vault}/#{ERB::Util.url_encode(page)})" 133 | end 134 | end 135 | replacements 136 | end 137 | 138 | def strip_links(line) 139 | replacements = {} 140 | line.scan(/(\[\[(.*?)\]\])/) do |match| 141 | wikilink = match[0] 142 | text = match[1] 143 | link, label = text.split('|') 144 | page, anchor = link.split('#') 145 | replacements[wikilink] = if !label.nil? 146 | # [[Internal link|Alias]] -> Alias 147 | label 148 | elsif !anchor.nil? 149 | if page.empty? 150 | # [[#Reference]] -> Reference 151 | anchor 152 | else 153 | # [[Internal link#Reference]] -> Internal link > Reference 154 | "#{page} > #{anchor}" 155 | end 156 | else 157 | # [[Internal link]] -> Internal link 158 | page 159 | end 160 | end 161 | replacements 162 | end 163 | 164 | # Source: https://github.com/guanting112/remove_emoji 165 | EMOJI_REGEX = /[\uFE00-\uFE0F\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9-\u21AA\u231A-\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA-\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614-\u2615\u2618\u261D\u2620\u2622-\u2623\u2626\u262A\u262E-\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267B\u267E-\u267F\u2692-\u2697\u2699\u269B-\u269C\u26A0-\u26A1\u26AA-\u26AB\u26B0-\u26B1\u26BD-\u26BE\u26C4-\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3-\u26D4\u26E9-\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u2709\u270A-\u270B\u270C-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733-\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934-\u2935\u2B05-\u2B07\u2B1B-\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299\u{1F004}\u{1F0CF}\u{1F170}-\u{1F171}\u{1F17E}\u{1F17F}\u{1F18E}\u{1F191}-\u{1F19A}\u{1F1E6}-\u{1F1FF}\u{1F201}-\u{1F202}\u{1F21A}\u{1F22F}\u{1F232}-\u{1F23A}\u{1F250}-\u{1F251}\u{1F300}-\u{1F320}\u{1F321}\u{1F324}-\u{1F32C}\u{1F32D}-\u{1F32F}\u{1F330}-\u{1F335}\u{1F336}\u{1F337}-\u{1F37C}\u{1F37D}\u{1F37E}-\u{1F37F}\u{1F380}-\u{1F393}\u{1F396}-\u{1F397}\u{1F399}-\u{1F39B}\u{1F39E}-\u{1F39F}\u{1F3A0}-\u{1F3C4}\u{1F3C5}\u{1F3C6}-\u{1F3CA}\u{1F3CB}-\u{1F3CE}\u{1F3CF}-\u{1F3D3}\u{1F3D4}-\u{1F3DF}\u{1F3E0}-\u{1F3F0}\u{1F3F3}-\u{1F3F5}\u{1F3F7}\u{1F3F8}-\u{1F3FF}\u{1F400}-\u{1F43E}\u{1F43F}\u{1F440}\u{1F441}\u{1F442}-\u{1F4F7}\u{1F4F8}\u{1F4F9}-\u{1F4FC}\u{1F4FD}\u{1F4FF}\u{1F500}-\u{1F53D}\u{1F549}-\u{1F54A}\u{1F54B}-\u{1F54E}\u{1F550}-\u{1F567}\u{1F56F}-\u{1F570}\u{1F573}-\u{1F579}\u{1F57A}\u{1F587}\u{1F58A}-\u{1F58D}\u{1F590}\u{1F595}-\u{1F596}\u{1F5A4}\u{1F5A5}\u{1F5A8}\u{1F5B1}-\u{1F5B2}\u{1F5BC}\u{1F5C2}-\u{1F5C4}\u{1F5D1}-\u{1F5D3}\u{1F5DC}-\u{1F5DE}\u{1F5E1}\u{1F5E3}\u{1F5E8}\u{1F5EF}\u{1F5F3}\u{1F5FA}\u{1F5FB}-\u{1F5FF}\u{1F600}\u{1F601}-\u{1F610}\u{1F611}\u{1F612}-\u{1F614}\u{1F615}\u{1F616}\u{1F617}\u{1F618}\u{1F619}\u{1F61A}\u{1F61B}\u{1F61C}-\u{1F61E}\u{1F61F}\u{1F620}-\u{1F625}\u{1F626}-\u{1F627}\u{1F628}-\u{1F62B}\u{1F62C}\u{1F62D}\u{1F62E}-\u{1F62F}\u{1F630}-\u{1F633}\u{1F634}\u{1F635}-\u{1F640}\u{1F641}-\u{1F642}\u{1F643}-\u{1F644}\u{1F645}-\u{1F64F}\u{1F680}-\u{1F6C5}\u{1F6CB}-\u{1F6CF}\u{1F6D0}\u{1F6D1}-\u{1F6D2}\u{1F6E0}-\u{1F6E5}\u{1F6E9}\u{1F6EB}-\u{1F6EC}\u{1F6F0}\u{1F6F3}\u{1F6F4}-\u{1F6F6}\u{1F6F7}-\u{1F6F8}\u{1F6F9}\u{1F910}-\u{1F918}\u{1F919}-\u{1F91E}\u{1F91F}\u{1F920}-\u{1F927}\u{1F928}-\u{1F92F}\u{1F930}\u{1F931}-\u{1F932}\u{1F933}-\u{1F93A}\u{1F93C}-\u{1F93E}\u{1F940}-\u{1F945}\u{1F947}-\u{1F94B}\u{1F94C}\u{1F94D}-\u{1F94F}\u{1F950}-\u{1F95E}\u{1F95F}-\u{1F96B}\u{1F96C}-\u{1F970}\u{1F973}-\u{1F976}\u{1F97A}\u{1F97C}-\u{1F97F}\u{1F980}-\u{1F984}\u{1F985}-\u{1F991}\u{1F992}-\u{1F997}\u{1F998}-\u{1F9A2}\u{1F9B0}-\u{1F9B9}\u{1F9C0}\u{1F9C1}-\u{1F9C2}\u{1F9D0}-\u{1F9E6}\u{1F9E7}-\u{1F9FF}\u23E9-\u23EC\u23F0\u23F3\u25FD-\u25FE\u267F\u2693\u26A1\u26D4\u26EA\u26F2-\u26F3\u26F5\u26FA\u{1F201}\u{1F232}-\u{1F236}\u{1F238}-\u{1F23A}\u{1F3F4}\u{1F6CC}\u{1F3FB}-\u{1F3FF}\u26F9\u{1F385}\u{1F3C2}-\u{1F3C4}\u{1F3C7}\u{1F3CA}\u{1F3CB}-\u{1F3CC}\u{1F442}-\u{1F443}\u{1F446}-\u{1F450}\u{1F466}-\u{1F469}\u{1F46E}\u{1F470}-\u{1F478}\u{1F47C}\u{1F481}-\u{1F483}\u{1F485}-\u{1F487}\u{1F4AA}\u{1F574}-\u{1F575}\u{1F645}-\u{1F647}\u{1F64B}-\u{1F64F}\u{1F6A3}\u{1F6B4}-\u{1F6B6}\u{1F6C0}\u{1F918}\u{1F919}-\u{1F91C}\u{1F91E}\u{1F926}\u{1F933}-\u{1F939}\u{1F93D}-\u{1F93E}\u{1F9B5}-\u{1F9B6}\u{1F9D1}-\u{1F9DD}\u200D\u20E3\uFE0F\u{1F9B0}-\u{1F9B3}\u{E0020}-\u{E007F}\u2388\u2600-\u2605\u2607-\u2612\u2616-\u2617\u2619\u261A-\u266F\u2670-\u2671\u2672-\u267D\u2680-\u2689\u268A-\u2691\u2692-\u269C\u269D\u269E-\u269F\u26A2-\u26B1\u26B2\u26B3-\u26BC\u26BD-\u26BF\u26C0-\u26C3\u26C4-\u26CD\u26CF-\u26E1\u26E2\u26E3\u26E4-\u26E7\u26E8-\u26FF\u2700\u2701-\u2704\u270C-\u2712\u2763-\u2767\u{1F000}-\u{1F02B}\u{1F02C}-\u{1F02F}\u{1F030}-\u{1F093}\u{1F094}-\u{1F09F}\u{1F0A0}-\u{1F0AE}\u{1F0AF}-\u{1F0B0}\u{1F0B1}-\u{1F0BE}\u{1F0BF}\u{1F0C0}\u{1F0C1}-\u{1F0CF}\u{1F0D0}\u{1F0D1}-\u{1F0DF}\u{1F0E0}-\u{1F0F5}\u{1F0F6}-\u{1F0FF}\u{1F10D}-\u{1F10F}\u{1F12F}\u{1F16C}-\u{1F16F}\u{1F1AD}-\u{1F1E5}\u{1F203}-\u{1F20F}\u{1F23C}-\u{1F23F}\u{1F249}-\u{1F24F}\u{1F252}-\u{1F25F}\u{1F260}-\u{1F265}\u{1F266}-\u{1F2FF}\u{1F321}-\u{1F32C}\u{1F394}-\u{1F39F}\u{1F3F1}-\u{1F3F7}\u{1F3F8}-\u{1F3FA}\u{1F4FD}-\u{1F4FE}\u{1F53E}-\u{1F53F}\u{1F540}-\u{1F543}\u{1F544}-\u{1F54A}\u{1F54B}-\u{1F54F}\u{1F568}-\u{1F579}\u{1F57B}-\u{1F5A3}\u{1F5A5}-\u{1F5FA}\u{1F6C6}-\u{1F6CF}\u{1F6D3}-\u{1F6D4}\u{1F6D5}-\u{1F6DF}\u{1F6E0}-\u{1F6EC}\u{1F6ED}-\u{1F6EF}\u{1F6F0}-\u{1F6F3}\u{1F6F9}-\u{1F6FF}\u{1F774}-\u{1F77F}\u{1F7D5}-\u{1F7FF}\u{1F80C}-\u{1F80F}\u{1F848}-\u{1F84F}\u{1F85A}-\u{1F85F}\u{1F888}-\u{1F88F}\u{1F8AE}-\u{1F8FF}\u{1F900}-\u{1F90B}\u{1F90C}-\u{1F90F}\u{1F93F}\u{1F96C}-\u{1F97F}\u{1F998}-\u{1F9BF}\u{1F9C1}-\u{1F9CF}\u{1F9E7}-\u{1FFFD}]/x 166 | 167 | if Vault.instance.add_title? 168 | title = File.basename(ENV['MARKED_PATH'], '.md').force_encoding('utf-8') 169 | title.gsub!(EMOJI_REGEX, '') if Vault.instance.strip_emojis? 170 | puts "# #{title}" 171 | puts 172 | end 173 | 174 | first = true 175 | front_matter = false 176 | comment = false 177 | vault = Vault.instance.vault 178 | 179 | $stdin.readlines.each do |line| 180 | # Strip out the YAML front matter, if any. 181 | if first 182 | first = false 183 | if line.strip == '---' 184 | front_matter = true 185 | next 186 | end 187 | end 188 | if front_matter 189 | front_matter = false if line.strip == '---' 190 | next 191 | end 192 | 193 | # Strip out HTML comments 194 | comment = true if line.start_with?('<!--') 195 | if comment 196 | comment = false if line.strip.end_with?('-->') 197 | next 198 | end 199 | 200 | # ^block-id on its own line -> skip completely 201 | next if line =~ /^\^[a-zA-Z0-9-]+$/ 202 | 203 | # ^block-id at the end of a line? -> strip off 204 | line.gsub!(/^(.*?)\s\^[a-zA-Z0-9-]+$/, '\1') 205 | # ![[Include This]], on a single line -> /path/to/reference 206 | if line =~ /^!\[\[(.*?)\]\]$/ 207 | path = Vault.instance.resolve(Regexp.last_match(1)) 208 | if path.nil? 209 | puts line 210 | else 211 | puts "/#{path}" 212 | end 213 | else 214 | # Remove emojis if needed 215 | line.gsub!(EMOJI_REGEX, '') if Vault.instance.strip_emojis? 216 | # Style #tags (settings['convert_tags']) 217 | if Vault.instance.convert_tags? 218 | if Vault.instance.obsidian_links? 219 | line.gsub!(/(?<=\A|\s)#([^# ]+)/, "<span class=\"mkstyledtag\"><a href=\"obsidian://search?vault=#{vault}&query=tag%3A\\1\">#\\1</a></span>") 220 | else 221 | line.gsub!(/(?<=\A|\s)#([^# ]+)/, '<span class="mkstyledtag">#\1</span>') 222 | end 223 | end 224 | # Fix all [[WikiLinks]] 225 | replacements = if Vault.instance.obsidian_links? 226 | obsidian_links(line, vault) 227 | else 228 | strip_links(line) 229 | end 230 | 231 | if Vault.instance.convert_markdown_links? 232 | # Match []() style links and convert to Obsidian urls 233 | line.scan(/\[(?<label>.*?)\]\((?<note>[^#]+)(?:\.md)?(?<anchor>#.*?)?\)/) do 234 | m = Regexp.last_match 235 | next if m['note'] =~ /^http/ 236 | 237 | wikilink = m[0] 238 | label = m['label'] 239 | link = m['note'] 240 | anchor = m['anchor'] 241 | replacements[wikilink] = "[#{label}](obsidian://vault/#{vault}/#{ERB::Util.url_encode("#{link}#{anchor}")})" 242 | end 243 | 244 | # Replace solitary anchors with appropriately-formatted anchor links based on Marked processor 245 | line.scan(/\[(?<label>.*?)\]\(#(?<anchor>.*?)\)/) do 246 | m = Regexp.last_match 247 | wikilink = m[0] 248 | label = m['label'] 249 | anchor = m['anchor'] 250 | replacements[wikilink] = "[#{label}](##{anchor.slugify})" 251 | end 252 | end 253 | 254 | replacements.each_pair { |k, v| line.gsub!(k, v) } 255 | puts line 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /scripts/jekyll-post: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -W1 2 | # frozen_string_literal: true 3 | 4 | # Version 2 (02-03-2015) 5 | # 6 | # Example custom processor for use with Marked <http://markedapp.com> and Jekyll _posts 7 | # It's geared toward my personal set of plugins and tags, but you'll get the idea. 8 | # It turns 9 | # {% img alignright /images/heythere.jpg 100 100 "Hey there" "hi" %} 10 | # into 11 | # <img src="../images/heythere.jpg" alt="Hey there" class="alignright" title="hi" /> 12 | # 13 | # replaces alignleft and alignright classes with appropriate style attribute 14 | # --- 15 | # Replaces {% gist XXXXX filename.rb %} with appropriate script tag 16 | # 17 | # Replace various other OctoPress, Jekyll and custom tags 18 | # 19 | # Processes final output with /usr/bin/kramdown (install kramdown as system gem: `sudo gem install kramdown`) 20 | # 21 | # Be sure to run *without* stripping YAML headers in Marked Behavior preferences. 22 | 23 | require 'rubygems' 24 | require 'shellwords' 25 | require 'kramdown' 26 | require 'uri' 27 | require 'cgi' 28 | require 'erb' 29 | require 'logger' 30 | require 'nokogiri/nokogiri' 31 | 32 | @logger = Logger.new(File.expand_path('~/logs/jekyllpre.log')) 33 | 34 | if ARGV[0] == 'testing' 35 | warn 'Just a test' 36 | Process.exit 0 37 | end 38 | 39 | def class_exists?(class_name) 40 | klass = Module.const_get(class_name) 41 | klass.is_a?(Class) 42 | rescue NameError 43 | false 44 | end 45 | 46 | if class_exists? 'Encoding' 47 | Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external') 48 | Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal') 49 | end 50 | 51 | begin 52 | content = $stdin.read.force_encoding('utf-8') 53 | rescue StandardError 54 | content = $stdin.read 55 | end 56 | 57 | # Title: Keyboard markup tag 58 | # Author: Brett Terpstra <https://brettterpstra.com> 59 | # Description: Apply HTML markup for keyboard shortcuts 60 | # 61 | # See Readme for syntax help and configuration details. 62 | # 63 | # Configuration options: 64 | # 65 | # kbd: 66 | # use_modifier_symbols: true 67 | # use_key_symbols: true 68 | # use_plus_sign: true 69 | # 70 | # example: 71 | # 72 | # Input: 73 | # 74 | # {% kbd ^~@r %} 75 | # 76 | # Output: 77 | # <span class="keycombo" title="Control-Option-Command-R"> 78 | # <kbd class="mod">⌃</kbd>+<kbd class="mod">⌥</kbd>+<kbd class="mod">⌘</kbd>+<kbd class="key">R</kbd> 79 | # </span> 80 | 81 | # String Helpers 82 | class ::String 83 | # Convert natural language combo to shorcut symbols 84 | # ctrl-cmd-f => ^@f 85 | def clean_combo 86 | # Only remove hyphens preced and followed by non-space character 87 | # to avoid removing hyphen from 'option-shift--' or 'command -' 88 | gsub!(/(?<=\S)-(?=\S)/, ' ') 89 | gsub!(/\b(comm(and)?|cmd|clover)\b/i, '@') 90 | gsub!(/\b(cont(rol)?|ctl|ctrl)\b/i, '^') 91 | gsub!(/\b(opt(ion)?|alt)\b/i, '~') 92 | gsub!(/\bshift\b/i, '$') 93 | gsub!(/\b(func(tion)?|fn)\b/i, '*') 94 | self 95 | end 96 | 97 | # For combos containing shift key, use upper symbol for keys with two characters 98 | # Shift-/ should be Shift-? 99 | def lower_to_upper 100 | doubles = [ 101 | [',', '<'], 102 | ['.', '>'], 103 | ['/', '?'], 104 | [';', ':'], 105 | ["'", '"'], 106 | ['[', '{'], 107 | [']', '}'], 108 | ['\\', '|'], 109 | ['-', '_'], 110 | ['=', '+'] 111 | ] 112 | 113 | lowers = [] 114 | uppers = [] 115 | doubles.each do |dbl| 116 | lowers.push(dbl[0]) 117 | uppers.push(dbl[1]) 118 | end 119 | 120 | lowers.include?(self) ? uppers[lowers.index(self)] : self 121 | end 122 | 123 | # Detect combos using upper character of double 124 | # Command-? should be Command-Shift-? 125 | def upper? 126 | uppers = %w(< > ? : " { } | ! @ # $ % ^ & * \( \) _ +) 127 | uppers.include?(self) 128 | end 129 | 130 | def clean_combo! 131 | replace clean_combo 132 | end 133 | 134 | # Convert modifier shortcut symbols to unicode 135 | def to_mod 136 | characters = { 137 | '^' => '⌃', 138 | '~' => '⌥', 139 | '$' => '⇧', 140 | '@' => '⌘', 141 | '*' => 'Fn' 142 | } 143 | characters.key?(self) ? characters[self] : self 144 | end 145 | 146 | # Convert unicode modifiers to HTML entities 147 | def mod_to_ent(use_symbol) 148 | entities = { 149 | '⌃' => '⌃', 150 | '⌥' => '⌥', 151 | '⇧' => '⇧', 152 | '⌘' => '⌘', 153 | 'Fn' => 'Fn' 154 | } 155 | names = { 156 | '⌃' => 'Control', 157 | '⌥' => 'Option', 158 | '⇧' => 'Shift', 159 | '⌘' => 'Command', 160 | 'Fn' => 'Function' 161 | } 162 | if entities.key?(self) 163 | use_symbol ? entities[self] : names[self] 164 | else 165 | self 166 | end 167 | end 168 | 169 | # Spell out modifier symbols for titles 170 | def mod_to_title 171 | entities = { 172 | '⌃' => 'Control', 173 | '⌥' => 'Option', 174 | '⇧' => 'Shift', 175 | '⌘' => 'Command', 176 | 'Fn' => 'Function' 177 | } 178 | entities.key?(self) ? entities[self] : self 179 | end 180 | 181 | # Spell out some characters that might be 182 | # indiscernable or easily confused 183 | def clarify_characters 184 | unclear_characters = { 185 | ',' => 'Comma (,)', 186 | '.' => 'Period (.)', 187 | ';' => 'Semicolon (;)', 188 | ':' => 'Colon (:)', 189 | '`' => 'Backtick (`)', 190 | '-' => 'Minus Sign (-)', 191 | '+' => 'Plus Sign (+)', 192 | '=' => 'Equals Sign (=)', 193 | '_' => 'Underscore (_)', 194 | '~' => 'Tilde (~)' 195 | } 196 | unclear_characters.key?(self) ? unclear_characters[self] : self 197 | end 198 | 199 | def name_to_ent(use_symbol) 200 | k = 201 | case strip.downcase 202 | when /^f(\d{1,2})$/ 203 | num = Regexp.last_match(1) 204 | ["F#{num}", "F#{num}", "F#{num} Key"] 205 | when /^apple$/ 206 | ['Apple', '', 'Apple menu'] 207 | when /^tab$/ 208 | ['', '⇥', 'Tab Key'] 209 | when /^caps(lock)?$/ 210 | ['Caps Lock', '⇪', 'Caps Lock Key'] 211 | when /^eject$/ 212 | ['Eject', '⏏', 'Eject Key'] 213 | when /^return$/ 214 | ['Return', '⏎', 'Return Key'] 215 | when /^enter$/ 216 | ['Enter', '⌤', 'Enter (Fn Return) Key'] 217 | when /^(del(ete)?|back(space)?)$/ 218 | ['Del', '⌫', 'Delete'] 219 | when /^fwddel(ete)?$/ 220 | ['Fwd Del', '⌦', 'Forward Delete (Fn Delete)'] 221 | when /^(esc(ape)?)$/ 222 | ['Esc', '⎋', 'Escape Key'] 223 | when /^right?$/ 224 | ['Right Arrow', '→', 'Right Arrow Key'] 225 | when /^left$/ 226 | ['Left Arrow', '←', 'Left Arrow Key'] 227 | when /^up?$/ 228 | ['Up Arrow', '↑', 'Up Arrow Key'] 229 | when /^down$/ 230 | ['Down Arrow', '↓', 'Down Arrow Key'] 231 | when /^pgup$/ 232 | ['PgUp', '⇞', 'Page Up Key'] 233 | when /^pgdn$/ 234 | ['PgDn', '⇟', 'Page Down Key'] 235 | when /^home$/ 236 | ['Home', '↖', 'Home Key'] 237 | when /^end$/ 238 | ['End', '↘', 'End Key'] 239 | when /^click$/ 240 | ['click', '<i class="fas fa-mouse-pointer"></i>', 'left click'] 241 | else 242 | [self, self, capitalize] 243 | end 244 | use_symbol ? [k[1], k[2]] : [k[0], k[2]] 245 | end 246 | end 247 | 248 | class KBDTag 249 | @combos = nil 250 | 251 | def initialize(markup) 252 | @combos = [] 253 | 254 | markup.split(%r{ / }).each do |combo| 255 | mods = [] 256 | key = '' 257 | combo.clean_combo! 258 | combo.strip.split(//).each do |char| 259 | next if char == ' ' 260 | 261 | case char 262 | when /[⌃⇧⌥⌘]/ 263 | mods.push(char) 264 | when /[*\^$@~]/ 265 | mods.push(char.to_mod) 266 | else 267 | key += char 268 | end 269 | end 270 | mods = sort_mods(mods) 271 | title = '' 272 | if key.length == 1 273 | if mods.empty? && (key =~ /[A-Z]/ || key.upper?) 274 | # If there are no modifiers, convert uppercase letter 275 | # to "Shift-[Uppercase Letter]", uppercase lowercase keys 276 | mods.push('$'.to_mod) 277 | end 278 | key = key.lower_to_upper if mods.include?('$'.to_mod) 279 | key.upcase! 280 | title = key.clarify_characters 281 | elsif mods.include?('$'.to_mod) 282 | key = key.lower_to_upper 283 | end 284 | key.gsub!(/"/, '"') 285 | @combos.push({ mods: mods, key: key, title: title }) 286 | end 287 | end 288 | 289 | def sort_mods(mods) 290 | order = ['Fn', '⌃', '⌥', '⇧', '⌘'] 291 | mods.uniq! 292 | mods.sort { |a, b| order.index(a) < order.index(b) ? -1 : 1 } 293 | end 294 | 295 | def render() 296 | use_key_symbol = true 297 | use_mod_symbol = true 298 | use_plus = false 299 | 300 | output = [] 301 | 302 | @combos.each do |combo| 303 | next unless combo[:mods].length || combo[:key].length 304 | 305 | kbds = [] 306 | title = [] 307 | combo[:mods].each do |mod| 308 | mod_class = use_mod_symbol ? 'mod symbol' : 'mod' 309 | kbds.push(%(<kbd class="#{mod_class}">#{mod.mod_to_ent(use_mod_symbol)}</kbd>)) 310 | title.push(mod.mod_to_title) 311 | end 312 | unless combo[:key].empty? 313 | key, keytitle = combo[:key].name_to_ent(use_key_symbol) 314 | key_class = use_key_symbol ? 'key symbol' : 'key' 315 | keytitle = keytitle.clarify_characters if keytitle.length == 1 316 | kbds.push(%(<kbd class="#{key_class}">#{key}</kbd>)) 317 | title.push(keytitle) 318 | end 319 | kbd = if use_mod_symbol 320 | use_plus ? kbds.join('+') : kbds.join 321 | else 322 | kbds.join('-') 323 | end 324 | span_class = "keycombo #{use_mod_symbol && !use_plus ? 'combined' : 'separated'}" 325 | kbd = %(<span class="#{span_class}" title="#{title.join('-')}">#{kbd}</span>) 326 | output.push(kbd) 327 | end 328 | 329 | output.join('/') 330 | end 331 | end 332 | 333 | class String 334 | def inject_meta(string) 335 | keys = `echo #{Shellwords.escape(self)}|multimarkdown -m`.strip 336 | if keys.empty? 337 | "#{string}\n\n#{self}" 338 | else 339 | "#{string}\n#{self}" 340 | end 341 | end 342 | 343 | def inject_meta!(string) 344 | replace inject_meta(string) 345 | end 346 | end 347 | 348 | def no_custom(reason) 349 | @logger.info("NO CUSTOM: #{reason}") 350 | puts 'NOCUSTOM' 351 | Process.exit 352 | end 353 | 354 | def process_jekyll(parts, style = 'brettterpstra-2023') 355 | # full path to image folder 356 | full_image_path = '/Users/ttscoff/Sites/dev/bt/source/' 357 | # Read YAML headers as needed before cutting them out 358 | post_title = parts[1].match(/^title:\s+(!\s*)?["']?(.*?)["']?\s*$/i) 359 | post_title = post_title.nil? ? '' : post_title[2].strip 360 | 361 | no_custom('no post title') if post_title == '' 362 | # Remove YAML 363 | # content.sub!(/^---.*?---\s*$/m,'') 364 | content = parts[2..].join('---') 365 | 366 | # Fenced code 367 | content.gsub!(/^(\s*)```bunch/, '\1```bash') 368 | content.gsub!(/^([ \t]*)```(\w+)?\s*(.*?)\1```/m) do 369 | m = Regexp.last_match 370 | spacer = "#{m[1]} " 371 | m[3].split(/\n/).map { |l| spacer + l.strip }.join("\n") 372 | end 373 | 374 | content.gsub!(/\{\{\s*site\.baseurl\s*\}\}/, full_image_path.sub(%r{/$}, '')) 375 | 376 | # Replace include tags 377 | content.gsub!(/\{%\s*include (\S+)\s*%\}/) do 378 | m = Regexp.last_match 379 | file = File.join(full_image_path, '_includes', m[1]) 380 | if File.exist?(file) 381 | include_content = IO.read(file) 382 | include_content 383 | else 384 | "\n\n`Missing file: #{file}`\n\n" 385 | end 386 | end 387 | 388 | # Replace kbd tags 389 | content.gsub!(/\{%\s*kbd (.*?)\s*%\}/) do 390 | m = Regexp.last_match 391 | k = KBDTag.new(m[1]) 392 | k.render 393 | end 394 | 395 | # Replace Gif tags 396 | content.gsub!(/\{% (hover)?gif (\S+) (".*?" ){,2}%\}/) do 397 | m = Regexp.last_match 398 | hover = m[1] ? 'hover' : 'animated' 399 | image = File.join(full_image_path, m[2]) 400 | caption = if m[3] 401 | "<figcaption>#{m[3].strip.sub(/^"(.*?)"$/, '\1')}</figcaption>" 402 | else 403 | '' 404 | end 405 | if image =~ /\.mp4/ 406 | %(<figure class="#{hover}_vid_frame" tabindex="0"> 407 | <video muted loop playsinline> 408 | <source src="#{image}" type="video/mp4"> 409 | </video>#{caption}</figure>) 410 | else 411 | %(<figure class="#{hover}_gif_frame" tabindex="0"> 412 | <img class="animated_gif" src="#{image}">#{caption}</figure>) 413 | end 414 | end 415 | 416 | # Update image urls to point to absolute file path 417 | content.gsub!(%r{([("])/uploads/(\d+/.*?)([ )"])}, "\\1#{full_image_path}\\2\\3") 418 | 419 | # Process image Liquid tags 420 | content.gsub!(/\{% img (.*?) %\}/) do |img| 421 | if img =~ %r{\{% img (\S.*\s+)?(https?://\S+|/\S+|\S+/\s+)(\s+\d+\s+\d+)?(\s+.+)? %\}}i 422 | m = Regexp.last_match 423 | classes = m[1].strip if m[1] 424 | src = m[2].sub(%r{^/bunch}, '') 425 | # size = $3 426 | title = m[4] 427 | 428 | if /(?:"|')([^"']+)?(?:"|')\s+(?:"|')([^"']+)?(?:"|')/ =~ title 429 | t = Regexp.last_match 430 | title = t[1] 431 | alt = t[2] 432 | elsif title 433 | alt = title.gsub!(/"/, '"') 434 | end 435 | classes&.gsub!(/"/, '') 436 | end 437 | 438 | cssstyle = '' 439 | cssstyle = %( style="float:right;margin:0 0 10px 10px") if classes =~ /alignright/ 440 | cssstyle = %( style="float:left;margin:0 10px 10px 0") if classes =~ /alignleft/ 441 | cssstyle = %( style="width:100%;height:auto") if classes =~ /aligncenter/ 442 | 443 | %(<img src="#{File.join(full_image_path, src)}" alt="#{alt}" class="#{classes}" title="#{title}"#{cssstyle} />) 444 | end 445 | 446 | # Process gist tags 447 | content.gsub!(/\{% gist(.*?) %\}/) do |gist| 448 | gistparts = gist.match(/\{% gist (\S+) (.*?)?%\}/) 449 | 450 | if gistparts 451 | gist_id = gistparts[1].strip 452 | file = gistparts[2].nil? ? '' : "?file-#{gistparts[2].strip}" 453 | %(<script src="https://gist.github.com/#{gist_id}.js#{file}"></script>) 454 | else 455 | '' 456 | end 457 | end 458 | 459 | # Replace YouTube tags with a placeholder block 460 | # <http://brettterpstra.com/2013/01/20/jekyll-tag-plugin-for-responsive-youtube-video-embeds/> 461 | content.gsub!(/\{% youtube (\S+) ((\d+) (\d+) )?(".*?" )?%\}/) do 462 | # id = $1 463 | # width, height = $2.nil? ? [640, 480] : [$3, $4] # width:#{width}px;height:#{height}px; 464 | cssstyle = 'position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden;background:#666;' 465 | %(<div class="bt-video-container" style="#{cssstyle}"> 466 | <h3 style="text-align:center;margin-top:25%;">YouTube Video</h3></div>) 467 | end 468 | 469 | # HTML5 semantic pullquote plugin 470 | content.gsub!(/\{% pullquote(.*?) %\}(.*?)\{% endpullquote %\}/m) do 471 | m = Regexp.last_match 472 | quoteblock = m[2] 473 | if quoteblock =~ /\{"\s*(.+?)\s*"\}/m 474 | quote = m[1] 475 | "<span class='pullquote' data-pullquote='#{quote}'>#{quoteblock.gsub(/\{"\s*|\s*"\}/, '')}</span>" 476 | else 477 | quoteblock 478 | end 479 | end 480 | 481 | # Custom downloads manager plugin shiv 482 | 483 | content.gsub!(/\{% download(.*?) %\}/, %(<div class="download"><h4>A download</h4> 484 | <p class="dl_icon"><a href="#"><img src="/Users/ttscoff/Sites/dev/bt/source/images/serviceicon.jpg"></a></p> 485 | <div class="dl_body"><p class="dl_link"><a href="#">Download this download</a></p> 486 | <p class="dl_description">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 487 | tempor incididunt ut labore et dolore magna aliqua.</p><p class="dl_updated">Updated Today</p> 488 | <p class="dl_info"><a href="#" title="More information on this download">More info…</a></p></div></div>)) 489 | 490 | # remove remaining {{ liquid includes }} 491 | content.gsub!(/\{\{\s*(.*?)\s*\}\}/, '') 492 | content.gsub!(/\{%\s*(.*?)\s*%\}/, '') 493 | 494 | page_title = "## #{post_title}\n\n" 495 | 496 | content = <<~EOCONTENT 497 | #{page_title}#{content} 498 | EOCONTENT 499 | 500 | puts Kramdown::Document.new(content).to_html 501 | end 502 | 503 | begin 504 | ext = ENV['MARKED_EXT'].downcase || 'md' 505 | 506 | parts = content.split(/^---\s*$/) 507 | 508 | if ENV['MARKED_ORIGIN'] =~ %r{_(drafts|posts)/} && parts.length > 2 509 | warn('Looks like a BT.com post') 510 | parts[1] << "\nmarked style: brettterpstra-2023" 511 | process_jekyll(parts) 512 | else 513 | no_custom('wrong extension') if ext.downcase != 'md' 514 | 515 | warn('Not a Jekyll post') 516 | puts 'NOCUSTOM' 517 | end 518 | rescue StandardError => e 519 | puts 'NOCUSTOM' 520 | warn(e) 521 | warn(e.backtrace) 522 | @logger.fatal(e) 523 | end 524 | -------------------------------------------------------------------------------- /scripts/bunch-post: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -W1 2 | # frozen_string_literal: true 3 | 4 | # Version 2 (02-03-2015) 5 | # 6 | # Example custom processor for use with Marked <http://markedapp.com> and Jekyll _posts 7 | # It's geared toward my personal set of plugins and tags, but you'll get the idea. 8 | # It turns 9 | # {% img alignright /images/heythere.jpg 100 100 "Hey there" "hi" %} 10 | # into 11 | # <img src="../images/heythere.jpg" alt="Hey there" class="alignright" title="hi" /> 12 | # 13 | # replaces alignleft and alignright classes with appropriate style attribute 14 | # --- 15 | # Replaces {% gist XXXXX filename.rb %} with appropriate script tag 16 | # 17 | # Replace various other OctoPress, Jekyll and custom tags 18 | # 19 | # Processes final output with /usr/bin/kramdown (install kramdown as system gem: `sudo gem install kramdown`) 20 | # 21 | # Be sure to run *without* stripping YAML headers in Marked Behavior preferences. 22 | 23 | require 'rubygems' 24 | require 'shellwords' 25 | require 'kramdown' 26 | require 'uri' 27 | require 'cgi' 28 | require 'erb' 29 | require 'logger' 30 | require 'nokogiri/nokogiri' 31 | 32 | @logger = Logger.new(File.expand_path('~/logs/jekyllpre.log')) 33 | 34 | if ARGV[0] == 'testing' 35 | warn 'Just a test' 36 | Process.exit 0 37 | end 38 | 39 | def class_exists?(class_name) 40 | klass = Module.const_get(class_name) 41 | klass.is_a?(Class) 42 | rescue NameError 43 | false 44 | end 45 | 46 | if class_exists? 'Encoding' 47 | Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external') 48 | Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal') 49 | end 50 | 51 | begin 52 | content = $stdin.read.force_encoding('utf-8') 53 | rescue StandardError 54 | content = $stdin.read 55 | end 56 | 57 | # Title: Keyboard markup tag 58 | # Author: Brett Terpstra <https://brettterpstra.com> 59 | # Description: Apply HTML markup for keyboard shortcuts 60 | # 61 | # See Readme for syntax help and configuration details. 62 | # 63 | # Configuration options: 64 | # 65 | # kbd: 66 | # use_modifier_symbols: true 67 | # use_key_symbols: true 68 | # use_plus_sign: true 69 | # 70 | # example: 71 | # 72 | # Input: 73 | # 74 | # {% kbd ^~@r %} 75 | # 76 | # Output: 77 | # <span class="keycombo" title="Control-Option-Command-R"> 78 | # <kbd class="mod">⌃</kbd>+<kbd class="mod">⌥</kbd>+<kbd class="mod">⌘</kbd>+<kbd class="key">R</kbd> 79 | # </span> 80 | 81 | # String Helpers 82 | class ::String 83 | # Convert natural language combo to shorcut symbols 84 | # ctrl-cmd-f => ^@f 85 | def clean_combo 86 | # Only remove hyphens preced and followed by non-space character 87 | # to avoid removing hyphen from 'option-shift--' or 'command -' 88 | gsub!(/(?<=\S)-(?=\S)/, ' ') 89 | gsub!(/\b(comm(and)?|cmd|clover)\b/i, '@') 90 | gsub!(/\b(cont(rol)?|ctl|ctrl)\b/i, '^') 91 | gsub!(/\b(opt(ion)?|alt)\b/i, '~') 92 | gsub!(/\bshift\b/i, '$') 93 | gsub!(/\b(func(tion)?|fn)\b/i, '*') 94 | self 95 | end 96 | 97 | # For combos containing shift key, use upper symbol for keys with two characters 98 | # Shift-/ should be Shift-? 99 | def lower_to_upper 100 | doubles = [ 101 | [',', '<'], 102 | ['.', '>'], 103 | ['/', '?'], 104 | [';', ':'], 105 | ["'", '"'], 106 | ['[', '{'], 107 | [']', '}'], 108 | ['\\', '|'], 109 | ['-', '_'], 110 | ['=', '+'] 111 | ] 112 | 113 | lowers = [] 114 | uppers = [] 115 | doubles.each do |dbl| 116 | lowers.push(dbl[0]) 117 | uppers.push(dbl[1]) 118 | end 119 | 120 | lowers.include?(self) ? uppers[lowers.index(self)] : self 121 | end 122 | 123 | # Detect combos using upper character of double 124 | # Command-? should be Command-Shift-? 125 | def upper? 126 | uppers = %w(< > ? : " { } | ! @ # $ % ^ & * \( \) _ +) 127 | uppers.include?(self) 128 | end 129 | 130 | def clean_combo! 131 | replace clean_combo 132 | end 133 | 134 | # Convert modifier shortcut symbols to unicode 135 | def to_mod 136 | characters = { 137 | '^' => '⌃', 138 | '~' => '⌥', 139 | '$' => '⇧', 140 | '@' => '⌘', 141 | '*' => 'Fn' 142 | } 143 | characters.key?(self) ? characters[self] : self 144 | end 145 | 146 | # Convert unicode modifiers to HTML entities 147 | def mod_to_ent(use_symbol) 148 | entities = { 149 | '⌃' => '⌃', 150 | '⌥' => '⌥', 151 | '⇧' => '⇧', 152 | '⌘' => '⌘', 153 | 'Fn' => 'Fn' 154 | } 155 | names = { 156 | '⌃' => 'Control', 157 | '⌥' => 'Option', 158 | '⇧' => 'Shift', 159 | '⌘' => 'Command', 160 | 'Fn' => 'Function' 161 | } 162 | if entities.key?(self) 163 | use_symbol ? entities[self] : names[self] 164 | else 165 | self 166 | end 167 | end 168 | 169 | # Spell out modifier symbols for titles 170 | def mod_to_title 171 | entities = { 172 | '⌃' => 'Control', 173 | '⌥' => 'Option', 174 | '⇧' => 'Shift', 175 | '⌘' => 'Command', 176 | 'Fn' => 'Function' 177 | } 178 | entities.key?(self) ? entities[self] : self 179 | end 180 | 181 | # Spell out some characters that might be 182 | # indiscernable or easily confused 183 | def clarify_characters 184 | unclear_characters = { 185 | ',' => 'Comma (,)', 186 | '.' => 'Period (.)', 187 | ';' => 'Semicolon (;)', 188 | ':' => 'Colon (:)', 189 | '`' => 'Backtick (`)', 190 | '-' => 'Minus Sign (-)', 191 | '+' => 'Plus Sign (+)', 192 | '=' => 'Equals Sign (=)', 193 | '_' => 'Underscore (_)', 194 | '~' => 'Tilde (~)' 195 | } 196 | unclear_characters.key?(self) ? unclear_characters[self] : self 197 | end 198 | 199 | def name_to_ent(use_symbol) 200 | k = 201 | case strip.downcase 202 | when /^f(\d{1,2})$/ 203 | num = Regexp.last_match(1) 204 | ["F#{num}", "F#{num}", "F#{num} Key"] 205 | when /^apple$/ 206 | ['Apple', '', 'Apple menu'] 207 | when /^tab$/ 208 | ['', '⇥', 'Tab Key'] 209 | when /^caps(lock)?$/ 210 | ['Caps Lock', '⇪', 'Caps Lock Key'] 211 | when /^eject$/ 212 | ['Eject', '⏏', 'Eject Key'] 213 | when /^return$/ 214 | ['Return', '⏎', 'Return Key'] 215 | when /^enter$/ 216 | ['Enter', '⌤', 'Enter (Fn Return) Key'] 217 | when /^(del(ete)?|back(space)?)$/ 218 | ['Del', '⌫', 'Delete'] 219 | when /^fwddel(ete)?$/ 220 | ['Fwd Del', '⌦', 'Forward Delete (Fn Delete)'] 221 | when /^(esc(ape)?)$/ 222 | ['Esc', '⎋', 'Escape Key'] 223 | when /^right?$/ 224 | ['Right Arrow', '→', 'Right Arrow Key'] 225 | when /^left$/ 226 | ['Left Arrow', '←', 'Left Arrow Key'] 227 | when /^up?$/ 228 | ['Up Arrow', '↑', 'Up Arrow Key'] 229 | when /^down$/ 230 | ['Down Arrow', '↓', 'Down Arrow Key'] 231 | when /^pgup$/ 232 | ['PgUp', '⇞', 'Page Up Key'] 233 | when /^pgdn$/ 234 | ['PgDn', '⇟', 'Page Down Key'] 235 | when /^home$/ 236 | ['Home', '↖', 'Home Key'] 237 | when /^end$/ 238 | ['End', '↘', 'End Key'] 239 | when /^click$/ 240 | ['click', '<i class="fas fa-mouse-pointer"></i>', 'left click'] 241 | else 242 | [self, self, capitalize] 243 | end 244 | use_symbol ? [k[1], k[2]] : [k[0], k[2]] 245 | end 246 | end 247 | 248 | class KBDTag 249 | @combos = nil 250 | 251 | def initialize(markup) 252 | @combos = [] 253 | 254 | markup.split(%r{ / }).each do |combo| 255 | mods = [] 256 | key = '' 257 | combo.clean_combo! 258 | combo.strip.split(//).each do |char| 259 | next if char == ' ' 260 | 261 | case char 262 | when /[⌃⇧⌥⌘]/ 263 | mods.push(char) 264 | when /[*\^$@~]/ 265 | mods.push(char.to_mod) 266 | else 267 | key += char 268 | end 269 | end 270 | mods = sort_mods(mods) 271 | title = '' 272 | if key.length == 1 273 | if mods.empty? && (key =~ /[A-Z]/ || key.upper?) 274 | # If there are no modifiers, convert uppercase letter 275 | # to "Shift-[Uppercase Letter]", uppercase lowercase keys 276 | mods.push('$'.to_mod) 277 | end 278 | key = key.lower_to_upper if mods.include?('$'.to_mod) 279 | key.upcase! 280 | title = key.clarify_characters 281 | elsif mods.include?('$'.to_mod) 282 | key = key.lower_to_upper 283 | end 284 | key.gsub!(/"/, '"') 285 | @combos.push({ mods: mods, key: key, title: title }) 286 | end 287 | end 288 | 289 | def sort_mods(mods) 290 | order = ['Fn', '⌃', '⌥', '⇧', '⌘'] 291 | mods.uniq! 292 | mods.sort { |a, b| order.index(a) < order.index(b) ? -1 : 1 } 293 | end 294 | 295 | def render() 296 | use_key_symbol = true 297 | use_mod_symbol = true 298 | use_plus = false 299 | 300 | output = [] 301 | 302 | @combos.each do |combo| 303 | next unless combo[:mods].length || combo[:key].length 304 | 305 | kbds = [] 306 | title = [] 307 | combo[:mods].each do |mod| 308 | mod_class = use_mod_symbol ? 'mod symbol' : 'mod' 309 | kbds.push(%(<kbd class="#{mod_class}">#{mod.mod_to_ent(use_mod_symbol)}</kbd>)) 310 | title.push(mod.mod_to_title) 311 | end 312 | unless combo[:key].empty? 313 | key, keytitle = combo[:key].name_to_ent(use_key_symbol) 314 | key_class = use_key_symbol ? 'key symbol' : 'key' 315 | keytitle = keytitle.clarify_characters if keytitle.length == 1 316 | kbds.push(%(<kbd class="#{key_class}">#{key}</kbd>)) 317 | title.push(keytitle) 318 | end 319 | kbd = if use_mod_symbol 320 | use_plus ? kbds.join('+') : kbds.join 321 | else 322 | kbds.join('-') 323 | end 324 | span_class = "keycombo #{use_mod_symbol && !use_plus ? 'combined' : 'separated'}" 325 | kbd = %(<span class="#{span_class}" title="#{title.join('-')}">#{kbd}</span>) 326 | output.push(kbd) 327 | end 328 | 329 | output.join('/') 330 | end 331 | end 332 | 333 | class String 334 | def inject_meta(string) 335 | keys = `echo #{Shellwords.escape(self)}|multimarkdown -m`.strip 336 | if keys.empty? 337 | "#{string}\n\n#{self}" 338 | else 339 | "#{string}\n#{self}" 340 | end 341 | end 342 | 343 | def inject_meta!(string) 344 | replace inject_meta(string) 345 | end 346 | end 347 | 348 | def no_custom(reason) 349 | @logger.info("NO CUSTOM: #{reason}") 350 | puts 'NOCUSTOM' 351 | Process.exit 352 | end 353 | 354 | def process_jekyll(parts, style = 'Bunch') 355 | return parts.join("\n") if parts[1].nil? 356 | 357 | # full path to image folder 358 | full_image_path = '/Users/ttscoff/Sites/dev/bunch/' 359 | base_url = '/Users/ttscoff/Sites/dev/bunch' 360 | # Read YAML headers as needed before cutting them out 361 | post_title = parts[1].match(/^title:\s+(!\s*)?["']?(.*?)["']?\s*$/i) 362 | post_title = post_title.nil? ? '' : post_title[2].strip 363 | 364 | no_custom('no post title') if post_title == '' 365 | # Remove YAML 366 | # content.sub!(/^---.*?---\s*$/m,'') 367 | content = parts[2..].join('---') 368 | 369 | # Fenced code 370 | content.gsub!(/^(\s*)```bunch/, '\1```bash') 371 | content.gsub!(/^([ \t]*)```(\w+)?\s*(.*?)\1```/m) do 372 | m = Regexp.last_match 373 | spacer = "#{m[1]} " 374 | m[3].split(/\n/).map { |l| spacer + l.strip }.join("\n") 375 | end 376 | 377 | config = YAML.safe_load(IO.read('/Users/ttscoff/Sites/dev/bunch/_config.yml')) 378 | content.gsub!(%r{\{\{\s*site\.baseurl(?:.*?)\s*\}\}/(.*?)/?(#.*?)?\)}, File.join(base_url, '\1.md\2)')) 379 | 380 | content.gsub!(/\{\{\s*site\.(.*?)(?: .*?)?\s*\}\}/) do 381 | m = Regexp.last_match 382 | if config&.key?(m[1]) 383 | config[m[1]] 384 | else 385 | '' 386 | end 387 | end 388 | content.gsub!(/\{\{\s*site\.baseurl\s*\}\}/, '/Users/ttscoff/Sites/dev/bunch') 389 | 390 | # Replace include tags 391 | content.gsub!(/\{%\s*include (\S+)\s*%\}/) do 392 | m = Regexp.last_match 393 | file = File.join(full_image_path, '_includes', m[1]) 394 | if File.exist?(file) 395 | include_content = IO.read(file) 396 | include_content 397 | else 398 | "\n\n`Missing file: #{file}`\n\n" 399 | end 400 | end 401 | 402 | # Replace kbd tags 403 | content.gsub!(/\{%\s*kbd (.*?)\s*%\}/) do 404 | m = Regexp.last_match 405 | k = KBDTag.new(m[1]) 406 | k.render 407 | end 408 | 409 | # Replace Gif tags 410 | content.gsub!(/\{% (hover)?gif (\S+) (".*?" ){,2}%\}/) do 411 | m = Regexp.last_match 412 | hover = m[1] ? 'hover' : 'animated' 413 | image = File.join(full_image_path, m[2]) 414 | caption = if m[3] 415 | "<figcaption>#{m[3].strip.sub(/^"(.*?)"$/, '\1')}</figcaption>" 416 | else 417 | '' 418 | end 419 | if image =~ /\.mp4/ 420 | %(<figure class="#{hover}_vid_frame" tabindex="0"> 421 | <video muted loop playsinline> 422 | <source src="#{image}" type="video/mp4"> 423 | </video>#{caption}</figure>) 424 | else 425 | %(<figure class="#{hover}_gif_frame" tabindex="0"> 426 | <img class="animated_gif" src="#{image}">#{caption}</figure>) 427 | end 428 | end 429 | 430 | # Update image urls to point to absolute file path 431 | content.gsub!(%r{([("])/uploads/(\d+/.*?)([ )"])}, "\\1#{full_image_path}\\2\\3") 432 | 433 | # Process image Liquid tags 434 | content.gsub!(/\{% img (.*?) %\}/) do |img| 435 | if img =~ %r{\{% img (\S.*\s+)?(https?://\S+|/\S+|\S+/\s+)(\s+\d+\s+\d+)?(\s+.+)? %\}}i 436 | m = Regexp.last_match 437 | classes = m[1].strip if m[1] 438 | src = m[1].sub(%r{^/bunch}, '') 439 | # size = $3 440 | title = m[4] 441 | 442 | if /(?:"|')([^"']+)?(?:"|')\s+(?:"|')([^"']+)?(?:"|')/ =~ title 443 | t = Regexp.last_match 444 | title = t[1] 445 | alt = t[2] 446 | elsif title 447 | alt = title.gsub!(/"/, '"') 448 | end 449 | classes&.gsub!(/"/, '') 450 | end 451 | 452 | cssstyle = %( style="float:right;margin:0 0 10px 10px") if classes =~ /alignright/ 453 | cssstyle = %( style="float:left;margin:0 10px 10px 0") if classes =~ /alignleft/ 454 | 455 | %(<img src="#{File.join(full_image_path, src)}" alt="#{alt}" class="#{classes}" title="#{title}"#{cssstyle} />) 456 | end 457 | 458 | # Process gist tags 459 | content.gsub!(/\{% gist(.*?) %\}/) do |gist| 460 | gistparts = gist.match(/\{% gist (\S+) (.*?)?%\}/) 461 | 462 | if gistparts 463 | gist_id = gistparts[1].strip 464 | file = gistparts[2].nil? ? '' : "?file-#{gistparts[2].strip}" 465 | %(<script src="https://gist.github.com/#{gist_id}.js#{file}"></script>) 466 | else 467 | '' 468 | end 469 | end 470 | 471 | # Replace YouTube tags with a placeholder block 472 | # <http://brettterpstra.com/2013/01/20/jekyll-tag-plugin-for-responsive-youtube-video-embeds/> 473 | content.gsub!(/\{% youtube (\S+) ((\d+) (\d+) )?(".*?" )?%\}/) do 474 | # id = $1 475 | # width, height = $2.nil? ? [640, 480] : [$3, $4] # width:#{width}px;height:#{height}px; 476 | cssstyle = 'position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden;background:#666;' 477 | %(<div class="bt-video-container" style="#{cssstyle}"> 478 | <h3 style="text-align:center;margin-top:25%;">YouTube Video</h3></div>) 479 | end 480 | 481 | # HTML5 semantic pullquote plugin 482 | content.gsub!(/\{% pullquote(.*?) %\}(.*?)\{% endpullquote %\}/m) do 483 | m = Regexp.last_match 484 | quoteblock = m[2] 485 | if quoteblock =~ /\{"\s*(.+?)\s*"\}/m 486 | quote = m[1] 487 | "<span class='pullquote' data-pullquote='#{quote}'>#{quoteblock.gsub(/\{"\s*|\s*"\}/, '')}</span>" 488 | else 489 | quoteblock 490 | end 491 | end 492 | 493 | # Custom downloads manager plugin shiv 494 | content.gsub!(/\{%\s*(download(beta)?(button)?)(.*?)\s*%\}/) do 495 | m = Regexp.last_match 496 | button = true if m[3] 497 | text = m[4] ? m[4].strip : 'Download Bunch vX.X.X' 498 | %(<a#{button ? ' class="download button"' : ''} href="#"><i class="fa fa-download"></i> #{text}</a>) 499 | end 500 | 501 | # remove remaining {{ liquid includes }} 502 | content.gsub!(/\{\{\s*(.*?)\s*\}\}/, '') 503 | content.gsub!(/\{%\s*(.*?)\s*%\}/, '') 504 | 505 | nav = '' 506 | 507 | exts = ['*.md', '*.markdown'] 508 | files = Dir.glob(exts.map { |ext| File.join(base_url, ext) }) 509 | files.concat(Dir.glob(exts.map { |ext| File.join(base_url, 'docs', '**', ext) })) 510 | pages = [] 511 | files.each do |file| 512 | path = file.sub(/\.(md|markdown)$/, '').sub(%r{^.*?bunch/}, '').split(%r{/}) 513 | page = path.pop 514 | page_path = %(<span class="parent">#{path.map { |x| x[0..2] }.join('/')}/</span><span class="page">#{page}</span>) 515 | pages.push(%(<li class="nav-list-item"><a class="nav-list-link" href="#{file}">#{page_path}</a></li>)) 516 | end 517 | nav = %(<div class="side-bar"> 518 | <nav id="site-nav" class="site-nav"><ul class="nav-list"> 519 | #{pages.join("\n")} 520 | </ul></nav> 521 | </div> 522 | ) 523 | script = '' 524 | 525 | script = %( 526 | 527 | <script src="/Users/ttscoff/Library/Application%20Support/Marked%202/Custom%20CSS/Bunch.js?#{rand(10_000)}"></script> 528 | <script src="https://kit.fontawesome.com/fb6c61417e.js" crossorigin="anonymous"></script> 529 | ) 530 | 531 | out = Kramdown::Document.new(content).to_html 532 | out = %(<div id="main-content" class="main-content">\n\n#{out}\n\n</div>\n#{nav}\n#{script}) 533 | $stdout.puts out 534 | end 535 | 536 | begin 537 | parts = content.split(/^---\s*$/) 538 | process_jekyll(parts, 'Bunch') 539 | rescue StandardError => e 540 | puts 'NOCUSTOM' 541 | warn(e) 542 | warn(e.backtrace) 543 | @logger.fatal(e) 544 | end 545 | -------------------------------------------------------------------------------- /scripts/lib/helpbuilder.rb: -------------------------------------------------------------------------------- 1 | class HelpBuilder 2 | include Utils 3 | attr_accessor :config, :outfolder, :basefolder, :index, :version, :keywords, :filelist 4 | 5 | def initialize(deploy, build_index, scripts_only) 6 | @settings = DEFAULT_SETTINGS.dup 7 | @internal = '' 8 | @basefolder = File.join(File.dirname(__FILE__), '..') 9 | Dir.chdir(@basefolder) 10 | config_file = File.expand_path("#{@basefolder}/config.yaml") 11 | 12 | @config = Utils.load_config(config_file) 13 | 14 | @main_title = @config['Title'] 15 | @main_logo = @config['Logo'] 16 | @main_css = @config['CSS'] 17 | @secondary_css = @config['CSS2'] 18 | 19 | @version = @config['Version'].to_s 20 | @help_outfolder = File.expand_path("#{@basefolder}/#{@config['Title']}.help") 21 | @outfolder = File.expand_path("#{@basefolder}/#{@config['Title']}.web") 22 | 23 | # @keywords = keyword_list 24 | @filelist = file_list 25 | @searchindex = [] 26 | 27 | # Make storage directory if needed 28 | FileUtils.mkdir_p(@outfolder, mode: 0o755) unless File.exist? @outfolder 29 | FileUtils.mkdir_p(@help_outfolder, mode: 0o755) unless File.exist? @help_outfolder 30 | 31 | if scripts_only 32 | copy_dependencies 33 | run_deploy(deploy: deploy) 34 | Process.exit 35 | end 36 | 37 | generate_index_page 38 | generate_changelog 39 | 40 | @index = generate_index('../') 41 | build_help 42 | generate_marked_index 43 | generate_full_index if build_index 44 | generate_search_page 45 | copy_dependencies 46 | 47 | run_deploy(deploy: deploy) 48 | end 49 | 50 | def run_deploy(deploy: true) 51 | if deploy 52 | # Utils.update_status("Building Apple Help Index",{:last => true}) 53 | # %x{hiutil -C -g -s en -m 3 -f "#{@help_outfolder}/#{@config["Title"]}.helpindex" "#{@help_outfolder}"} 54 | Utils.update_status("Deploying to #{@config['TargetProject']}", { last: true }) 55 | FileUtils.copy_entry(@help_outfolder, File.expand_path(@config['TargetProject'])) 56 | `touch "#{File.expand_path(@config['TargetProject'])}"` 57 | end 58 | Utils.update_status("Populating test project (#{@config['TestingProject']})", { last: true }) 59 | FileUtils.copy_entry(@outfolder, File.expand_path(@config['TestingProject'])) 60 | end 61 | 62 | def generate_marked_index 63 | Utils.update_status('Generating Marked 2 index') 64 | output = ['# nvUltra Docs'] 65 | @config['Pages'].each_with_index do |page, _i| 66 | output.push "{{content/#{page['file']}.md}}" 67 | end 68 | File.open(File.join(@basefolder, 'index.md'), 'w') do |f| 69 | f.puts output.join("\n\n") 70 | end 71 | end 72 | 73 | def generate_search_json 74 | Utils.update_status('Generating search') 75 | out = { 'pages' => @searchindex } 76 | out.to_json 77 | end 78 | 79 | def read_changelog 80 | # input = IO.read(File.expand_path("~/Dropbox/nvALT2.2/nvUltra release notes.md")).force_encoding('utf-8') 81 | input = IO.read(File.expand_path('content/changelog.md')).force_encoding('utf-8') 82 | 83 | input.gsub!(/^(nvUltra.*?(\d\.\d\.\d) \(.*?\)\n-+)/m, "%%%%BREAK%%%%\n\\1") 84 | input.gsub!(/^(.*?)\n=+/m, "\n## \\1\n") 85 | 86 | updates = input.split(/%%%%BREAK%%%%/) 87 | 88 | out = "# nvUltra Changelog\n\n" 89 | 90 | updates.each do |update| 91 | m = update.match(/^nvUltra.*?(\d\.\d\.\d) \(([0-9.]+)\)\n-+/) 92 | content = update.gsub(/^nvUltra.*?(\d\.\d\.\d) \(([0-9.]+)\)\n-+/, '').strip 93 | next if m.nil? 94 | 95 | ver = "#{m[1]} (#{m[2]})" 96 | out += "### Version #{ver}\n\n<section>\n\n" 97 | out += content + "\n\n</section>\n\n" 98 | end 99 | 100 | out 101 | end 102 | 103 | def generate_search_page 104 | Utils.update_status('Building Search') 105 | @sections = generate_index('./') 106 | template = ERB.new(Utils.load_template("#{@basefolder}/resources/search_template.html")) 107 | prefix = './' 108 | @subtitle = @config['Title'] 109 | @main_logo = @config['Logo'] 110 | @main_css = @config['CSS'] 111 | @secondary_css = @config['CSS2'] 112 | @searchjson = generate_search_json 113 | @title = @config['Title'] + ' - Search' 114 | @section_class = 'search' 115 | @page_class = 'search' 116 | 117 | @prefix = './' 118 | File.open(@outfolder + '/search.html', 'w+') do |out| 119 | out.puts(template.result(binding)) 120 | end 121 | File.open(@help_outfolder + '/search.html', 'w+') do |out| 122 | out.puts(template.result(binding)) 123 | end 124 | File.open(@help_outfolder + '/search.json', 'w+') do |f| 125 | f.puts @searchjson 126 | end 127 | end 128 | 129 | def generate_changelog 130 | Utils.update_status('Building Changelog') 131 | 132 | @sections = generate_index('./') 133 | 134 | template = ERB.new(Utils.load_template("#{@basefolder}/resources/template.html")) 135 | help_template = ERB.new(Utils.load_template("#{@basefolder}/resources/template_internal.html")) 136 | prefix = './' 137 | @subtitle = @config['Title'] 138 | 139 | changelog = read_changelog 140 | @title = @config['Title'] + ' - Changelog' 141 | @section_class = 'changelog' 142 | @page_class = 'changelog' 143 | text = ERB.new(changelog).result(binding) 144 | 145 | @content = `echo #{Shellwords.escape(text)}|/usr/local/bin/multimarkdown` 146 | prefix = './' 147 | File.open(@outfolder + '/changelog.html', 'w+') do |outfile| 148 | outfile.puts(template.result(binding)) 149 | end 150 | File.open(@help_outfolder + '/changelog.html', 'w+') do |outfile| 151 | outfile.puts(help_template.result(binding)) 152 | end 153 | end 154 | 155 | def generate_index_page 156 | Utils.update_status('Building Home Page') 157 | @sections = generate_index('./') 158 | template = ERB.new(Utils.load_template("#{@basefolder}/resources/template.html")) 159 | help_template = ERB.new(Utils.load_template("#{@basefolder}/resources/template_internal.html")) 160 | 161 | prefix = './' 162 | @subtitle = @main_title 163 | 164 | infile = @basefolder + "/content#{@settings[:debug]}/" + @config['MDIndex'] + '.md' 165 | @title = @config['Title'] 166 | @section_class = 'gettingstarted' 167 | @page_class = 'overview' 168 | 169 | @prevlink = '' 170 | next_page = @config['Pages'][1] 171 | @nextlink = %(<a href="#{next_page['file']}.html" class="nextlink">#{next_page['title']} <b>▶</b></a>) 172 | 173 | text = ERB.new(Utils.load_template(infile)).result(binding) 174 | text = text.render_liquid 175 | 176 | has_images = text.scan(/\[(.*?)\]:\s*(?!http)(.*?\.(jpg|png|gif|svg))\s*$/) 177 | if !has_images.empty? 178 | Utils.update_status("Adding sizes to #{has_images.length} images") 179 | has_images.each do |image_match| 180 | image_id = image_match[0] 181 | image_path = image_match[1] 182 | image_path = if image_path =~ %r{^[/~]} # absolute path 183 | File.expand_path(image_path) 184 | else 185 | File.expand_path("#{@basefolder}/content#{@settings[:debug]}/#{image_path}") 186 | end 187 | width = `sips -g pixelWidth "#{image_path}"|tail -n1|awk '{print $2}'`.strip 188 | height = `sips -g pixelHeight "#{image_path}"|tail -n1|awk '{print $2}'`.strip 189 | text.gsub!(/\[#{image_id}\]:.*?\.(jpg|png|gif|svg)/, 190 | "[#{image_id}]: #{image_match[1]} width=#{width}px height=#{height}px") 191 | end 192 | end 193 | 194 | @content = `echo #{Shellwords.escape(Utils.remove_todos(text))}|/usr/local/bin/multimarkdown` 195 | @prefix = './' 196 | 197 | File.open("#{@outfolder}/index.html", 'w+') do |outfile| 198 | outfile.puts(template.result(binding)) 199 | end 200 | File.open("#{@outfolder}/index.html", 'w+') do |outfile| 201 | outfile.puts(help_template.result(binding)) 202 | end 203 | end 204 | 205 | def generate_index(prefix) 206 | Utils.update_status('Building Sidebar') 207 | 208 | index_template = ERB.new <<-SECTIONTEMPLATE 209 | <ul class="uk-nav"><%= @lessons %></ul> 210 | SECTIONTEMPLATE 211 | 212 | @lessons = '' 213 | @config['Pages'].each do |page| 214 | @title = page['title'] 215 | @filename = page['file'] 216 | @page_class = @filename.downcase.gsub(/[-_]/, '') 217 | @lessons += ERB.new(%(<li> 218 | <a class="<%= @page_class %>" href="<%= @filename %>.html"><h4><%= @title %></h4></a> 219 | </li>\n)).result(binding) 220 | end 221 | 222 | index_template.result(binding) 223 | end 224 | 225 | def generate_page(page, prev_page, next_page) 226 | Utils.update_status("Generating page: #{page['title']}") 227 | @subtitle = page['title'] 228 | 229 | infile = "#{@basefolder}/content#{@settings[:debug]}/#{page['file']}.md" 230 | outfile = "#{@outfolder}/#{page['file']}.html" 231 | help_outfile = "#{@help_outfolder}/#{page['file']}.html" 232 | 233 | @prevlink = prev_page ? %(<a href="#{prev_page[:url]}.html" class="prevlink"><b>◀</b> #{prev_page[:title]}</a>) : '' 234 | @nextlink = next_page ? %(<a href="#{next_page[:url]}.html" class="nextlink">#{next_page[:title]} <b>▶</b></a>) : '' 235 | @nextuplink = next_page ? %(<h4 class="nextup uk-heading-medium">Next up: #{@nextlink}</h4>) : '' 236 | @page_class = page['file'].downcase.gsub(/[-_]/, '') 237 | @section_class = page['file'].downcase.gsub(/[-_]/, '') 238 | @title = page['title'] 239 | @title = @config['Title'] + ' - Changelog' 240 | 241 | section_folder = '.' 242 | prefix = '../' 243 | 244 | text = ERB.new(Utils.load_template(infile)).result(binding) 245 | text = text.render_liquid 246 | 247 | has_images = text.scan(/\[(.*?)\]:\s*(?!http)(.*?\.(jpg|png|gif|svg))\s*$/) 248 | if has_images.length > 0 249 | Utils.update_status("Adding sizes to #{has_images.length} images") 250 | has_images.each do |image_match| 251 | image_id = image_match[0] 252 | image_path = image_match[1] 253 | image_path = if image_path =~ %r{^[/~]} # absolute path 254 | File.expand_path(image_path) 255 | else 256 | File.expand_path("#{@basefolder}/content#{@settings[:debug]}/#{image_path}") 257 | end 258 | width = `sips -g pixelWidth "#{image_path}"|tail -n1|awk '{print $2}'`.strip 259 | height = `sips -g pixelHeight "#{image_path}"|tail -n1|awk '{print $2}'`.strip 260 | text.gsub!(/\[#{image_id}\]:.*?\.(jpg|png|gif|svg)/, 261 | "[#{image_id}]: #{image_match[1]} width=#{width}px height=#{height}px") 262 | end 263 | end 264 | 265 | @content = `echo #{Shellwords.escape(Utils.remove_todos(text))}|/usr/local/bin/multimarkdown` 266 | @sections = @index 267 | 268 | output = ERB.new(Utils.load_template("#{@basefolder}/resources/template.html")).result(binding) 269 | help_output = ERB.new(Utils.load_template("#{@basefolder}/resources/template_internal.html")).result(binding) 270 | 271 | [[outfile, output], [help_outfile, help_output]].each do |out| 272 | FileUtils.mkdir_p(File.dirname(out[0])) unless File.exist?(File.dirname(out[0])) 273 | if File.directory?(File.dirname(out[0])) 274 | File.open(out[0], 'w+') do |f| 275 | f.puts(out[1]) 276 | end 277 | else 278 | Utils.update_status("ERROR: #{File.dirname(out[0])} exists and is not a folder", { last: true }) 279 | Process.exit 1 280 | end 281 | end 282 | end 283 | 284 | def build_help 285 | @config['Pages'].each_with_index do |page, i| 286 | prev_page = nil 287 | next_page = nil 288 | 289 | if i > 0 290 | previous = @config['Pages'][i - 1] 291 | prev_page = { 292 | url: previous['file'], 293 | title: previous['title'] 294 | } 295 | end 296 | 297 | if i < @config['Pages'].length - 1 298 | nxt = @config['Pages'][i + 1] 299 | next_page = { 300 | url: nxt['file'], 301 | title: nxt['title'] 302 | } 303 | end 304 | 305 | generate_page(page, prev_page, next_page) 306 | end 307 | 308 | [@outfolder, @help_outfolder].each do |odir| 309 | Utils.update_status("Copying images to #{odir}") 310 | FileUtils.copy_entry("#{@basefolder}/content#{@settings[:debug]}/images", "#{odir}/images") 311 | # FileUtils.copy_entry("#{@basefolder}/images","#{odir}/images") 312 | end 313 | end 314 | 315 | def file_list 316 | list = {} 317 | @config['Pages'].each do |page| 318 | list[page['file']] = page['file'] 319 | end 320 | list 321 | end 322 | 323 | def keyword_list 324 | list = {} 325 | @config['Sections'].each do |section| 326 | folder = section['folder'] 327 | section['pages'].each do |page| 328 | next if page['keywords'].nil? 329 | 330 | page['keywords'].each do |keyword| 331 | list[keyword] = "#{folder}/#{page['file']}.html" 332 | end 333 | end 334 | end 335 | list 336 | end 337 | 338 | def wiki_link(input, page, index) 339 | return input if @keywords.empty? 340 | 341 | prefix = index ? '' : '../' 342 | keywords.each do |k, v| 343 | input.gsub!(/ (#{k})\b/i, " [\\1](#{v})") if v !~ %r{.*?/#{page}.html$} 344 | end 345 | input 346 | end 347 | 348 | def resolve_internal_links(input, index) 349 | list = @filelist 350 | prefix = index ? '' : '../' 351 | input.gsub(/\[\[(.*?)\]\]/) do |match| 352 | file = match.gsub(/\[\[(.*?)\]\]/, '\\1') 353 | suffix = '.html' 354 | if file =~ /(.+?)#(.+)$/ 355 | file = ::Regexp.last_match(1) 356 | suffix = '.html#' + ::Regexp.last_match(2) 357 | end 358 | list[file] + suffix unless list[file].nil? 359 | end 360 | end 361 | 362 | def copy_dependencies 363 | @config['Dependencies'].each do |dep| 364 | dep = File.join(@config['DependenciesBase'], dep) 365 | if dep =~ /\.s?css$/ 366 | Utils.update_status("Converting/Minifying CSS: #{dep}") 367 | filename = dep.gsub(/\.s?css$/, '') 368 | minified_fn = filename + '.min.css' 369 | `sass --scss #{dep} #{filename}.css` if dep =~ /\.scss$/ 370 | `/usr/local/bin/yuicompressor -o "#{minified_fn}" "#{filename}.css"` 371 | dep = minified_fn 372 | Utils.update_status("Minified: #{minified_fn}", { last: true }) 373 | end 374 | Utils.update_status("Copying #{dep}") 375 | if File.directory?(dep) 376 | [@outfolder, @help_outfolder].each do |folder| 377 | Utils.update_status("Copying #{dep} to #{folder}") 378 | FileUtils.cp_r(dep, folder) 379 | end 380 | else 381 | [@outfolder, @help_outfolder].each do |folder| 382 | FileUtils.copy("#{@basefolder}/#{dep}", folder) if dep =~ /(css|jpg|png|gif|json)$/ 383 | end 384 | end 385 | end 386 | js_file = concat_js 387 | Utils.update_status("Created #{js_file}", { last: true }) 388 | FileUtils.copy("#{js_file}", @outfolder) 389 | FileUtils.copy("#{js_file}", @help_outfolder) 390 | end 391 | 392 | def read_html(html_file) 393 | f = File.open(html_file) 394 | doc = Nokogiri::HTML(f) 395 | f.close 396 | doc 397 | end 398 | 399 | def strip_html(input) 400 | CGI.unescapeHTML(input 401 | .gsub(%r{<(script|style|pre|code|figure).*?>.*?</\1>}im, '') 402 | .gsub(/<!--.*?-->/m, '') 403 | .gsub(/<(img|hr|br).*?>/i, ' ') 404 | .gsub(%r{<(dd|a|h\d|p|small|b|i|blockquote|li)( [^>]*?)?>(.*?)</\1>}i, ' \\3 ') 405 | .gsub(%r{</?(dt|a|ul|ol)( [^>]+)?>}i, ' ') 406 | .gsub(/<[^>]+?>/, '') 407 | .gsub(/\[\d+\]/, '') 408 | .gsub(/’/, "'").gsub(/&.*?;/, ' ').gsub(/;/, ' ') 409 | .gsub(/\u2028/, '') 410 | .gsub(/[^a-z0-9,"'?.! \n]/i, '')).strip.gsub(/[\t\n\r]+/, ' ').squeeze(' ') 411 | end 412 | 413 | def find_stems(content) 414 | keywords = [] 415 | words = content.gsub(/[^a-z0-9 ]/i, ' ').split(' ').map do |word| 416 | word.gsub(/^\W+|\W+$/, '').strip.downcase 417 | end 418 | words.delete_if do |word| 419 | word.length < 3 420 | end.delete_if { |word| @settings[:skipwords].include? word.gsub(/[^a-z0-9 ]/i, '').downcase } 421 | count = {} 422 | words = words.map { |word| Text::PorterStemming.stem(word).downcase }.uniq.each do |word| 423 | if count.key? word 424 | count[word] += 1 425 | else 426 | count[word] = 1 427 | end 428 | end 429 | 430 | count.delete_if { |_c, v| v < 3 } 431 | 432 | count.keys 433 | end 434 | 435 | def generate_full_index 436 | Utils.update_status('Generating full keyword index') 437 | version = @version 438 | contents = [] 439 | keywords = [] 440 | @config['Pages'].each do |page| 441 | keywords = [] 442 | page_title = page['title'] 443 | filename = page['file'] + '.html' 444 | doc = read_html(@outfolder + '/' + filename) 445 | keywords = find_stems(doc.css('#content').text) 446 | keywords.concat(page['keywords']) if page['keywords'] 447 | contents << { 448 | 'title' => page_title, 449 | 'link' => filename, 450 | 'keywords' => keywords 451 | } 452 | index_item = { 453 | 'title' => page_title, 454 | 'loc' => filename, 455 | 'tags' => keywords.join(' '), 456 | 'text' => strip_html(doc.css('#content').text).to_json, 457 | 'children' => [] 458 | } 459 | doc.css('#content h2,#content h3').each do |link| 460 | keywords = [] 461 | title_link = filename + '#' + link['id'] 462 | title = link.content 463 | keywords = find_stems(title) 464 | contents << { 465 | 'title' => (page_title + ': ' + title).strip, 466 | 'link' => title_link, 467 | 'keywords' => keywords 468 | } 469 | next unless link.name == 'h2' 470 | 471 | index_item['children'].push({ 472 | 'title' => title.strip, 473 | 'loc' => title_link 474 | }) 475 | end 476 | if index_item['children'].size > 0 477 | sections = '' 478 | index_item['children'].each do |c| 479 | sections += " #{c['title']}" 480 | end 481 | index_item['text'] = sections + ' ' + index_item['text'] 482 | end 483 | @searchindex.push(index_item) 484 | end 485 | 486 | contents = contents.sort do |a, b| 487 | a['title'] <=> b['title'] 488 | end 489 | 490 | output = %(<h1>Index</h1>\n<div class="uk-margin"><form action="search.html" class="uk-search uk-search-default"><input class="uk-search-input" type="search" placeholder="Keyword Filter" name="filter" id="topicsearch"></form></div>\n<ul id="topiclist">\n) 491 | 492 | contents.each do |item| 493 | output += "<li class=\"#{item['keywords'].join(' ')}\"><a href=\"#{item['link']}\">#{item['title']}</a></li>\n" 494 | end 495 | 496 | output += "</ul>\n\n" 497 | 498 | @subtitle = @main_title 499 | 500 | # infile = "#{@basefolder}/#{section_folder}/#{page["file"]}.md" 501 | # outfile = "#{@outfolder}/contents.html" 502 | @section_folder = '.' 503 | @title = 'Index' 504 | @content = output 505 | @sections = @index.gsub(%r{\.\./}, '') 506 | @prefix = '' 507 | @page_class = 'fullindex' 508 | @section_class = 'fullindex' 509 | output = ERB.new(Utils.load_template("#{@basefolder}/resources/template.html")).result(binding) 510 | help_output = ERB.new(Utils.load_template("#{@basefolder}/resources/template_internal.html")).result(binding) 511 | 512 | File.open("#{@outfolder}/contents.html", 'w+') do |out| 513 | out.puts(output) 514 | end 515 | File.open("#{@help_outfolder}/contents.html", 'w+') do |out| 516 | out.puts(help_output) 517 | end 518 | end 519 | 520 | def concat_js 521 | Utils.update_status('Concatenating JS') 522 | Dir.chdir(@basefolder) 523 | filename = @config['Title'].downcase.gsub(/[^a-z]/, '') 524 | concat_fn = filename + '.concat.js' 525 | minified_fn = filename + '.min.js' 526 | concat = File.new(concat_fn, 'w+') 527 | @config['Dependencies'].each do |file| 528 | next unless file =~ /\.js$/ 529 | 530 | File.open(File.join(@config['DependenciesBase'], file)).each do |line| 531 | concat.puts(line.chomp) 532 | end 533 | end 534 | concat.close 535 | output = `/usr/local/bin/yuicompressor --nomunge "#{concat_fn}"` 536 | jquery = IO.read('resources/jquery.min.js') 537 | File.open(minified_fn, 'w+') do |f| 538 | f.puts jquery 539 | f.puts output 540 | end 541 | minified_fn 542 | end 543 | end 544 | --------------------------------------------------------------------------------