├── .tool-versions ├── Gemfile ├── test ├── marked.gif ├── nvUltraIcon-400.png ├── taskpaper │ ├── Test2.md │ ├── Test3.md │ └── Test1.md ├── emoji.md ├── test_helper.rb ├── default_test.rb ├── headlines.md ├── del.md ├── sections.md ├── highlight.md ├── test.rb ├── transclude │ └── Marked2.6.311054-releasenotes.md ├── lists.md ├── lists2.md ├── emphasis.md ├── table.md ├── Upcoming.md ├── codeblocks.md ├── Byword.md └── riot-web.md ├── screenshots └── mdless.png ├── .gitignore ├── lib ├── mdless │ ├── version.rb │ ├── array.rb │ ├── hash.rb │ ├── tables.rb │ ├── taskpaper.rb │ ├── string.rb │ ├── theme.rb │ ├── colors.rb │ ├── converter.rb │ └── console.rb └── mdless.rb ├── .github └── FUNDING.yml ├── features ├── step_definitions │ └── mdless_steps.rb ├── mdless.feature └── support │ └── env.rb ├── .irbrc ├── bin └── mdless ├── pushgem.fish ├── LICENSE ├── mdless.gemspec ├── Rakefile ├── CHANGELOG.md └── README.md /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.3.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /test/marked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/mdless/HEAD/test/marked.gif -------------------------------------------------------------------------------- /screenshots/mdless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/mdless/HEAD/screenshots/mdless.png -------------------------------------------------------------------------------- /test/nvUltraIcon-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/mdless/HEAD/test/nvUltraIcon-400.png -------------------------------------------------------------------------------- /test/taskpaper/Test2.md: -------------------------------------------------------------------------------- 1 | Project: 2 | - Task1 3 | - Task2 @na 4 | - Task3 5 | Project2: 6 | -------------------------------------------------------------------------------- /test/emoji.md: -------------------------------------------------------------------------------- 1 | ## Emoji 2 | 3 | :smile: 4 | 5 | :umbrella: 6 | 7 | 8 | How about a :+1: or a :-1:? -------------------------------------------------------------------------------- /test/taskpaper/Test3.md: -------------------------------------------------------------------------------- 1 | 2 | 01. Project: 3 | - Task1 4 | - Task2 @na 5 | - Task3 6 | 01. Project2: 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | html 2 | pkg 3 | mdless.sublime-* 4 | *.taskpaper 5 | Gemfile.lock 6 | vendor 7 | .tool-versions 8 | -------------------------------------------------------------------------------- /lib/mdless/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CLIMarkdown 4 | VERSION = '2.1.62' 5 | end 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ttscoff] 2 | custom: ['https://brettterpstra.com/support/', 'https://brettterpstra.com/donate/'] 3 | -------------------------------------------------------------------------------- /test/taskpaper/Test1.md: -------------------------------------------------------------------------------- 1 | --- 2 | just: some 3 | meta: data 4 | --- 5 | 6 | Project: 7 | - Task1 8 | - Task2 @na 9 | - Task3 10 | 11 | Project2: 12 | -------------------------------------------------------------------------------- /features/step_definitions/mdless_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I get help for "([^"]*)"$/ do |app_name| 2 | @app_name = app_name 3 | step %(I run `#{app_name} help`) 4 | end 5 | 6 | # Add more step definitions here 7 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | # Add test libraries you want to use here, e.g. mocha 4 | 5 | class Test::Unit::TestCase 6 | 7 | # Add global extensions to the test case class here 8 | 9 | end 10 | -------------------------------------------------------------------------------- /.irbrc: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.join(__dir__, 'lib') 2 | require_relative 'lib/mdless' 3 | include CLIMarkdown 4 | config = File.expand_path('~/.config/mdless/config.yml') 5 | MDLess.options = YAML.load(IO.read(config)) if File.exist?(config) 6 | -------------------------------------------------------------------------------- /test/default_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DefaultTest < Test::Unit::TestCase 4 | 5 | def setup 6 | end 7 | 8 | def teardown 9 | end 10 | 11 | def test_the_truth 12 | assert true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /features/mdless.feature: -------------------------------------------------------------------------------- 1 | Feature: My bootstrapped app kinda works 2 | In order to get going on coding my awesome app 3 | I want to have aruba and cucumber setup 4 | So I don't have to do it myself 5 | 6 | Scenario: App just runs 7 | When I get help for "mdless" 8 | Then the exit status should be 0 9 | -------------------------------------------------------------------------------- /lib/mdless/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ::Array 4 | def longest_element 5 | return self if nil? || empty? 6 | 7 | group_by(&:size).max.last[0] 8 | end 9 | 10 | def longest_elements 11 | return [] if nil? || empty? 12 | 13 | group_by(&:size).max.last 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mdless/hash.rb: -------------------------------------------------------------------------------- 1 | module CLIMarkdown 2 | # Hash helpers 3 | class ::Hash 4 | def deep_merge(second) 5 | merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : Array === v1 && Array === v2 ? v1 | v2 : [:undefined, nil, :nil].include?(v2) ? v1 : v2 } 6 | self.merge(second ? second.to_h : second, &merger) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/headlines.md: -------------------------------------------------------------------------------- 1 | # No monkey business 2 | 3 | ## Headline (with parens) 4 | 5 | ## Headline no parens 6 | 7 | ### Headline with a question mark? 8 | 9 | ### Ok, how about an asterisk* 10 | 11 | ## Headline with none of the above 12 | 13 | Setex header 1 14 | ============== 15 | 16 | Or is it? 17 | ========= 18 | 19 | Setex header 2 20 | -------------- 21 | 22 | (it is...) 23 | ---------- 24 | -------------------------------------------------------------------------------- /test/del.md: -------------------------------------------------------------------------------- 1 | ## Strikethrough 2 | 3 | Lorem ipsum dolor sit amet, consectetur ~~adipisicing elit~~, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ~~ullamco laboris~~ nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 4 | 5 | ## ~~Headline~~ -------------------------------------------------------------------------------- /bin/mdless: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'mdless' 5 | 6 | def class_exists?(class_name) 7 | klass = Module.const_get(class_name) 8 | klass.is_a?(Class) 9 | rescue NameError 10 | false 11 | end 12 | 13 | if class_exists? 'Encoding' 14 | Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external') 15 | Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal') 16 | end 17 | 18 | CLIMarkdown::Converter.new(ARGV) 19 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'aruba/cucumber' 2 | 3 | ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}" 4 | LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib') 5 | 6 | Before do 7 | # Using "announce" causes massive warnings on 1.9.2 8 | @puts = true 9 | @original_rubylib = ENV['RUBYLIB'] 10 | ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s 11 | end 12 | 13 | After do 14 | ENV['RUBYLIB'] = @original_rubylib 15 | end 16 | -------------------------------------------------------------------------------- /test/sections.md: -------------------------------------------------------------------------------- 1 | # Testing sections 2 | 3 | ## This is section 1 4 | 5 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 6 | 7 | ## This is section 2 8 | 9 | It has a lot less content. 10 | 11 | ### This is section 2b 12 | 13 | Well, neato, it works. 14 | 15 | ## This is section 3 16 | 17 | When will it end? 18 | -------------------------------------------------------------------------------- /pushgem.fish: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/fish 2 | 3 | set -l ver (rake bump[inc]) 4 | changelog -f git >current_changelog.md 5 | changelog -u 6 | git commit -a -F current_changelog.md 7 | git push 8 | rake clobber package 9 | GEM_HOST_OTP_CODE=$(op item get RubyGems --fields type=OTP --format json | jq -r .totp) gem push pkg/mdless-$ver.gem 10 | # hub release create -m "v$ver" $ver 11 | git pull 12 | git flow release start $ver 13 | git flow release finish -m "v$ver" $ver 14 | FORCE_PUSH=true git push --all 15 | FORCE_PUSH=true git push --tags 16 | gh release create $ver --title "$ver" -F current_changelog.md 17 | git pull 18 | git push 19 | git checkout $(git config --get gitflow.branch.develop) 20 | rm current_changelog.md 21 | -------------------------------------------------------------------------------- /test/highlight.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Highlighter test 3 | --- 4 | ## Highlighter ==highlight== header 5 | 6 | This is a ==highlight inside== a paragraph. 7 | 8 | - This is a ==highlight== in the middle of a list item 9 | 10 | * Make sure you have Chrome installed (a recent version, like 59) 11 | * Make sure you have `matrix-js-sdk` and `matrix-react-sdk` installed and built, as above 12 | * `yarn test` 13 | 14 | We do not recommend running Riot from the same domain name as your Matrix homeserver. The reason is the risk of XSS 15 | (cross-site-scripting) vulnerabilities that could occur if someone caused Riot to load and render malicious [==user-generated content==](https://brettterpstra.com) from a Matrix API which then had trusted access to Riot (or other apps) due to sharing the same 16 | domain. 17 | 18 | -------------------------------------------------------------------------------- /test/test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | def convert_markdown(input) 3 | @headers = get_headers(input) 4 | # yaml/MMD headers 5 | in_yaml = false 6 | if input.split("\n")[0] =~ /(?i-m)^---[ \t]*?(\n|$)/ 7 | @log.info("Found YAML") 8 | # YAML 9 | in_yaml = true 10 | input.sub!(/(?i-m)^---[ \t]*\n([\s\S]*?)\n[\-.]{3}[ \t]*\n/) do |yaml| 11 | m = Regexp.last_match 12 | 13 | @log.info("Processing YAML Header") 14 | m[0].split(/\n/).map {|line| 15 | if line =~ /^[\-.]{3}\s*$/ 16 | line = c([:d,:black,:on_black]) + "% " + c([:d,:black,:on_black]) + line 17 | else 18 | line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2') 19 | line = c([:d,:black,:on_black]) + "% " + c([:d,:white]) + line 20 | end 21 | if @cols - line.uncolor.size > 0 22 | line += " "*(@cols-line.uncolor.size) 23 | end 24 | }.join("\n") + "#{xc}\n" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/transclude/Marked2.6.311054-releasenotes.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Marked 2.6.31 (1054) 6 | 7 | > Quick fix for blank CriticMarkup previews 8 | 9 | Follow [@markedapp on Twitter](https://twitter.com/markedapp) or [@marked@indieapps.space on Mastodon](https://indieapps.space/@marked) for non-automatic-update updates. You know, "news." (Mastodon account is more active, if you're making a choice.) 10 | 11 | #### FIXED 12 | 13 | - Blank CriticMarkup preview when python is installed in /opt/homebrew/bin 14 | 15 | See the [full changelog](http://marked2app.com/help/changelog.html) for all the recent updates. 16 | 17 | 18 | And now, because you read all the way through the release notes, a special message just for you. ↓ 19 | 20 | --- 21 | 22 | ## Keep Going... ↓ 23 | 24 | --- 25 | 26 | 27 | ![][cheery] 28 | 29 | # Enjoy! 30 | 31 | [cheery]: http://asssets.markedapp.com.s3.amazonaws.com/puppiesAndKittens.png style="width:100%;height:auto;" 32 | 33 | -------------------------------------------------------------------------------- /test/lists.md: -------------------------------------------------------------------------------- 1 | transclude base: test/transclude 2 | title: Lists with transclude 3 | 4 | {{Marked2.6.311054-releasenotes.md}} 5 | 6 | # [%title] 7 | 8 | 1. delete line feeds (\u2028) 9 | 2. Trim content to first line (up to \r\n) 10 | 3. read into array 11 | 4. delete elements ==without== tags keys 12 | 5. modify content values 13 | - remove colons, pipes and _slashes_ 14 | - truncate to 40 chars to create title 15 | 6. If content value + ".txt" exists, apply OS tags and/or insert metadata line (shouldn't need to respect any existing metadata) 16 | 17 | This contains a paragraph 18 | 19 | Maybe two 20 | 7. This contains 21 | 1. ordered list 22 | 2. items 23 | 1. Further nested 24 | 25 | ```ruby 26 | def code_block 27 | puts "hello world" 28 | end 29 | 30 | def second 31 | puts "goodbye world" 32 | end 33 | ``` 34 | 2. And again 35 | 8. End of list 36 | 37 | --- 38 | 39 | 1. This should 40 | 2. start a new 41 | 3. list 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Brett Terpstra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mdless.gemspec: -------------------------------------------------------------------------------- 1 | # Ensure we require the local version and not one we might have installed already 2 | require './lib/mdless/version.rb' 3 | spec = Gem::Specification.new do |s| 4 | s.name = 'mdless' 5 | s.version = CLIMarkdown::VERSION 6 | s.author = 'Brett Terpstra' 7 | s.email = 'me@brettterpstra.com' 8 | s.homepage = 'http://brettterpstra.com/project/mdless/' 9 | s.platform = Gem::Platform::RUBY 10 | s.summary = 'A pager like less, but for Markdown files' 11 | s.description = 'A CLI that provides a formatted and highlighted view of Markdown files in a terminal' 12 | s.license = 'MIT' 13 | s.files = Dir['lib/**/*.rb'] + Dir['bin/*'] 14 | s.files << 'CHANGELOG.md' 15 | s.files << 'README.md' 16 | s.require_paths << 'lib' 17 | s.extra_rdoc_files = ['README.md'] 18 | s.rdoc_options << '--title' << 'mdless' << '--main' << 'README.md' << '--markup' << 'markdown' << '-ri' 19 | s.bindir = 'bin' 20 | s.executables << 'mdless' 21 | s.add_dependency 'redcarpet', '~> 3.6' 22 | s.add_dependency 'rouge', '~> 4.2' 23 | s.add_dependency 'tty-screen', '~> 0.8' 24 | s.add_dependency 'tty-spinner', '~> 0.8' 25 | s.add_dependency 'tty-which', '~> 0.5' 26 | s.add_development_dependency 'rake', '~> 13' 27 | s.add_development_dependency 'rdoc', '>= 6.6.2' 28 | s.add_development_dependency 'rubocop', '~> 0.49' 29 | end 30 | -------------------------------------------------------------------------------- /test/lists2.md: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 2 | 3 | - level 1 4 | * level 2 5 | 6 | ```ruby 7 | def start() end 8 | ``` 9 | 10 | + level 3 11 | - level 4 12 | * level 2 13 | + level 3 14 | - level 4 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 17 | 18 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 19 | 20 | - level 5 21 | - level 4 22 | * numbered lists 23 | 1. numbered 1 24 | 1. Numbered 2 25 | 2. numbered 1 26 | 3. numbered 1 27 | * bullet 28 | * bullet 29 | * longer line Lorem ipsum dolor sit amet, consectetur adipisicing elit 30 | - level 1 31 | -------------------------------------------------------------------------------- /test/emphasis.md: -------------------------------------------------------------------------------- 1 | 1. “*blah*” 2 | 1. *“blah”* 3 | 1. *blah “blah”* 4 | 1. *“blah” blah* 5 | 1. (*“blah”*) 6 | 1. (*“blah”* blah) 7 | 1. (blah *“blah”*) 8 | 1. (“*blah*”) 9 | 1. (“*blah*” blah) 10 | 1. (blah “*blah*”) 11 | 1. "*blah*" 12 | 1. *"blah"* 13 | 1. *blah "blah"* 14 | 1. *"blah" blah* 15 | 1. (*"blah"*) 16 | 1. (*"blah"* blah) 17 | 1. (blah *"blah"*) 18 | 1. ("*blah*") 19 | 1. ("*blah*" blah) 20 | 1. (blah "*blah*") 21 | 1. “**blah**” 22 | 1. **“blah”** 23 | 1. _**blah “blah”**_ 24 | 1. **“blah” blah** 25 | 1. (**“blah”**) 26 | 1. (**“blah”** blah) 27 | 1. (blah **“blah”**) 28 | 1. (“**blah**”) 29 | 1. (“**blah**” blah) 30 | 1. (blah “**blah**”) 31 | 1. "**blah**" 32 | 1. **"blah"** 33 | 1. **blah "blah"** 34 | 1. **"blah" blah** 35 | 1. (**"blah"**) 36 | 1. (**"blah"** blah) 37 | 1. (blah **"blah"**) 38 | 1. ("**blah**") 39 | 1. ("**blah**" blah) 40 | 1. (blah "**blah**") 41 | 1. “_blah_” 42 | 1. _“blah”_ 43 | 1. _blah “blah”_ 44 | 1. _“blah” blah_ 45 | 1. (_“blah”_) 46 | 1. (_“blah”_ blah) 47 | 1. (blah _“blah”_) 48 | 1. (“_blah_”) 49 | 1. (“_blah_” blah) 50 | 1. (blah “_blah_”) 51 | 1. "_blah_" 52 | 1. _"blah"_ 53 | 1. _blah "blah"_ 54 | 1. _"blah" blah_ 55 | 1. (_"blah"_) 56 | 1. (_"blah"_ blah) 57 | 1. (blah _"blah"_) 58 | 1. ("_blah_") 59 | 1. ("_blah_" blah) 60 | 1. (blah "_blah_") 61 | 1. “__blah__” 62 | 1. __“blah”__ 63 | 1. __blah “blah”__ 64 | 1. __“blah” blah__ 65 | 1. (__“blah”__) 66 | 1. (__“blah”__ blah) 67 | 1. (blah __“blah”__) 68 | 1. (“__blah__”) 69 | 1. (“__blah__” blah) 70 | 1. (blah “__blah__”) 71 | 1. "__blah__" 72 | 1. __"blah"__ 73 | 1. __blah "blah"__ 74 | 1. __"blah" blah__ 75 | 1. (__"blah"__) 76 | 1. (__"blah"__ blah) 77 | 1. (blah __"blah"__) 78 | 1. ("__blah__") 79 | 1. ("__blah__" blah) 80 | 1. (blah "__blah__") 81 | -------------------------------------------------------------------------------- /lib/mdless.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | require 'logger' 5 | require 'open3' 6 | require 'optparse' 7 | require 'shellwords' 8 | 9 | require 'redcarpet' 10 | require 'rouge' 11 | require 'tty-screen' 12 | require 'tty-spinner' 13 | require 'tty-which' 14 | 15 | require_relative 'mdless/theme' 16 | require_relative 'mdless/array' 17 | require_relative 'mdless/colors' 18 | require_relative 'mdless/converter' 19 | require_relative 'mdless/hash' 20 | require_relative 'mdless/string' 21 | require_relative 'mdless/tables' 22 | require_relative 'mdless/taskpaper' 23 | require_relative 'mdless/version' 24 | require_relative 'mdless/console' 25 | require_relative 'mdless/emoji' 26 | module CLIMarkdown 27 | EXECUTABLE_NAME = 'mdless' 28 | end 29 | 30 | module MDLess 31 | class << self 32 | include CLIMarkdown::Theme 33 | attr_accessor :options, :cols, :file, :meta 34 | 35 | def log 36 | @log ||= Logger.new($stderr) 37 | end 38 | 39 | def log_level(level) 40 | @log.level = level 41 | end 42 | 43 | def theme 44 | @theme ||= load_theme(@options[:theme]) 45 | end 46 | 47 | def pygments_styles 48 | @pygments_styles ||= read_pygments_styles 49 | end 50 | 51 | def pygments_lexers 52 | @pygments_lexers ||= read_pygments_lexers 53 | end 54 | 55 | def read_pygments_styles 56 | MDLess.log.info 'Reading Pygments styles' 57 | pyg = TTY::Which.which('pygmentize') 58 | res = `#{pyg} -L styles` 59 | res.scan(/\* ([\w-]+):/).map { |l| l[0] } 60 | end 61 | 62 | def read_pygments_lexers 63 | MDLess.log.info 'Reading Pygments lexers' 64 | pyg = TTY::Which.which('pygmentize') 65 | res = `#{pyg} -L lexers` 66 | lexers = res.scan(/\* ([\w-]+(?:, [\w-]+)*):/).map { |l| l[0] } 67 | lexers_a = [] 68 | lexers.each { |l| lexers_a.concat(l.split(/, /)) } 69 | lexers_a 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/clean' 2 | require 'rubygems' 3 | require 'rubygems/package_task' 4 | require 'rdoc' 5 | require 'rdoc/task' 6 | 7 | 8 | Rake::RDocTask.new do |rd| 9 | rd.main = "README.md" 10 | rd.rdoc_files.include("README.md","lib/**/*.rb","bin/**/*") 11 | rd.title = 'mdless' 12 | rd.markup = 'markdown' 13 | end 14 | 15 | spec = eval(File.read('mdless.gemspec')) 16 | 17 | Gem::PackageTask.new(spec) do |pkg| 18 | end 19 | 20 | desc 'Install the gem in the current ruby' 21 | task :install, :all do |t, args| 22 | args.with_defaults(:all => false) 23 | if args[:all] 24 | sh 'rvm all do gem install pkg/*.gem' 25 | sh 'sudo gem install pkg/*.gem' 26 | else 27 | sh 'gem install pkg/*.gem' 28 | end 29 | end 30 | 31 | desc 'Development version check' 32 | task :ver do 33 | version_file = 'lib/mdless/version.rb' 34 | content = IO.read(version_file) 35 | m = content.match(/VERSION *= *(?['"])(?\d+)\.(?\d+)\.(?\d+)(?
\S+)?\k/)
36 |   puts "#{m['maj']}.#{m['min']}.#{m['pat']}#{m['pre']}"
37 | end
38 | 
39 | desc 'Bump incremental version number'
40 | task :bump, :type do |t, args|
41 |   args.with_defaults(type: 'inc')
42 |   version_file = 'lib/mdless/version.rb'
43 |   content = IO.read(version_file)
44 |   content.sub!(/VERSION *= *(?['"])(?\d+)\.(?\d+)\.(?\d+)(?
\S+)?\k/) do
45 |     m = Regexp.last_match
46 |     major = m['maj'].to_i
47 |     minor = m['min'].to_i
48 |     inc = m['pat'].to_i
49 |     pre = m['pre']
50 | 
51 |     case args[:type]
52 |     when /^maj/
53 |       major += 1
54 |       minor = 0
55 |       inc = 0
56 |     when /^min/
57 |       minor += 1
58 |       inc = 0
59 |     else
60 |       inc += 1
61 |     end
62 | 
63 |     $stdout.print "#{major}.#{minor}.#{inc}#{pre}"
64 |     "VERSION = '#{major}.#{minor}.#{inc}#{pre}'"
65 |   end
66 | 
67 |   File.open(version_file, 'w+') { |f| f.puts content }
68 | end
69 | 
70 | task default: %i[test features]
71 | task build: %i[clobber rdoc package]
72 | 


--------------------------------------------------------------------------------
/test/table.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: Table test
 3 | ---
 4 | 
 5 | This is a ==nifty== little table
 6 | 
 7 | | Just                 | a      |  header |     row      |
 8 | | :------------------- | ------ | ------: | :----------: |
 9 | | a ==something== long | short  |  medium |              |
10 | | a                    | little |    more | data for you |
11 | | an empty row         |        |         |              |
12 | | this                 | should | benefit | all humanity |
13 | | where would          | I be   | without |     you?     |
14 | 
15 | Here's another table with fewer columns
16 | 
17 | | Key   | Value  |
18 | | :---- | :----- |
19 | | name  | action |
20 | | other | row    |
21 | 
22 | | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod |   tempor incididunt ut labore | et dolore magna aliqua. Ut enim ad minim veniam,   |
23 | | :----------------------------------------------------------------------- | ----------------------------: | :------------------------------------------------- |
24 | | quis nostrud exercitation                                                |          ullamco laboris nisi | ut aliquip ex ea commodo                           |
25 | | consequat. Duis aute irure dolor                                         | in reprehenderit in voluptate | velit esse cillum dolore eu fugiat nulla pariatur. |
26 | 
27 | | Table containing  | links                                                 | and stuff     |
28 | | :---------------- | :---------------------------------------------------- | :------------ |
29 | | Here's a name     | And an  email                   | link to see   |
30 | | How it renders    | Other one                      | how we doing? |
31 | | Does it matter    |  if there's                    | text after?   |
32 | | Maybe it's before | before                        | text before?  |
33 | | Maybe both?       | this  might                   | work better?  |
34 | | yeah              | bo  th                                     | works?        |
35 | | What about        | [non email](https://brettterpstra.com)                | links?        |
36 | | What about        | [non email](https://brettterpstra.com) with text      | links?        |
37 | | What about        | text before [non email](https://brettterpstra.com)    | links?        |
38 | | What about        | both [non email](https://brettterpstra.com) with text | links?        |
39 | [Emails]
40 | 


--------------------------------------------------------------------------------
/test/Upcoming.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: just a title thing
 3 | date: 2023-12-15 08:00
 4 | ---
 5 | 
 6 | | App | slug | date | drawing |
 7 | |:--|:--|:--|:--|
 8 | SpamSieve | spamsieve2023 | 2023-12-11 07:00:00 -0600 | 2023-12-15 07:00:00 -0600
 9 | App Tamer | apptamer2023 | 2023-12-18 07:00:00 -0600 | 2023-12-22 07:00:00 -0600
10 | Kaleidoscope | kaleidoscope2023 | 2023-12-25 07:00:00 -0600 | 2023-12-29 07:00:00 -0600
11 | Curio | curio2023 | 2024-01-01 07:00:00 -0600 | 2024-01-05 07:00:00 -0600
12 | Keyboard Maestro | keyboardmaestro2024 | 2024-01-08 07:00:00 -0600 | 2024-01-12 07:00:00 -0600
13 | Bartender | bartender2024 | 2024-01-15 07:00:00 -0600 | 2024-01-19 07:00:00 -0600
14 | OmniFocus | omnifocus2024 | 2024-01-22 07:00:00 -0600 | 2024-01-26 07:00:00 -0600
15 | MarsEdit | marsedit2024 | 2024-01-29 07:00:00 -0600 | 2024-02-02 07:00:00 -0600
16 | FastScripts | fastscripts2024 | 2024-02-05 07:00:00 -0600 | 2024-02-09 07:00:00 -0600
17 | TaskPaper | taskpaper2024 | 2024-02-12 07:00:00 -0600 | 2024-02-16 07:00:00 -0600
18 | Black Ink | blackink2024 | 2024-02-19 07:00:00 -0600 | 2024-02-23 07:00:00 -0600
19 | Things for Mac | things2024 | 2024-02-26 07:00:00 -0600 | 2024-03-01 07:00:00 -0600
20 | Bike | bike2024 | 2024-03-04 07:00:00 -0600 | 2024-03-08 07:00:00 -0600
21 | Flexibits Premium | flexibits2024 | 2024-03-11 08:00:00 -0500 | 2024-03-15 08:00:00 -0500
22 | EagleFiler | eaglefiler2024 | 2024-03-18 08:00:00 -0500 | 2024-03-22 08:00:00 -0500
23 | The Archive | thearchive2024 | 2024-03-25 08:00:00 -0500 | 2024-03-29 08:00:00 -0500
24 | Unite 5 | unite2024 | 2024-04-01 08:00:00 -0500 | 2024-04-05 08:00:00 -0500
25 | Bear | bear2024 | 2024-04-08 08:00:00 -0500 | 2024-04-12 08:00:00 -0500
26 | Acorn | acorn2024 | 2024-04-15 08:00:00 -0500 | 2024-04-19 08:00:00 -0500
27 | OmniGraffle | omnigraffle2024 | 2024-04-22 08:00:00 -0500 | 2024-04-26 08:00:00 -0500
28 | MacUpdater | macupdater2024 | 2024-04-29 08:00:00 -0500 | 2024-05-03 08:00:00 -0500
29 | BBEdit | bbedit2024 | 2024-05-06 08:00:00 -0500 | 2024-05-10 08:00:00 -0500
30 | DEVONthink | devonthink2024 | 2024-05-13 08:00:00 -0500 | 2024-05-17 08:00:00 -0500
31 | iThoughtsX | ithoughtsx2024 | 2024-05-20 08:00:00 -0500 | 2024-05-24 08:00:00 -0500
32 | RetroBatch Pro | retrobatch2024 | 2024-05-27 08:00:00 -0500 | 2024-05-31 08:00:00 -0500
33 | Audio Hijack | audiohijack2024 | 2024-06-03 08:00:00 -0500 | 2024-06-07 08:00:00 -0500
34 | OmniOutliner | omnioutliner2024 | 2024-06-10 08:00:00 -0500 | 2024-06-14 08:00:00 -0500
35 | Marked | marked2024 | 2024-06-17 08:00:00 -0500 | 2024-06-21 08:00:00 -0500
36 | Dropzone | dropzone2024 | 2024-06-24 08:00:00 -0500 | 2024-06-28 08:00:00 -0500
37 | TableFlip | tableflip2024 | 2024-07-01 08:00:00 -0500 | 2024-07-05 08:00:00 -0500
38 | Backblaze | backblaze2024 | 2024-07-08 08:00:00 -0500 | 2024-07-12 08:00:00 -0500
39 | PopClip | popclip2024 | 2024-07-15 08:00:00 -0500 | 2024-07-19 08:00:00 -0500
40 | TextBuddy | textbuddy2024 | 2024-07-22 08:00:00 -0500 | 2024-07-26 08:00:00 -0500
41 | SoundSource | soundsource2024 | 2024-07-29 08:00:00 -0500 | 2024-08-02 08:00:00 -0500
42 | iStat Menus | istatmenus2024 | 2024-08-05 08:00:00 -0500 | 2024-08-09 08:00:00 -0500
43 | Obidian Sync | obsidian2024 | 2024-08-12 08:00:00 -0500 | 2024-08-16 08:00:00 -0500
44 | Morpho Converter Pro | morpho2024 | 2024-08-19 08:00:00 -0500 | 2024-08-23 08:00:00 -0500
45 | OmniPlan | omniplan2024 | 2024-08-26 08:00:00 -0500 | 2024-08-30 08:00:00 -0500
46 | 


--------------------------------------------------------------------------------
/lib/mdless/tables.rb:
--------------------------------------------------------------------------------
  1 | module CLIMarkdown
  2 |   class MDTableCleanup
  3 |     PAD_CHAR = "\u00A0"
  4 | 
  5 |     def initialize(input)
  6 |       @string = input
  7 |     end
  8 | 
  9 |     def parse
 10 |       @format_row = []
 11 |       @table = []
 12 |       fmt = []
 13 |       cols = 0
 14 |       rows = @string.split(/\r?\n/)
 15 |       rows.each do |row|
 16 |         row.strip!
 17 |         row.sub!(/^\s*\|?/, "").sub!(/\|?\s*$/, "")
 18 |         row_array = row.split(/\|/)
 19 |         row_array.map! { |cell| cell.strip }
 20 |         if row =~ /^[\|:\- ]+$/
 21 |           fmt = row_array
 22 |         else
 23 |           @table.push row_array
 24 |         end
 25 |         cols = row_array.length if row_array.length > cols
 26 |       end
 27 | 
 28 |       fmt.each_with_index do |cell, i|
 29 |         cell.strip!
 30 |         f = case cell
 31 |           when /^:.*?:$/
 32 |             :center
 33 |           when /[^:]+:$/
 34 |             :right
 35 |           else
 36 |             :just
 37 |           end
 38 |         @format_row.push(f)
 39 |       end
 40 | 
 41 |       if @format_row.length < cols
 42 |         (cols - @format_row.length).times do
 43 |           @format_row.push(:left)
 44 |         end
 45 |       end
 46 | 
 47 |       @table.map! do |row|
 48 |         if row.length < cols
 49 |           (cols - row.length).times do
 50 |             row.push("")
 51 |           end
 52 |         end
 53 |         row
 54 |       end
 55 |       @table
 56 |     end
 57 | 
 58 |     def table
 59 |       @table ||= parse
 60 |     end
 61 | 
 62 |     def column_width(idx)
 63 |       @widths ||= column_widths
 64 |       @widths[idx]
 65 |     end
 66 | 
 67 |     def column_widths
 68 |       @widths = []
 69 |       @format_row.length.times do
 70 |         @widths.push(0)
 71 |       end
 72 | 
 73 |       table.each do |row|
 74 |         @format_row.each_with_index do |_, i|
 75 |           length = row[i].uncolor.remove_pre_post.strip.length
 76 |           @widths[i] = length if length > @widths[i]
 77 |         end
 78 |       end
 79 | 
 80 |       @widths
 81 |     end
 82 | 
 83 |     def pad(string, alignment, length)
 84 |       naked = string.uncolor.remove_pre_post.strip
 85 |       case alignment
 86 |       when :center
 87 |         naked.strip.center(length, PAD_CHAR).sub(/#{Regexp.escape(naked)}/, string)
 88 |       when :right
 89 |         naked.strip.rjust(length, PAD_CHAR).sub(/#{Regexp.escape(naked)}/, string)
 90 |       when :left
 91 |         naked.strip.ljust(length, PAD_CHAR).sub(/#{Regexp.escape(naked)}/, string)
 92 |       else
 93 |         naked.strip.ljust(length, PAD_CHAR).sub(/#{Regexp.escape(naked)}/, string)
 94 |       end
 95 |     end
 96 | 
 97 |     def separator(length, alignment)
 98 |       out = "".ljust(length, "-")
 99 |       case alignment
100 |       when :left
101 |         ":#{out}-"
102 |       when :right
103 |         "-#{out}:"
104 |       when :center
105 |         ":#{out}:"
106 |       else
107 |         "-#{out}-"
108 |       end
109 |     end
110 | 
111 |     def header_separator_row
112 |       output = []
113 |       @format_row.each_with_index do |column, i|
114 |         output.push separator(column_width(i), column)
115 |       end
116 |       "|#{output.join("|")}|"
117 |     end
118 | 
119 |     def table_border
120 |       output = []
121 |       @format_row.each_with_index do |column, i|
122 |         output.push separator(column_width(i), column)
123 |       end
124 |       "+#{output.join("+").gsub(/:/, "-")}+"
125 |     end
126 | 
127 |     def to_md
128 |       output = []
129 |       t = table.clone
130 |       t.each do |row|
131 |         new_row = row.map.with_index { |cell, i| pad(cell, @format_row[i], column_width(i)) }.join(" | ")
132 |         output.push("| #{new_row} |")
133 |       end
134 |       output.insert(1, header_separator_row)
135 |       output.insert(0, table_border)
136 |       output.push(table_border)
137 |       output.join("\n")
138 |     end
139 |   end
140 | end
141 | 


--------------------------------------------------------------------------------
/lib/mdless/taskpaper.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | module CLIMarkdown
  4 |   module TaskPaper
  5 |     TASK_RX = /^(?(?:    |\t)*?)(?-)(?\s+\S.*?)$/
  6 |     PROJECT_RX = /^(?(?:    |\t)*?)(?[^- \t].*?:)(? +@\S+)*$/
  7 |     NOTE_RX = /^(?(?:    |\t)+)(?(?]/)
 16 |         if MDLess.theme.key?(keys[0])
 17 |           val = MDLess.theme[keys.shift]
 18 |         else
 19 |           @log.error("Invalid theme key: #{key}") unless keys[0] =~ /^text/
 20 |           return c([:reset])
 21 |         end
 22 |         keys.each do |k|
 23 |           if val.key?(k)
 24 |             val = val[k]
 25 |           else
 26 |             @log.error("Invalid theme key: #{k}")
 27 |             return c([:reset])
 28 |           end
 29 |         end
 30 |         if val.is_a? String
 31 |           val = "x #{val}"
 32 |           res = val.split(/ /).map(&:to_sym)
 33 |           c(res)
 34 |         else
 35 |           c([:reset])
 36 |         end
 37 |       end
 38 | 
 39 |       def is_taskpaper?(input)
 40 |         return true if MDLess.file =~ /\.taskpaper$/
 41 | 
 42 |         projects = sections(input)
 43 | 
 44 |         tasks = 0
 45 |         if projects.count > 1
 46 |           projects.each do |proj, content|
 47 |             tasks += content['content'].scan(TASK_RX).count
 48 |           end
 49 |         end
 50 | 
 51 |         if tasks >= 6
 52 |           return true
 53 |         else
 54 |           tst = input.dup.remove_meta
 55 |           tst = tst.gsub(PROJECT_RX, '')
 56 |           tst = tst.gsub(TASK_RX, '')
 57 |           tst = tst.gsub(NOTE_RX, '')
 58 |           tst = tst.gsub(/^ *\n$/, '')
 59 |           return tst.strip.length == 0
 60 |         end
 61 |       end
 62 | 
 63 |       def section(input, string)
 64 |         sects = sections(input)
 65 |         sects_to_s(sects.filter { |k, _| k.downcase =~ string.downcase.to_rx })
 66 |       end
 67 | 
 68 |       def sects_to_s(sects)
 69 |         sects.map do |k, v|
 70 |           "#{k}#{v['content']}"
 71 |         end.join("\n")
 72 |       end
 73 | 
 74 |       def indent(input, idnt)
 75 |         input.split(/\n/).map do |line|
 76 |           line.sub(/^#{idnt}/, '')
 77 |         end.join("\n")
 78 |       end
 79 | 
 80 |       def sections(input)
 81 |         heirarchy = {}
 82 |         sects = input.to_enum(:scan, /(?mix)
 83 |                                       (?<=\n|\A)(?(?:    |\t)*?)
 84 |                                       (?[^- \t\n].*?:)\s*(?=\n)
 85 |                                       (?.*?)
 86 |                                       (?=\n\k\S.*?:|\Z)$/).map { Regexp.last_match }
 87 |         sects.each do |sect|
 88 |           heirarchy[sect['project']] = {}
 89 |           heirarchy[sect['project']]['content'] = indent(sect['content'], sect['indent'])
 90 |           heirarchy = heirarchy.merge(sections(sect['content']))
 91 |         end
 92 | 
 93 |         heirarchy
 94 |       end
 95 | 
 96 |       def list_projects(input)
 97 |         projects = input.to_enum(:scan, PROJECT_RX).map { Regexp.last_match }
 98 |         projects.delete_if { |proj| proj['project'] =~ /^[ \n]*$/ }
 99 |         projects.map! { |proj| "#{color('taskpaper marker')}#{proj['indent']}- #{color('taskpaper project')}#{proj['project'].sub(/:$/, '')}" }
100 |         projects.join("\n")
101 |       end
102 | 
103 |       def highlight(input)
104 |         mc = color('taskpaper marker')
105 |         tc = color('taskpaper task')
106 |         pc = color('taskpaper project')
107 |         nc = color('taskpaper note')
108 | 
109 |         if MDLess.options[:section]
110 |           matches = []
111 |           MDLess.options[:section].each do |sect|
112 |             matches << section(input, sect)
113 |           end
114 |           input = matches.join("\n")
115 |         end
116 | 
117 |         input.gsub!(/\t/, '    ')
118 | 
119 |         input.gsub!(PROJECT_RX) do
120 |           m = Regexp.last_match
121 |           "#{m['indent']}#{pc}#{m['project']}#{m['tags']}"
122 |         end
123 | 
124 |         input.gsub!(TASK_RX) do
125 |           m = Regexp.last_match
126 |           "#{m['indent']}#{mc}- #{tc}#{m['task']}"
127 |         end
128 | 
129 |         input.gsub!(NOTE_RX) do
130 |           m = Regexp.last_match
131 |           "#{m['indent']}#{nc}#{m['note']}"
132 |         end
133 | 
134 |         input
135 |       end
136 |     end
137 |   end
138 | end
139 | 


--------------------------------------------------------------------------------
/lib/mdless/string.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | # String helpers
  4 | class ::String
  5 |   include CLIMarkdown::Colors
  6 | 
  7 |   def clean_empty_lines
  8 |     gsub(/^[ \t]+$/, '')
  9 |   end
 10 | 
 11 |   def clean_empty_lines!
 12 |     replace clean_empty_lines
 13 |   end
 14 | 
 15 |   def color(key)
 16 |     val = nil
 17 |     keys = key.split(/[ ,>]/)
 18 |     if MDLess.theme.key?(keys[0])
 19 |       val = MDLess.theme[keys.shift]
 20 |     else
 21 |       MDLess.log.error("Invalid theme key!: #{key}") unless keys[0] =~ /^text/
 22 |       return c([:reset])
 23 |     end
 24 |     keys.each do |k|
 25 |       if val.key?(k)
 26 |         val = val[k]
 27 |       else
 28 |         MDLess.log.error("Invalid theme key@: #{k}")
 29 |         return c([:reset])
 30 |       end
 31 |     end
 32 |     if val.is_a? String
 33 |       val = "x #{val}"
 34 |       res = val.split(/ /).map(&:to_sym)
 35 |       c(res)
 36 |     else
 37 |       c([:reset])
 38 |     end
 39 |   end
 40 | 
 41 |   def to_rx(distance: 2, string_start: false)
 42 |     chars = downcase.split(//)
 43 |     pre = string_start ? '^' : '^.*?'
 44 |     /#{pre}#{chars.join(".{,#{distance}}")}.*?$/
 45 |   end
 46 | 
 47 |   def clean_header_ids!
 48 |     replace clean_header_ids
 49 |   end
 50 | 
 51 |   def clean_header_ids
 52 |     gsub(/ +\[.*?\] *$/, '').gsub(/ *\{#.*?\} *$/, '').strip
 53 |   end
 54 | 
 55 |   def color_meta(cols)
 56 |     @cols = cols
 57 |     input = dup
 58 |     input.clean_empty_lines!
 59 |     MDLess.meta = {}
 60 | 
 61 |     in_yaml = false
 62 |     first_line = input.split("\n").first
 63 |     if first_line =~ /(?i-m)^---[ \t]*?$/
 64 |       MDLess.log.info('Found YAML')
 65 |       # YAML
 66 |       in_yaml = true
 67 |       input.sub!(/(?i-m)^---[ \t]*\n(?[\s\S]*?)\n[-.]{3}[ \t]*\n/m) do
 68 |         m = Regexp.last_match
 69 |         MDLess.log.info('Processing YAML Header')
 70 |         YAML.unsafe_load(m['content']).map { |k, v| MDLess.meta[k.downcase] = v }
 71 |         lines = m['content'].split(/\n/)
 72 |         longest = lines.inject { |memo, word| memo.length > word.length ? memo : word }.length
 73 |         longest = longest < @cols ? longest + 1 : @cols
 74 |         lines.map do |line|
 75 |           if line =~ /^[-.]{3}\s*$/
 76 |             line = "#{color('metadata marker')}#{'%' * longest}"
 77 |           else
 78 |             line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2')
 79 |             line = "#{color('metadata marker')}%#{color('metadata color')}#{line}#{xc}"
 80 |           end
 81 | 
 82 |           line += "\u00A0" * (longest - line.uncolor.strip.length) if (longest - line.uncolor.strip.length).positive?
 83 |           line + xc
 84 |         end.join("\n") + "#{xc}\n"
 85 |       end
 86 |     end
 87 | 
 88 |     if !in_yaml && first_line =~ /(?i-m)^[\w ]+:\s+\S+/
 89 |       MDLess.log.info('Found MMD Headers')
 90 |       input.sub!(/(?i-m)^([\S ]+:[\s\S]*?)+(?=\n *\n)/) do |mmd|
 91 |         lines = mmd.split(/\n/)
 92 |         return mmd if lines.count > 20
 93 | 
 94 |         longest = lines.inject { |memo, word| memo.length > word.length ? memo : word }.length
 95 |         longest = longest < @cols ? longest + 1 : @cols
 96 |         lines.map do |line|
 97 |           line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2')
 98 |           parts = line.match(/[ \t]*(.*?): +(.*?)$/)
 99 |           key = parts[1].gsub(/[^a-z0-9\-_]/i, '')
100 |           value = parts[2].strip
101 |           MDLess.meta[key] = value
102 |           line = "#{color('metadata color')}#{line}#{xc}"
103 |           line += "\u00A0" * (longest - line.uncolor.strip.length) if (longest - line.uncolor.strip.length).positive?
104 |           line + xc
105 |         end.join("\n") + "#{"\u00A0" * longest}#{xc}\n"
106 |       end
107 |     end
108 | 
109 |     input
110 |   end
111 | 
112 |   def highlight_tags
113 |     log = MDLess.log
114 |     tag_color = color('at_tags tag')
115 |     value_color = color('at_tags value')
116 |     gsub(/(?
\s|m)(?@[^ \]:;.?!,("'\n]+)(?:(?\()(?.*?)(?\)))?(?=[ ;!,.?]|$)/) do
117 |       m = Regexp.last_match
118 |       last_color = m.pre_match.last_color_code
119 |       [
120 |         m['pre'],
121 |         tag_color,
122 |         m['tag'],
123 |         m['lparen'],
124 |         value_color,
125 |         m['value'],
126 |         tag_color,
127 |         m['rparen'],
128 |         xc,
129 |         last_color
130 |       ].join
131 |     end
132 |   end
133 | 
134 |   def scrub
135 |     encode('utf-16', invalid: :replace).encode('utf-8')
136 |   end
137 | 
138 |   def scrub!
139 |     replace scrub
140 |   end
141 | 
142 |   def valid_pygments_theme?
143 |     return false unless TTY::Which.exist?('pygmentize')
144 | 
145 |     MDLess.pygments_styles.include?(self)
146 |   end
147 | 
148 |   def remove_meta
149 |     first_line = split("\n").first
150 |     if first_line =~ /(?i-m)^---[ \t]*?$/
151 |       sub(/(?im)^---[ \t]*\n([\s\S\n]*?)\n[-.]{3}[ \t]*\n/, '')
152 |     elsif first_line =~ /(?i-m)^[\w ]+:\s+\S+/
153 |       sub(/(?im)^([\S ]+:[\s\S]*?)+(?=\n *\n)/, '')
154 |     else
155 |       self
156 |     end
157 |   end
158 | 
159 |   def valid_lexer?
160 |     return false unless TTY::Which.exist?('pygmentize')
161 | 
162 |     MDLess.pygments_lexers.include?(downcase)
163 |   end
164 | end
165 | 


--------------------------------------------------------------------------------
/lib/mdless/theme.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | module CLIMarkdown
  3 |   module Theme
  4 |     THEME_DEFAULTS = {
  5 |       'metadata' => {
  6 |         'border' => 'd blue on_black',
  7 |         'marker' => 'd black on_black',
  8 |         'color' => 'd white on_black'
  9 |       },
 10 |       'emphasis' => {
 11 |         'bold' => 'b',
 12 |         'bold_character' => '**',
 13 |         'italic' => 'u i',
 14 |         'italic_character' => '_',
 15 |         'bold-italic' => 'b u i'
 16 |       },
 17 |       'highlight' => 'b black on_yellow',
 18 |       'h1' => {
 19 |         'color' => 'b intense_black on_white',
 20 |         'pad' => 'd black on_white',
 21 |         'pad_char' => '='
 22 |       },
 23 |       'h2' => {
 24 |         'color' => 'b white on_intense_black',
 25 |         'pad' => 'd white on_intense_black',
 26 |         'pad_char' => '-'
 27 |       },
 28 |       'h3' => {
 29 |         'color' => 'u b yellow'
 30 |       },
 31 |       'h4' => {
 32 |         'color' => 'u yellow'
 33 |       },
 34 |       'h5' => {
 35 |         'color' => 'b white'
 36 |       },
 37 |       'h6' => {
 38 |         'color' => 'b white'
 39 |       },
 40 |       'link' => {
 41 |         'brackets' => 'b black',
 42 |         'text' => 'u b blue',
 43 |         'url' => 'cyan'
 44 |       },
 45 |       'image' => {
 46 |         'bang' => 'red',
 47 |         'brackets' => 'b black',
 48 |         'title' => 'cyan',
 49 |         'url' => 'u yellow'
 50 |       },
 51 |       'list' => {
 52 |         'ul_char' => '*',
 53 |         'bullet' => 'b intense_red',
 54 |         'number' => 'b intense_blue',
 55 |         'color' => 'intense_white'
 56 |       },
 57 |       'footnote' => {
 58 |         'brackets' => 'b black on_black',
 59 |         'caret' => 'b yellow on_black',
 60 |         'title' => 'x yellow on_black',
 61 |         'note' => 'u white on_black'
 62 |       },
 63 |       'code_span' => {
 64 |         'marker' => 'b white',
 65 |         'color' => 'b white on_intense_black',
 66 |         'character' => '`'
 67 |       },
 68 |       'code_block' => {
 69 |         'marker' => 'intense_black',
 70 |         'character' => '>',
 71 |         'bg' => 'on_black',
 72 |         'color' => 'white on_black',
 73 |         'border' => 'blue',
 74 |         'title' => 'magenta',
 75 |         'eol' => 'intense_black on_black',
 76 |         'pygments_theme' => 'monokai'
 77 |       },
 78 |       'blockquote' => {
 79 |         'marker' => {
 80 |           'character' => '>',
 81 |           'color' => 'yellow'
 82 |         },
 83 |         'color' => 'b white'
 84 |       },
 85 |       'dd' => {
 86 |         'term' => 'black on_white',
 87 |         'marker' => 'd red',
 88 |         'color' => 'b white'
 89 |       },
 90 |       'hr' => {
 91 |         'color' => 'd white'
 92 |       },
 93 |       'table' => {
 94 |         'border' => 'd black',
 95 |         'header' => 'yellow',
 96 |         'divider' => 'b black',
 97 |         'color' => 'white',
 98 |         'bg' => 'on_black'
 99 |       },
100 |       'html' => {
101 |         'brackets' => 'd yellow on_black',
102 |         'color' => 'yellow on_black'
103 |       },
104 |       'math' => {
105 |         'brackets' => 'b black',
106 |         'equation' => 'b blue'
107 |       },
108 |       'super' => 'b green',
109 |       'deletion' => 'strikethrough red',
110 |       'text' => 'white',
111 |       'at_tags' => {
112 |         'tag' => 'magenta',
113 |         'value' => 'b white'
114 |       },
115 |       'taskpaper' => {
116 |         'marker' => 'b white',
117 |         'project' => 'b green',
118 |         'task' => 'white',
119 |         'note' => 'd white'
120 |       }
121 |     }.freeze
122 | 
123 |     def load_theme_file(theme_file)
124 |       raise "Theme #{theme_file} doesn't exist" unless File.exist?(theme_file)
125 | 
126 |       begin
127 |         theme_contents = IO.read(theme_file)
128 |         new_theme = YAML.load(theme_contents)
129 |         theme = THEME_DEFAULTS.deep_merge(new_theme)
130 |         # # write merged theme back in case there are new keys since
131 |         # # last updated
132 |         # File.open(theme_file,'w') {|f|
133 |         #   f.puts theme.to_yaml
134 |         # }
135 |       rescue StandardError => e
136 |         @log.warn('Error merging user theme')
137 |         warn e
138 |         warn e.backtrace
139 |         theme = THEME_DEFAULTS
140 |         if File.basename(theme_file) =~ /mdless\.theme/
141 |           FileUtils.rm(theme_file)
142 |           @log.info("Rewriting default theme file to #{theme_file}")
143 |           File.open(theme_file, 'w') { |f| f.puts theme.to_yaml }
144 |         end
145 |       end
146 |       theme
147 |     end
148 | 
149 |     def load_theme(theme)
150 |       config_dir = File.expand_path('~/.config/mdless')
151 |       default_theme_file = File.join(config_dir, 'mdless.theme')
152 |       if theme =~ /default/i || !theme
153 |         theme_file = default_theme_file
154 |       else
155 |         theme = theme.strip.sub(/(\.theme)?$/, '.theme')
156 |         theme_file = File.join(config_dir, theme)
157 |       end
158 | 
159 |       unless File.directory?(config_dir)
160 |         @log.info("Creating config directory at #{config_dir}")
161 |         FileUtils.mkdir_p(config_dir)
162 |       end
163 | 
164 |       unless File.exist?(theme_file)
165 |         if File.exist?(default_theme_file)
166 |           @log.info('Specified theme not found, using default')
167 |           theme_file = default_theme_file
168 |         else
169 |           theme = THEME_DEFAULTS
170 |           @log.info("Writing fresh theme file to #{theme_file}")
171 |           File.open(theme_file, 'w') { |f| f.puts theme.to_yaml }
172 |         end
173 |       end
174 | 
175 |       load_theme_file(theme_file)
176 |     end
177 |   end
178 | end
179 | 


--------------------------------------------------------------------------------
/test/codeblocks.md:
--------------------------------------------------------------------------------
  1 | title: Code block tests
  2 | date: yesterday
  3 | 
  4 | Code block tests
  5 | ================
  6 | 
  7 | ```
  8 | #!
  9 | ```
 10 | 
 11 | ```
 12 | # Just a comment
 13 | do_thing()
 14 | ```
 15 | 
 16 |     # Another comment
 17 |     does_this_work?
 18 | 
 19 | 
 20 | How about definition lists?
 21 | : Do those work with redcarpet?
 22 | : What about @tags? @tag(value 2o2-3)
 23 | 
 24 | Just some text[^fn0] before we get started.
 25 | 
 26 | ![Image test](https://raw.githubusercontent.com/eddieantonio/i/master/imgcat.png "imgcat cat")
after a break 27 | 28 | [^fn0]: This is the first footnote 29 | 30 | This is some text after the image. 31 | 32 | This is [a test link](https://brettterpstra.com). This is a test of html tag sytling.[^fn1] 33 | 34 | | a table | to see | how | 35 | | :---- |----|:---:| 36 | coloring | works|out 37 | 38 | Indented code 39 | This should just display as indented text 40 | 41 | Lorem ipsum [dolor sit amet][reflink], consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. [Excepteur sint occaecat](https://test.com) cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 42 | 43 | [reflink]: https://brettterpstra.com/should/become/inline "This should become an inline link" 44 | 45 | * list test 46 | * more list with **some bold** in it 47 | * more list nested 48 | + Just checking 49 | 1. nested numeric 50 | 2. list stuff 51 | 3. you wouldn't get it 52 | * another list 53 | 1. this should 54 | 2. start over 55 | 56 | --- 57 | 58 | 1. Numbered list 59 | 2. it has numbers 60 | 3. neat 61 | 62 | ## Nested, malformed language 63 | 64 | 1. If you're using the `develop` branch then it is recommended to set up a proper development environment ("Setting up a dev environment" below) however one can install the develop versions of the dependencies instead: 65 | 66 | ``` bash 67 | 1 scripts/fetch-develop.deps.sh 68 | ``` 69 | 70 | Whenever you git pull on `riot-web` you will also probably need to force an update 71 | to these dependencies - the simplest way is to re-run the script, but you can also 72 | manually update and rebuild them: 73 | 74 | ```bash 75 | cd matrix-js-sdk 76 | git pull 77 | yarn install # re-run to pull in any new dependencies 78 | cd ../matrix-react-sdk 79 | git pull 80 | yarn install 81 | ``` 82 | 83 | Or just use https://riot.im/develop - the continuous integration release of the 84 | develop branch. (Note that we don't reference the develop versions in git directly 85 | due to https://github.com/npm/npm/issues/3055.) 86 | 87 | 1. super indented 88 | 89 | ```bash 90 | cd matrix-js-sdk 91 | - git pull 92 | ``` 93 | 94 | ### Outdented, no language 95 | 96 | Wait a few seconds for the initial build to finish; you should see something like: 97 | 98 | ```console 99 | Hash: b0af76309dd56d7275c8 100 | Version: webpack 1.12.14 101 | Time: 14533ms 102 | Asset Size Chunks Chunk Names 103 | bundle.js 4.2 MB 0 [emitted] main 104 | bundle.css 91.5 kB 0 [emitted] main 105 | bundle.js.map 5.29 MB 0 [emitted] main 106 | bundle.css.map 116 kB 0 [emitted] main 107 | + 1013 hidden modules 108 | ``` 109 | 110 | Remember, the command will not terminate since it runs the web server 111 | and rebuilds source files when they change. This development server also 112 | disables caching, so do NOT use it in production. 113 | 114 | ## Indented, no language 115 | 116 | Open http://127.0.0.1:8080/ in your browser to see your newly built Riot. 117 | 118 | If you're building a custom branch, or want to use the develop branch, check out the appropriate 119 | riot-web branch and then run: 120 | 121 | docker build -t vectorim/riot-web:develop \ 122 | --build-arg USE_CUSTOM_SDKS=true \ 123 | --build-arg REACT_SDK_REPO="https://github.com/matrix-org/matrix-react-sdk.git" \ 124 | --build-arg REACT_SDK_BRANCH="develop" \ 125 | --build-arg JS_SDK_REPO="https://github.com/matrix-org/matrix-js-sdk.git" \ 126 | --build-arg JS_SDK_BRANCH="develop" \ 127 | . 128 | 129 | 130 | ## Language via hashbang 131 | 132 | ```ruby 133 | #!/usr/bin/env ruby 134 | def convert_markdown(input) 135 | @headers = get_headers(input) 136 | # yaml/MMD headers 137 | in_yaml = false 138 | if input.split("\n")[0] =~ /(?i-m)^---[ \t]*?(\n|$)/ 139 | @log.info("Found YAML") 140 | # YAML 141 | in_yaml = true 142 | input.sub!(/(?i-m)^---[ \t]*\n([\s\S]*?)\n[\-.]{3}[ \t]*\n/) do |yaml| 143 | m = Regexp.last_match 144 | 145 | @log.info("Processing YAML Header") 146 | m[0].split(/\n/).map {|line| 147 | if line =~ /^[\-.]{3}\s*$/ 148 | line = c([:d,:black,:on_black]) + "% " + c([:d,:black,:on_black]) + line 149 | else 150 | line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2') 151 | line = c([:d,:black,:on_black]) + "% " + c([:d,:white]) + line 152 | end 153 | if @cols - line.uncolor.size > 0 154 | line += " "*(@cols-line.uncolor.size) 155 | end 156 | }.join("\n") + "#{xc}\n" 157 | end 158 | end 159 | end 160 | ``` 161 | 162 | ## Wrapping indented code 163 | 164 | ``` 165 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 166 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 167 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 168 | ``` 169 | 170 | [^fn1]: this should end up under its origin paragraph 171 | 172 | ## more headlines 173 | 174 | ## for testing 175 | 176 | ## index listing 177 | 178 | ## and indentation 179 | 180 | ```ruby 181 | def double_indented_code 182 | puts "one too many indents" 183 | end 184 | ``` 185 | 186 | ## almost enough 187 | 188 | ## gotta get to 10 189 | 190 | -------------------------------------------------------------------------------- /lib/mdless/colors.rb: -------------------------------------------------------------------------------- 1 | module CLIMarkdown 2 | module Colors 3 | ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)/.freeze 4 | 5 | COLORS = { 6 | :reset => 0, # synonym for :clear 7 | :x => 0, 8 | :bold => 1, 9 | :b => 1, 10 | :dark => 2, 11 | :d => 2, 12 | :italic => 3, # not widely implemented 13 | :i => 3, 14 | :underline => 4, 15 | :underscore => 4, # synonym for :underline 16 | :u => 4, 17 | :blink => 5, 18 | :rapid_blink => 6, # not widely implemented 19 | :negative => 7, # no reverse because of String#reverse 20 | :r => 7, 21 | :concealed => 8, 22 | :strikethrough => 9, # not widely implemented 23 | :black => 30, 24 | :red => 31, 25 | :green => 32, 26 | :yellow => 33, 27 | :blue => 34, 28 | :magenta => 35, 29 | :cyan => 36, 30 | :white => 37, 31 | :on_black => 40, 32 | :on_red => 41, 33 | :on_green => 42, 34 | :on_yellow => 43, 35 | :on_blue => 44, 36 | :on_magenta => 45, 37 | :on_cyan => 46, 38 | :on_white => 47, 39 | :intense_black => 90, # High intensity, aixterm (works in OS X) 40 | :intense_red => 91, 41 | :intense_green => 92, 42 | :intense_yellow => 93, 43 | :intense_blue => 94, 44 | :intense_magenta => 95, 45 | :intense_cyan => 96, 46 | :intense_white => 97, 47 | :on_intense_black => 100, # High intensity background, aixterm (works in OS X) 48 | :on_intense_red => 101, 49 | :on_intense_green => 102, 50 | :on_intense_yellow => 103, 51 | :on_intense_blue => 104, 52 | :on_intense_magenta => 105, 53 | :on_intense_cyan => 106, 54 | :on_intense_white => 107 55 | } 56 | 57 | def uncolor 58 | self.unpad.gsub(/\e\[[\d;]+m/,'') 59 | end 60 | 61 | def remove_pre_post 62 | gsub(/<<(pre|post)\d+>>/, '') 63 | end 64 | 65 | def unpad 66 | self.gsub(/\u00A0/, ' ') 67 | end 68 | 69 | # Get the calculated ANSI color at the end of the 70 | # string 71 | # 72 | # @return ANSI escape sequence to match color 73 | # 74 | def last_color_code 75 | m = scan(ESCAPE_REGEX) 76 | 77 | em = [] 78 | fg = nil 79 | bg = nil 80 | rgbf = nil 81 | rgbb = nil 82 | 83 | m.each do |c| 84 | case c 85 | when '0' 86 | em = ['0'] 87 | fg, bg, rgbf, rgbb = nil 88 | when /;38;/ 89 | fg = nil 90 | rgbf = c 91 | when /;48;/ 92 | bg = nil 93 | rgbb = c 94 | else 95 | em = [] 96 | c.split(/;/).each do |i| 97 | x = i.to_i 98 | if x <= 9 99 | em << x 100 | elsif x >= 30 && x <= 39 101 | rgbf = nil 102 | fg = x 103 | elsif x >= 40 && x <= 49 104 | rgbb = nil 105 | bg = x 106 | elsif x >= 90 && x <= 97 107 | rgbf = nil 108 | fg = x 109 | elsif x >= 100 && x <= 107 110 | rgbb = nil 111 | bg = x 112 | end 113 | end 114 | end 115 | end 116 | 117 | escape = '' 118 | escape += "\e[#{em.join(';')}m" unless em.empty? 119 | escape += "\e[#{rgbb}m" if rgbb 120 | escape += "\e[#{rgbf}m" if rgbf 121 | fg_bg = [fg, bg].delete_if(&:nil?).join(';') 122 | escape += "\e[#{fg_bg}m" unless fg_bg.empty? 123 | escape 124 | end 125 | 126 | def blackout(bgcolor) 127 | key = bgcolor.to_sym 128 | bg = COLORS.key?(key) ? COLORS[key] : 40 129 | self.gsub(/(^|$)/,"\e[#{bg}m").gsub(/3([89])m/,"#{bg};3\\1m") 130 | end 131 | 132 | def uncolor! 133 | self.replace self.uncolor 134 | end 135 | 136 | def size_clean 137 | self.uncolor.size 138 | end 139 | 140 | def wrap(width=78, foreground=:x) 141 | return self if uncolor =~ /(^([%~] |\s*>)| +[=-]{5,})/ 142 | 143 | visible_width = 0 144 | lines = [] 145 | line = '' 146 | last_ansi = '' 147 | 148 | line += match(/^\s*/)[0].gsub(/\t/, ' ') 149 | input = dup # .gsub(/(\w-)(\w)/,'\1 \2') 150 | # input.gsub!(/\[.*?\]\(.*?\)/) do |link| 151 | # link.gsub(/ /, "\u00A0") 152 | # end 153 | input.split(/\s/).each do |word| 154 | last_ansi = line.last_color_code 155 | if word =~ /[\s\t]/ 156 | line << word 157 | elsif visible_width + word.size_clean >= width 158 | lines << line + xc 159 | visible_width = word.size_clean 160 | line = last_ansi + word 161 | elsif line.empty? 162 | visible_width = word.size_clean 163 | line = last_ansi + word 164 | else 165 | visible_width += word.size_clean + 1 166 | line << ' ' << last_ansi + word 167 | end 168 | end 169 | lines << line + match(/\s*$/)[0] + xc if line 170 | lines.map!.with_index do |l, i| 171 | (i.positive? ? l[i - 1].last_color_code : '') + l 172 | end 173 | lines.join("\n").gsub(/\[.*?\]\(.*?\)/) do |link| 174 | link.gsub(/\u00A0/, ' ') 175 | end 176 | end 177 | 178 | def c(args) 179 | out = [] 180 | 181 | args.each do |arg| 182 | if arg.to_s =~ /^([bf]g|on_)?([a-f0-9]{3}|[a-f0-9]{6})$/i 183 | out.concat(rgb(arg.to_s)) 184 | elsif COLORS.key? arg 185 | out << COLORS[arg] 186 | end 187 | end 188 | if !out.empty? 189 | "\e[#{out.join(';')}m" 190 | else 191 | '' 192 | end 193 | end 194 | 195 | private 196 | 197 | def rgb(hex) 198 | is_bg = hex.match(/^(bg|on_)/) ? true : false 199 | hex_string = hex.sub(/^(bg|on_)?(.{3}|.{6})/, '\2') 200 | hex_string.gsub!(/(.)/, '\1\1') if hex_string.length == 3 201 | 202 | parts = hex_string.match(/(?..)(?..)(?..)/) 203 | t = [] 204 | %w[r g b].each do |e| 205 | t << parts[e].hex 206 | end 207 | 208 | [is_bg ? 48 : 38, 2].concat(t) 209 | end 210 | 211 | def xc(foreground=:x) 212 | c([foreground]) 213 | end 214 | end 215 | end 216 | 217 | class String 218 | include CLIMarkdown::Colors 219 | end 220 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.1.60 2 | 3 | 2.1.61 4 | 5 | 2.1.62 6 | 7 | 2.1.58 8 | : Longest_element in mismatched code fences 9 | 10 | 2.1.50 11 | : YAML load errors in Ruby 3.3 12 | 13 | 2.1.49 14 | : YAML load errors in Ruby 3.32.1.46 15 | : Fix for table cells containing links 16 | 17 | 2.1.35 18 | : Ruby 2.7 error (again) 19 | 20 | 2.1.30 21 | : Error when $EDITOR is not defined 22 | 23 | 2.1.29 24 | : More code cleanup, help output improvements 25 | : Line breaks in help output 26 | : This release should fix an error on Ruby 2.7 in string.rb 27 | 28 | 2.1.28 29 | : Default to 0 width, which makes the width the column width of the terminal 30 | : Don't save a --width setting to config, require that to be manually updated if desired 31 | 32 | 2.1.27 33 | : Error handling when YAML can't be processed 34 | 35 | 2.1.25 36 | : YAML loading issues caused by safe_load 37 | 38 | 2.1.24 39 | : Save MultiMarkdown metadata into a hash 40 | : Allow [%metakey] replacements 41 | : Allow {{filename}} transcusions (MultiMarkdown), respects "transclude base:" metadata 42 | : Transclude documents with `{{filename}}`, nesting allowed, "transclude base:" metadata respected (even in YAML) 43 | : Metadata can be used in `[%key]` format to have it replaced in the output based on metadata values 44 | 45 | 2.1.23 46 | : Fix release pipeline to get version number correct in git release 47 | : Changelog mismatch 48 | 49 | 2.1.22 50 | : TaskPaper file with metadata causing negative argument error 51 | : Remove `
` from metadata 52 | : YAML metadata and negative line lengths 53 | >>>>>>> release/2.1.30 54 | 55 | 2.1.14 56 | : Spaces on a line separating metadata won't break display 57 | : Preserve line breaks in metadata 58 | : Failure to display metadata fixed 59 | 60 | 2.1.13 61 | : Remove debugging statement 62 | 63 | 2.1.12 64 | : Fix list indentation when nesting 65 | 66 | 2.1.11 67 | : Better regex for highlighting raw HTML 68 | : Indentation in highlighted code blocks 69 | : HTML Tag highlighting breaking pre/post color conversion 70 | 71 | 2.1.10 72 | : Spinner while processing to indicate working 73 | : Image links can now be converted to reference format, with correct coloring 74 | : Render image links before reference link conversion 75 | 76 | 2.1.9 77 | : Code block prefix configurable, can be left empty to make more copyable code blocks 78 | : Remove empty lines from block quotes 79 | : Infinite loop when calculating ANSI emphasis 80 | : Don't accept colors or semicolons inside of @tag names 81 | 82 | 2.1.8 83 | : --update-theme option to add any missing keys to your theme file 84 | : Strip ul_char of any spaces before inserting 85 | 86 | 2.1.7 87 | : Dedup and remove empty escape codes before output 88 | : Tables losing column alignment 89 | : Unneccessarily long table cells 90 | 91 | 2.1.6 92 | : In addition to color names, you can now use 3 or 6-digit hex codes, prefix with "bg" or "on_" to affect background color 93 | : Better highlighting of h1/h2 when header contains a link 94 | : List items with multiple paragraphs incorrectly highlighted 95 | 96 | 2.1.3 97 | : Respect :width setting in config 98 | 99 | 2.0.24 100 | : Update readme with config descriptions 101 | : Code blocks containing YAML with `---` as the first line were being interpreted as Setext headers 102 | : Line breaks being consumed when matching tags for highlighting 103 | 104 | 2.0.21 105 | : When converting to reference links, catch links that have been wrapped 106 | 107 | 2.0.20 108 | : Subsequent tables inheriting first table's column count 109 | 110 | 2.0.19 111 | : `--section` can take string arguments to be fuzzy matched against headlines 112 | : Code refactoring 113 | : TaskPaper formatting now responds to --section with string matches 114 | : TaskPaper formatting now responds to --list to list projects 115 | : TaskPaper auto detection double checks content by removing all projects and tasks and seeing if there's anything left before deciding it's not TaskPaper content 116 | : Extra line break before headers 117 | : Wrap block quotes to max width 118 | : Missing first headline in output 119 | : Long links that were wrapped were not being replaced when converting to reference links 120 | 121 | 2.0.18 122 | : Better handling of default options set in config 123 | : More expansive detection of screen width, no longer just dependent on `tput` being available 124 | : Only extend borders and backgrounds on code blocks to the length of the longest line 125 | : Include the language in the top border of code blocks, if available 126 | : Validate themes and lexers using `pygmentize` output, with fallbacks 127 | : If width specified in config is greater than display columns, fall back to display columns as max width 128 | : Metadata (MMD/YAML) handling on TaskPaper files 129 | 130 | 2.0.17 131 | : Re-order command line options for more readable help output (`mdless -h`) 132 | 133 | 2.0.15 134 | : Highlight [[wiki links]] 135 | : TaskPaper rendering refinements 136 | : Handle TaskPaper tasks without project if --taskpaper is enabled 137 | : Wiki link highlighting is optional with `--[no-]wiki-links` and can be set in config 138 | : Nil error on short files 139 | : Project regex matching `- PROJECT NAME:` 140 | : If taskpaper is true, avoid all parsing other than tasks, projects, notes, and tags 141 | 142 | 2.0.8 143 | : Image rendering with chafa improved, still have to figure out a way to make sure content breaks around the embedded image 144 | : Only detect mmd headers on first line 145 | 146 | 2.0.7 147 | : Render links as reference links at the end of the file (`--links ref`) or per-paragraph (`--links para`). Defaults to inline (`--links inline`) 148 | : Pad numbers on headline listing to preserve indentation 149 | 150 | 2.0.6 151 | : Render links as reference links at the end of the file (`--links ref`) or per-paragraph (`--links para`). Defaults to inline (`--links inline`) 152 | : Pad numbers on headline listing to preserve indentation 153 | 154 | 2.0.5 155 | : Better highlighting of metadata (both YAML and MMD) 156 | 157 | 2.0.4 158 | : False MMD metadata detection 159 | 160 | 2.0.0 161 | : Rely on Redcarpet for Markdown parsing, far more accurate with a few losses I'll handle over time 162 | : Config file at ~/.config/mdless/config.yml 163 | : Allow inlining of footnotes 164 | : Nested list indentation 165 | 166 | 1.0.37 167 | : Comments inside of fenced code rendering as ATX headlines 168 | 169 | 1.0.35 170 | : Improved code block parsing and handling 171 | 172 | 1.0.33 173 | : Allow multiple sections with `-s 3,4,5` 174 | 175 | 1.0.32 176 | : Errors in Ruby 3.2 177 | 178 | 1.0.30 179 | : Errant pager output 180 | 181 | 1.0.29 182 | : Allow $ markers for equations 183 | : Don't force white foreground when resetting color (allow default terminal color to be foreground 184 | : Reset color properly when span elements are used in list items 185 | : Code block border wrapping 186 | : Use template settings for all footnote highlights 187 | : Errant pager output1.0.13 188 | : Fix for tables being eaten at the end of a document 189 | 190 | 1.0.10 191 | : Fix for regex characters in headlines breaking rendering 192 | 193 | 1.0.9 194 | : Catch error when breaking pipe 195 | 196 | 1.0.8 197 | : Improved table formatting 198 | 199 | 1.0.7 200 | : Force rewrite of damaged themes 201 | : Add iTerm marks to h1-3 when using iTerm and no pager, so you can navigate with ⌘⇧↑/↓ 202 | 203 | 1.0.6 204 | : Fresh theme write was outputting `--- default` instead of a theme 205 | : Better code span color 206 | : If `bat` is the pager, force into `--plain` mode 207 | 208 | 1.0.5 209 | : Stop adjusting for highest header 210 | 211 | 1.0.3 212 | : Sort options order in `--help` output 213 | : Allow multiple theme files, `--theme=NAME` option 214 | 215 | 1.0.2 216 | : Handle emphasis inside of quotes and parenthesis 217 | : Make emphasis themeable in mdless.theme 218 | : Fix for `-I` throwing error if imgcat isn't installed 219 | : remove backslash from escaped characters 220 | 221 | 1.0.1 222 | : Fix for header listing justification 223 | : Exclude horizontal rules `---` in header list 224 | 225 | 1.0.0 226 | : Just a version bump because I think it deserves it at this point. 227 | 228 | 0.0.15 229 | : User themeable 230 | : Handle Setex headers 231 | : General fixes and improvements 232 | 233 | 0.0.14 234 | : Don't run pygments on code blocks without language specified in either the fence or a hashbang on the first line 235 | : Fix to maintain indentation for fenced code in lists 236 | : Remove leading ~ for code blocks 237 | : Add background color 238 | : Add line ending marker to make more sense of code wrapping 239 | : lowercase code block fences 240 | : remove "end code" marker 241 | : Highlight with monokai theme 242 | : Black background for all (fenced) code blocks 243 | 244 | 0.0.13 245 | : Better language detection for code blocks 246 | : when available, use italic font for italics and bold-italics emphasis 247 | : new colorization for html tags 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mdless 2 | 3 | 4 | 5 | `mdless` is a utility that provides a formatted and highlighted view of Markdown files in Terminal. 6 | 7 | I often use iTerm2 in visor mode, so `qlmanage -p` is annoying. I still wanted a way to view Markdown files quickly and without cruft. 8 | 9 | ![mdless screenshot](screenshots/mdless.png) 10 | 11 | 12 | ## Features 13 | 14 | - Built in pager functionality with pipe capability, `less` replacement for Markdown files 15 | - Format tables 16 | - Colorize Markdown syntax for most elements 17 | - Normalize spacing and link formatting 18 | - Display footnotes after each paragraph 19 | - Inline image display (local, optionally remote) (with compatible tools like imgcat or chafa) 20 | - Syntax highlighting of code blocks when [Pygments](http://pygments.org/) is installed 21 | - List headlines in document 22 | - Display single section of the document based on headlines 23 | - Configurable Markdown options 24 | - Customizable colors 25 | - Add iTerm marks for h1-3 navigation when pager is disabled 26 | - TaskPaper syntax detection and highlighting 27 | 28 | ## Installation 29 | 30 | ### Gem install 31 | 32 | gem install mdless 33 | 34 | If you run into errors, try `gem install --user-install mdless`, or `sudo gem install mdless` (in that order). 35 | 36 | ### Homebrew 37 | 38 | mdless is also available via Homebrew (directly). 39 | 40 | brew install mdless 41 | 42 | 43 | ### Dependencies 44 | 45 | To render images, you need `imgcat` or `chafa` installed (`brew install chafa`). 46 | 47 | For syntax highlighting, the `pygmentize` command must be available, part of the [Pygments](http://pygments.org/) package (`brew install pygments`). 48 | 49 | ## Usage 50 | 51 | `mdless [options] path` or `cat [path] | mdless` 52 | 53 | The pager used is determined by system configuration in this order of preference: 54 | 55 | * `$PAGER` 56 | * `less` 57 | * `more` 58 | * `cat` 59 | * `pager` 60 | 61 | ### Options 62 | 63 | -c, --[no-]color Colorize output (default on) 64 | -d, --debug LEVEL Level of debug messages to output (1-4, 4 to see all messages) 65 | -h, --help Display this screen 66 | -i, --images=TYPE Include [local|remote (both)|none] images in output (requires chafa or imgcat, default none). 67 | -I, --all-images Include local and remote images in output (requires imgcat or chafa) 68 | -l, --list List headers in document and exit 69 | -p, --[no-]pager Formatted output to pager (default on) 70 | -P Disable pager (same as --no-pager) 71 | -s, --section=NUMBER[,NUMBER] Output only a headline-based section of the input (numeric from --list) 72 | -t, --theme=THEME_NAME Specify an alternate color theme to load 73 | -@, --at_tags Highlight @tags and values in the document 74 | -v, --version Display version number 75 | -w, --width=COLUMNS Column width to format for (default: terminal width) 76 | --[no-]autolink Convert bare URLs and emails to 77 | --[no-]inline_footnotes Display footnotes immediately after the paragraph that references them 78 | --[no-]intra-emphasis Parse emphasis inside of words (e.g. Mark_down_) 79 | --[no-]lax-spacing Allow lax spacing 80 | --links=FORMAT Link style ([inline, reference, paragraph], default inline, 81 | "paragraph" will position reference links after each paragraph) 82 | --[no-]linebreaks Preserve line breaks 83 | --[no-]syntax Syntax highlight code blocks 84 | --taskpaper=OPTION Highlight TaskPaper format (true|false|auto) 85 | --update_config Update the configuration file with new keys and current command line options 86 | --[no-]wiki-links Highlight [[wiki links]] 87 | 88 | ## Configuration 89 | 90 | The first time mdless is run, a config file will be written to `~/.config/mdless/config.yml`, based on the command line options used on the first run. Update that file to make any options permanent (config options will always be overridden by command line flags). 91 | 92 | ``` 93 | --- 94 | :at_tags: true 95 | :autolink: true 96 | :color: true 97 | :inline_footnotes: true 98 | :intra_emphasis: false 99 | :lax_spacing: true 100 | :links: :paragraph 101 | :local_images: true 102 | :pager: true 103 | :preserve_linebreaks: false 104 | :remote_images: false 105 | :syntax_higlight: true 106 | :taskpaper: :auto 107 | :theme: default 108 | :width: 120 109 | :wiki_links: true 110 | ``` 111 | 112 | - The `:at_tags` setting determines whether @tags will be highlighted. If this is enabled, colors will be pulled from the `at_tags` settings in the theme. 113 | - `:autolink` will determine whether bare urls are turned into `` urls. 114 | - `:color` will enable or disable all coloring. 115 | - `:inline_footnotes` will determine the placement of footnotes. If true, footnotes will be added directly after the element that refers to them. 116 | - `:intra_emphasis` will determine whether words containing underscores are rendered as italics or not. 117 | - `:lax_spacing` determines whether a blank line is required around HTML elements. 118 | - `:links` can be `inline`, `reference`, or `paragraph`. Paragraph puts reference links directly after the graf that refers to them. 119 | - `:local_images` determines whether local images are processed using `chafa` or `imgcat` (whichever is available). `:remote_images` does the same for images referenced with web urls. If `:remote_images` is true, then `:local_images` is automatically enabled. 120 | - `:pager` turns on or off pagination using `less` or closest available substitute. 121 | - `:preserve_linebreaks` determines whether hard breaks within paragraphs are preserved. When converting to HTML, most Markdown processors will cause consecutive lines to be merged together, which is the default behavior for `mdless`. Turning this option on will cause lines to remain hard wrapped. 122 | - `:syntax_highlight` will turn on/off syntax highlighting of code blocks (requires Pygments) 123 | - `:taskpaper` determines whether a file is rendered as a TaskPaper document. This can be set to `:auto` to have TaskPaper detected from extension or content. 124 | - `:theme` allows you to specify an alternate theme. See Customization below. 125 | - `:width` allows you to permanantly set a width for wrapping of lines. If the width specified is greater than the available columns of the display, the display columns will be used instead. 126 | - `:wiki_links` determines whether `[[wiki links]]` will be highlighted. If highlighted, colors are pulled from the `link` section of the theme. 127 | 128 | 129 | ## Customization 130 | 131 | On first run a default theme file will be placed in `~/.config/mdless/mdless.theme`. You can edit this file to modify the colors mdless uses when highlighting your files. You can copy this file and create multiple theme options which can be specified with the `-t NAME` option. For example, create `~/.config/mdless/brett.theme` and then call `mdless -t brett filename.md`. 132 | 133 | Colors are limited to basic ANSI codes, with support for bold, underline, italics (if available for the terminal/font), dark and bright, and foreground and background colors. 134 | 135 | Customizeable settings are stored in [YAML](https://yaml.org) format. A chunk of the settings file looks like this: 136 | 137 | ```yaml 138 | h1: 139 | color: b intense_black on_white 140 | pad: d black on_white 141 | pad_char: "=" 142 | ``` 143 | 144 | Font and color settings are set using a string of color names and modifiers. A typical string looks like `b red on_white`, which would give you a bold red font on a white background. In the YAML settings file there's no need for quotes, just put the string following the colon for the setting. 145 | 146 | You can also use 3 or 6-digit hex codes in place of color names. These can be prefixed with `bg` or `on_` to affect background colors, e.g. `bgFF0ACC`. The codes are case-insensitive and can be combined with emphasis modifiers like `b` or `u`. 147 | 148 | Some extra (non-color) settings are available for certain keys, e.g. `pad_char` to define the right padding character used on level 1 and 2 headlines. Note that you can change the [Pygments](http://pygments.org/) theme used for syntax highlighting with the code_block.pygments_theme setting. For a list of available styles (assuming you have Pygments installed), use `pygmentize -L styles`. 149 | 150 | The display of characters around emphasis and code spans can be configured. By default, the surrounding character for bold is `**`, italic is `_`, and code span is a backtick. You can leave these keys empty to not display characters at all. For triple-emphasized text, the text will be surrounded by italic and bold characters, in that order. 151 | 152 | ```yaml 153 | emphasis: 154 | bold: b 155 | bold_character: "**" 156 | italic: u i 157 | italic_character: "" 158 | bold-italic: b u i 159 | code_span: 160 | marker: b white 161 | color: b white on_intense_black 162 | character: "" 163 | ``` 164 | 165 | *Note:* the ANSI escape codes are reset every time the color changes, so, for example, if you have a key that defines underlines for the url in a link, the underline will automatically be removed when it gets to a bracket. This also means that if you define a background color, you'll need to define it again on all the keys that it should affect. 166 | 167 | Base colors: 168 | 169 | - black 170 | - red 171 | - green 172 | - yellow 173 | - blue 174 | - magenta 175 | - cyan 176 | - white 177 | 178 | Emphasis: 179 | 180 | - b (bold) 181 | - d (dark) 182 | - i (italic) 183 | - u (underline) 184 | - r (reverse, negative) 185 | 186 | To modify the emphasis, use 'b' (bold), 'i' (italic), 'u' (underline), e.g. `u yellow` for underlined yellow. These can be combined, e.g. `b u red`. 187 | 188 | Use 'r' to reverse foreground and background colors. `r white on_black` would display as `black on_white`. 'r' alone will reverse the current color set for a line. 189 | 190 | To set a background color, use `on_[color]` with one of the 8 colors. This can be used with foreground colors in the same setting, e.g. `white on_black`. 191 | 192 | Use 'd' (dark) to indicate the darker version of a foreground color. On macOS (and possibly other systems) you can use the brighter version of a color by prefixing with "intense", e.g. `intense_red` or `on_intense_black`. 193 | 194 | ## Integrations 195 | 196 | ### Ranger 197 | 198 | [Ranger](https://ranger.github.io) is a file manager that allows for quick navigation in the file hierarchy. A preview can be displayed for various file types. See docs at . 199 | 200 | mdless can be used in Ranger to preview Markdown and Taskpaper. 201 | 202 | Ranger is installed with `brew install ranger`. 203 | 204 | With `ranger --copy-config=scope` the configuration file for previews `scope.sh` is created in the directory `~/.config/ranger`. 205 | 206 | The configuration file is already preconfigured. The following can be inserted above html to use mdless. 207 | 208 | ``` 209 | ## Markdown 210 | md|taskpaper) 211 | mdless --taskpaper=auto -@ "${FILE_PATH}" && exit 5 212 | ;; 213 | ``` 214 | 215 | Thanks to Ralf Hülsmann for contributing! 216 | 217 | ### Gather 218 | 219 | [Gather](https://brettterpstra.com/projects/gather-cli/) is a tool for converting web pages to Markdown. You can use it with mdless to create a Lynx-style web browser: 220 | 221 | ```bash 222 | $ gather https://brettterpstra.com/projects/gather-cli/ | mdless 223 | ``` 224 | 225 | ### fzf 226 | 227 | [fzf](https://github.com/junegunn/fzf) is a tool for selecting files and other menu options with typeahead fuzzy matching. You can set up `mdless` as a previewer when selecting Markdown or TaskPaper files. 228 | 229 | ``` 230 | $ ls *.md | fzf --preview 'mdless {}' 231 | ``` 232 | 233 | ### Fish 234 | 235 | You can replace the cat command in Fish by creating the following functions in `~/.config/fish/functions` 236 | 237 | `get_ext.fish` 238 | 239 | ```fish 240 | function get_ext -d 'Get the file extension from the argument' 241 | set -l splits (string split "." $argv) 242 | echo $splits[-1] 243 | end 244 | ``` 245 | 246 | `cat.fish` 247 | 248 | ```fish 249 | function cat -d "Use bat instead of cat unless it's a Markdown file, then use mdless" 250 | set -l exts md markdown txt taskpaper 251 | 252 | if not test -f $argv[-1] 253 | echo "File not found: $argv[-1]" 254 | return 0 255 | end 256 | 257 | if contains (get_ext $argv[-1]) $exts 258 | mdless $argv 259 | else 260 | command bat --style plain --theme OneHalfDark $argv 261 | end 262 | end 263 | ``` 264 | 265 | Note that if you do this, and you need uncolored output to pipe somewhere, you'll need to use `command cat FILE` to revert to the built-in `cat`. Otherwise your text will be full of the escape codes that `mdless` uses to colorize the output. 266 | 267 | ## Similar Projects 268 | 269 | There are a few great options for Markdown viewing in the Terminal. If `mdless` doesn't do it for you, check out: 270 | 271 | - [Glow](https://github.com/charmbracelet/glow) 272 | - [Frogmouth](https://github.com/Textualize/frogmouth) 273 | 274 | 275 | -------------------------------------------------------------------------------- /test/Byword.md: -------------------------------------------------------------------------------- 1 | Title: Byword MultiMarkdown Guide 2 | Author: The Byword Team 3 | Email: byword@metaclassy.com 4 | Date: May 29, 2011 5 | 6 | # Byword MultiMarkdown Guide 7 | 8 | --- 9 | 10 | ## Summary [section-summary] 11 | 12 | * [Introduction][section-introduction] 13 | * [Syntax reference][section-syntax] 14 | * [Editing markdown documents][section-editing] 15 | * [Preview mode][section-preview] 16 | * [Exporting documents][section-export] 17 | * [MultiMarkdown highlights][section-mmd] 18 | 19 | --- 20 | 21 |
22 | Written and generated with Byword (view source) 23 |
24 | 25 | byword@metaclassy.com 26 | 27 | --- 28 | 29 | ## Introduction [section-introduction] 30 | 31 | Markdown is a text formatting syntax inspired on plain text email. It is extremely simple, memorizable and visually lightweight on artifacts so as not to hinder reading -- characteristics that go hand in hand with the essence of **Byword**. 32 | 33 | In the words of its creator, [John Gruber][link-gruber]: 34 | > The idea is that a Markdown-formatted document should be publishable as-is, as plain text, without looking like it has been marked up with tags or formatting instructions. 35 | 36 | [link-gruber]: http://daringfireball.net/ 37 | 38 | In the next sections you'll be guided through some of the features that will make **Byword** your new favorite Markdown editor. 39 | 40 | --- 41 | 42 | ## Syntax Reference [section-syntax] 43 | 44 | If you're unfamiliar with Markdown's syntax, please spare a couple of minutes going through the [syntax guide][link-syntax]. Otherwise, just go ahead and skip to the next section. 45 | 46 | [link-syntax]: syntax.html "Markdown syntax guide" 47 | 48 | --- 49 | 50 | ## Editing Markdown documents [section-editing] 51 | 52 | This section is dedicated to introduce you to the differences between editing plain/rich text documents and Markdown documents. 53 | 54 | ### Creating new documents [section-editing-create] 55 | 56 | To create a Markdown document, head to the File menu and select "New Markdown document" or simply press the shortcut ⇧⌘N. 57 | 58 | > **NOTE** 59 | > You can convert a plain text document to a Markdown document by going to the "Format" menu and pressing ⌥ to reveal Markdown conversion option or pressing the combination ⌥⇧⌘T. 60 | > 61 | > To confirm that you're editing in Markdown mode, look at the counters at the bottom of your screen. If the counters are not visible, you can enable them by using the shortcut ⇧⌘K . 62 | 63 | ### Opening documents [section-editing-open] 64 | 65 | Markdown documents are opened like any other document, but **Byword** will only recognize and activate Markdown features if the file is bearing a well-known extension. 66 | 67 | The recognized extensions are `.md`, `.markdown`, `.mdown` and `.markdn`. 68 | 69 | If the document does not have one of these well-known extensions, you can always enable Markdown features by converting the file (⌥⇧⌘T). 70 | 71 | > **NOTE** 72 | > While Markdown does not have an official extension we recommend the usage of `.md`, as it's the most widely adopted one. 73 | 74 | ### Handy shortcuts [section-editing-shortcuts] 75 | 76 | Even though Markdown's formatting syntax is light, there are a couple of commonly used style artifacts that force your hands out of their natural stance when typing -- **bold** and *italic*. 77 | 78 | **Byword** preserves the hotkeys widely used for these effects. If you're about to write a word in bold or italic, just type ⌘B or ⌘I and it will place the corresponding formatting elements in place and advance the cursor. You can also select a word and apply the style or, conversely, select a word wrapped by these styles and **Byword** will remove them for you. 79 | 80 | ### Images [section-editing-images] 81 | 82 | If you drag images into the text, they will automatically be replaced by a Markdown reference to the file. 83 | 84 | Due to **Byword**'s MultiMarkdown support you can even add custom attributes to your images, altering the way they're displayed. Please refer to [Custom attributes][section-mmd-attributes] section on the MultiMarkdown highlights chapter for more details. 85 | 86 | > **NOTE** 87 | > Keep in mind that when dragging images to the text, **Byword** will introduce a reference to that file's location on your disk (noticeable by the `file:` prefix). 88 | > When publishing online, make sure you update this reference, otherwise you'll run into broken links. 89 | 90 | --- 91 | 92 | ## Preview mode [section-preview] 93 | 94 | Markdown is often used as source to generate documents under more commonly used publishing formats like HTML. The fact that it's an extremely simple, plain text based formatting syntax pretty much turns any text editor into a Markdown editor. 95 | 96 | **Byword** expands the concept of a markdown editor by giving you the option to preview your text. At the distance of a shortcut (⌥⌘P), you can get a feel of how your writings will look like. 97 | 98 | ![Byword in preview mode][img-preview] 99 | 100 | [img-preview]: img/preview.png "Byword in preview mode" class="shadow" 101 | 102 | The preview mode will render the text using your current style settings. To dismiss this mode and go back to editing, just hit the Escape key. 103 | 104 | --- 105 | 106 | ## Exporting documents [section-export] 107 | 108 | In the vast majority of times, you will be using Markdown for its *raison d'être* -- as a source format to generate HTML. **Byword** let's you export the HTML output in two ways: 109 | 110 | ![Exporting options][img-export] 111 | 112 | [img-export]: img/export.png "Export options" 113 | 114 | * Copy the HTML output directly to[^test] your clipboard -- so you can conveniently paste it into your favorite HTML editor[^fn-export]; 115 | * Export to a file. 116 | 117 | [^test]: This is a little test. 118 | 119 | [^fn-export]: When copying to clipboard, **Byword** will only place the equivalent of the `body` tag contents. On the other hand, when exporting to a file, a complete HTML file will be generated. 120 | 121 | We know how much you love **Byword**'s aesthetics so we even added a little bonus to the option of exporting to a file. 122 | 123 | ![Exporting with Byword's current theme][img-export_theme] 124 | 125 | [img-export_theme]: img/export_theme.png "Exporting with Byword's current theme" 126 | 127 | Including **Byword**'s theme in the exported file will give you an exact copy of what you see in the preview mode. With this option enabled, font type, size and text width will be preserved when the output file is generated. 128 | 129 | --- 130 | 131 | ## MultiMarkdown highlights [section-mmd] 132 | 133 | As useful as Markdown is on its own, MultiMarkdown extends it with many features. This section will briefly introduce you to the most interesting of them. 134 | 135 | > **NOTE** 136 | > For a comprehensive reference, please refer to Fletcher T. Penney's [MultiMarkdown user guide][link-mmd_userguide]. 137 | 138 | [link-mmd_userguide]: https://github.com/fletcher/MultiMarkdown/blob/master/Documentation/MultiMarkdown%20User%27s%20Guide.md "Fletcher T. Penney's MultiMarkdown user guide" 139 | 140 | 141 | ### Cross-references [section-mmd-xrefs] 142 | 143 | Cross-references will become your new best friend when writing long documents. They will highly improve the navigability of the generated documents by giving the reader links to jump across sections with a single click. 144 | 145 | #### Example [section-mmd-xrefs-example] 146 | 147 | Clicking [here][section-preview] will lead you to the **Preview** section. 148 | 149 | #### Result [section-mmd-xrefs-result] 150 | 151 | Clicking [here][section-preview] will lead you do the **Preview** section. 152 | 153 | 154 | ### Footnotes [section-mmd-footnotes] 155 | 156 | Footnotes are a simple, yet effective way of conveying non-crucial information to the reader. 157 | 158 | Rather than parenthesizing a side note or place it between em-dashes -- as unimportant as it is, the reader will go through it, just like you did now -- you can defer its reading and expand on your thoughts there. 159 | 160 | #### Example 161 | 162 | Clicking this number[^fn-sample_footnote] will lead you to a footnote. 163 | 164 | [^fn-sample_footnote]: Handy! Now click the return link to go back. 165 | 166 | #### Result [section-mmd-footnotes-result] 167 | 168 | Clicking this number[^fn-sample_footnote] will lead you to a footnote. 169 | 170 | [^fn-sample_footnote]: Handy! Now click the return link to go back. 171 | 172 | 173 | ### Custom attributes [section-mmd-attributes] 174 | 175 | MultiMarkdown introduces an unobtrusive way of adding custom attributes to images and links, allowing you to change they way they are displayed. 176 | 177 | > **NOTE** 178 | > This is not available for inline links or images. 179 | 180 | #### Example [section-mmd-attributes-example] 181 | 182 | The original image is 128x128 and contains no shadow. 183 | ![Original icon][img-icon_original] 184 | 185 | It will be displayed as 96x96 with a subtle shadow. 186 | ![Styled icon][img-icon_styled] 187 | 188 | [img-icon_original]: img/icon128.png "B" 189 | [img-icon_styled]: img/icon128.png "B" width="96px" height="96px" 190 | class="shadow" 191 | 192 | #### Result [section-mmd-attributes-result] 193 | 194 | The original image is 128x128 and contains no shadow. 195 | 196 | ![Original icon][img-icon_original] 197 | 198 | It will be displayed as 96x96 with a subtle shadow. 199 | 200 | ![Styled icon][img-icon_styled] 201 | 202 | [img-icon_original]: img/icon128.png "A" 203 | [img-icon_styled]: img/icon128.png "B" width="96px" height="96px" class="shadow" 204 | 205 | 206 | ### Meta information [section-mmd-meta] 207 | 208 | With MultiMarkdown, you can also embed metadata on your documents. 209 | 210 | Metadata must be placed at the top of the document -- there can be no white-spaces before -- and it ends with the first empty line. Each entry is composed of key and values, separated by a colon (`:`). 211 | 212 | There are plenty of keys supported, some of the most common being `Title`, `Author`, `Date`, `Copyright`, `Keywords` and `Email`. Be sure to check [Fletcher's guide][link-mmd_userguide] for a full reference. 213 | 214 | > **TIP** 215 | > When adding metadata information to your documents, make sure you always leave two spaces at the end of each metadata line. This will ensure that exporting to plain Markdown will result in a properly formatted piece of text -- as opposed to a single run-on paragraph. 216 | 217 | #### Example [section-mmd-meta] 218 | 219 | Title: Document title 220 | Author: John Doe 221 | Jane Doe 222 | Date: January 1st, 2012 223 | 224 | 225 | ### Tables [section-mmd-tables] 226 | 227 | Tables are perfect to display structured data in rows and columns. MultiMarkdown supports the generation of tables by using a couple of simple rules alongside the use of the pipe character -- `|`. 228 | 229 | #### Example [section-mmd-tables-example] 230 | 231 | | First Header | Second Header | Third Header | 232 | | :------------ | :-----------: | -------------------: | 233 | | First row | Data | Very long data entry | 234 | | Second row | **Cell** | *Cell* | 235 | | Third row | Cell that spans across two columns || 236 | [Table caption, works as a reference][section-mmd-tables-table1] 237 | 238 | #### Result [section-mmd-tables-result] 239 | 240 | | First Header | Second Header | Third Header | 241 | | :------------ | :-----------: | -------------------: | 242 | | First row | Data | Very long data entry | 243 | | Second row | **Cell** | *Cell* | 244 | | Third row | Cell that spans across two columns || 245 | [Table caption, works as a reference][section-mmd-tables-table1] 246 | 247 | #### Structure [section-mmd-tables-structure] 248 | 249 | If you are familiar with HTML tables, you'll instantly recognize the structure of the table syntax. All tables must begin with one or more **rows** of **headers**, and each **row** may have one or more **columns**. 250 | 251 | These are the most important rules you'll be dealing with: 252 | 253 | * There must be at least one `|` per line; 254 | * After the header rows, there must be a line containing only `|`, `-`, `:`, `.`, or spaces; 255 | * Cell content must be on one line only; 256 | * Columns are separated by `|`. 257 | 258 | #### Alignment [section-mmd-tables-alignment] 259 | 260 | To align the data cells on the table, you need to introduce a special row right after the headers, that will determine how the following rows -- the data rows -- will be aligned. 261 | 262 | | Header One | Header Two | Header Three | Header Four | 263 | | ---------- | :--------- | :----------: | ----------: | 264 | | Default | Left | Center | Right | 265 | 266 | | Header One | Header Two | Header Three | Header Four | 267 | | ---------- | :--------- | :----------: | ----------: | 268 | | Default | Left | Center | Right | 269 | 270 | The placing of the colon (`:`) is optional and determines the alignment of columns in the data rows. This line is mandatory and must be placed between the headers and the data rows. 271 | 272 | Also, the usage of the `|` at the beginning or end of the rows is optional -- as long as at least one `|` is present in each row. 273 | 274 | #### Column spanning [section-mmd-tables-colspanning] 275 | 276 | To make a cell span across multiple columns, instead of using a single pipe (`|`) character to delimit that cell, use the number of pipes corresponding to the columns you wish to span. 277 | 278 | | Column 1 | Column 2 | Column 3 | Column 4 | 279 | | -------- | :------: | -------- | -------- | 280 | | No span | Span across three columns ||| 281 | 282 | | Column 1 | Column 2 | Column 3 | Column 4 | 283 | | -------- | :------: | -------- | -------- | 284 | | No span | Span across three columns ||| 285 | 286 | > **NOTE** 287 | > This is only an introduction to MultiMarkdown's tables. For the full reference, please refer to the "Tables" section on the [MultiMarkdown user guide][link-mmd_userguide]. 288 | 289 | --- 290 | 291 | [link-source]: guide.md "User guide MultiMarkdown source" 292 | 293 | If you have any doubts don't hesitate to contact us via email at **byword@metaclassy.com** or via Twitter at [@bywordapp][link-twitter_bywordapp] or [@metaclassy][link-twitter_metaclassy]. 294 | 295 | [link-twitter_bywordapp]: http://twitter.com/bywordapp "Byword on Twitter" 296 | [link-twitter_metaclassy]: http://twitter.com/metaclassy "Metaclassy on Twitter" 297 | 298 | Enjoy, 299 | The Byword team. 300 | -------------------------------------------------------------------------------- /test/riot-web.md: -------------------------------------------------------------------------------- 1 | Riot 2 | ==== 3 | 4 | Riot (formerly known as Vector) is a ==Matrix web client== built using the [Matrix React SDK](https://github.com/matrix-org/matrix-react-sdk). 5 | 6 | Riot is officially supported on the web in modern versions of Chrome, Firefox, and Safari. Other browsers may work, however 7 | official support is not provided. For accessing Riot on an Android or iOS device, check out [riot-android](https://github.com/vector-im/riot-android) 8 | and [riot-ios](https://github.com/vector-im/riot-ios) - riot-web does not support mobile devices. 9 | 10 | Getting Started 11 | =============== 12 | 13 | The easiest way to test Riot is to just use the hosted copy at https://riot.im/app. 14 | The `develop` branch is continuously deployed by Jenkins at https://riot.im/develop 15 | for those who like living dangerously. 16 | 17 | To host your own copy of Riot, the quickest bet is to use a pre-built 18 | released version of Riot: 19 | 20 | 1. Download the latest version from https://github.com/vector-im/riot-web/releases 21 | 1. Untar the tarball on your web server 22 | 1. Move (or symlink) the `riot-x.x.x` directory to an appropriate name 23 | 1. If desired, copy `config.sample.json` to `config.json` and edit it 24 | as desired. See the [configuration docs](docs/config.md) for details. 25 | 1. Enter the URL into your browser and log into Riot! 26 | 27 | Releases are signed using gpg and the OpenPGP standard, and can be checked against the public key located 28 | at https://packages.riot.im/riot-release-key.asc. 29 | 30 | Note that for the security of your chats will need to serve Riot 31 | over HTTPS. Major browsers also do not allow you to use VoIP/video 32 | chats over HTTP, as WebRTC is only usable over HTTPS. 33 | There are some exceptions like when using localhost, which is 34 | considered a [secure context](https://developer.mozilla.org/docs/Web/Security/Secure_Contexts) 35 | and thus allowed. 36 | 37 | To install Riot as a desktop application, see [Running as a desktop 38 | app](#running-as-a-desktop-app) below. 39 | 40 | Important Security Note 41 | ======================= 42 | 43 | We do not recommend running Riot from the same domain name as your Matrix 44 | homeserver. The reason is the risk of XSS (cross-site-scripting) 45 | vulnerabilities that could occur if someone caused Riot to load and render 46 | malicious user generated content from a Matrix API which then had trusted 47 | access to Riot (or other apps) due to sharing the same domain. 48 | 49 | We have put some coarse mitigations into place to try to protect against this 50 | situation, but it's still not good practice to do it in the first place. See 51 | https://github.com/vector-im/riot-web/issues/1977 for more details. 52 | 53 | The same applies for end-to-end encrypted content, but since this is decrypted 54 | on the client, Riot needs a way to supply the decrypted content from a separate 55 | origin to the one Riot is hosted on. This currently done with a 'cross origin 56 | renderer' which is a small piece of javascript hosted on a different domain. 57 | To avoid all Riot installs needing one of these to be set up, riot.im hosts 58 | one on usercontent.riot.im which is used by default. 59 | https://github.com/vector-im/riot-web/issues/6173 tracks progress on replacing 60 | this with something better. 61 | 62 | Building From Source 63 | ==================== 64 | 65 | Riot is a modular webapp built with modern ES6 and uses a Node.js build system. 66 | Ensure you have the latest LTS version of Node.js installed. 67 | 68 | Using `yarn` instead of `npm` is recommended. Please see the Yarn [install 69 | guide](https://yarnpkg.com/docs/install/) if you do not have it already. 70 | 71 | 1. Install or update `node.js` so that your `node` is at least v10.x. 72 | 1. Install `yarn` if not present already. 73 | 1. Clone the repo: `git clone https://github.com/vector-im/riot-web.git`. 74 | 1. Switch to the riot-web directory: `cd riot-web`. 75 | 1. Install the prerequisites: `yarn install`. 76 | 1. If you're using the `develop` branch then it is recommended to set up a proper development environment ("Setting up a dev environment" below) however one can install the develop versions of the dependencies instead: 77 | 78 | ```bash 79 | scripts/fetch-develop.deps.sh 80 | ``` 81 | 82 | Whenever you git pull on `riot-web` you will also probably need to force an update 83 | to these dependencies - the simplest way is to re-run the script, but you can also 84 | manually update and rebuild them: 85 | 86 | ```bash 87 | cd matrix-js-sdk 88 | git pull 89 | yarn install # re-run to pull in any new dependencies 90 | cd ../matrix-react-sdk 91 | git pull 92 | yarn install 93 | ``` 94 | 95 | Or just use https://riot.im/develop - the continuous integration release of the 96 | develop branch. (Note that we don't reference the develop versions in git directly 97 | due to https://github.com/npm/npm/issues/3055.) 98 | 1. Configure the app by copying `config.sample.json` to `config.json` and 99 | modifying it. See the [configuration docs](docs/config.md) for details. 100 | 1. `yarn dist` to build a tarball to deploy. Untaring this file will give 101 | a version-specific directory containing all the files that need to go on your 102 | web server. 103 | 104 | Note that `yarn dist` is not supported on Windows, so Windows users can run `yarn build`, 105 | which will build all the necessary files into the `webapp` directory. The version of Riot 106 | will not appear in Settings without using the dist script. You can then mount the 107 | `webapp` directory on your webserver to actually serve up the app, which is entirely static content. 108 | 109 | Running as a Desktop app 110 | ======================== 111 | 112 | Riot can also be run as a desktop app, wrapped in Electron. You can download a 113 | pre-built version from https://riot.im/download/desktop/ or, if you prefer, 114 | build it yourself. 115 | 116 | To build it yourself, follow the instructions below. 117 | 118 | 1. Follow the instructions in 'Building From Source' above, but run 119 | `yarn build` instead of `yarn dist` (since we don't need the tarball). 120 | 2. Install Electron and run it: 121 | 122 | ```bash 123 | yarn electron 124 | ``` 125 | 126 | To build packages, use `electron-builder`. This is configured to output: 127 | 128 | * `dmg` + `zip` for macOS 129 | * `exe` + `nupkg` for Windows 130 | * `deb` for Linux 131 | 132 | But this can be customised by editing the `build` section of package.json 133 | as per https://github.com/electron-userland/electron-builder/wiki/Options 134 | 135 | See https://github.com/electron-userland/electron-builder/wiki/Multi-Platform-Build 136 | for dependencies required for building packages for various platforms. 137 | 138 | The only platform that can build packages for all three platforms is macOS: 139 | ```bash 140 | brew install mono 141 | yarn install 142 | yarn build:electron 143 | ``` 144 | 145 | For other packages, use `electron-builder` manually. For example, to build a 146 | package for 64 bit Linux: 147 | 148 | 1. Follow the instructions in 'Building From Source' above 149 | 2. `node_modules/.bin/build -l --x64` 150 | 151 | All Electron packages go into `electron_app/dist/` 152 | 153 | Many thanks to @aviraldg for the initial work on the Electron integration. 154 | 155 | Other options for running as a desktop app: 156 | 157 | * see @asdf:matrix.org points out that you can use nativefier and it just works @testing @hello(tm) 158 | 159 | ```bash 160 | yarn global add nativefier 161 | nativefier https://riot.im/app/ 162 | ``` 163 | 164 | The [configuration docs](docs/config.md#desktop-app-configuration) show how to 165 | override the desktop app's default settings if desired. 166 | 167 | Running from Docker 168 | =================== 169 | 170 | The Docker image can be used to serve riot-web as a web server. The easiest way to use 171 | it is to use the prebuilt image: 172 | ```bash 173 | docker run -p 80:80 vectorim/riot-web 174 | ``` 175 | 176 | To supply your own custom `config.json`, map a volume to `/app/config.json`. For example, 177 | if your custom config was located at `/etc/riot-web/config.json` then your Docker command 178 | would be: 179 | ```bash 180 | docker run -p 80:80 -v /etc/riot-web/config.json:/app/config.json vectorim/riot-web 181 | ``` 182 | 183 | To build the image yourself: 184 | ```bash 185 | git clone https://github.com/vector-im/riot-web.git riot-web 186 | cd riot-web 187 | git checkout master 188 | docker build -t vectorim/riot-web . 189 | ``` 190 | 191 | If you're building a custom branch, or want to use the develop branch, check out the appropriate 192 | riot-web branch and then run: 193 | ```bash 194 | docker build -t vectorim/riot-web:develop \ 195 | --build-arg USE_CUSTOM_SDKS=true \ 196 | --build-arg REACT_SDK_REPO="https://github.com/matrix-org/matrix-react-sdk.git" \ 197 | --build-arg REACT_SDK_BRANCH="develop" \ 198 | --build-arg JS_SDK_REPO="https://github.com/matrix-org/matrix-js-sdk.git" \ 199 | --build-arg JS_SDK_BRANCH="develop" \ 200 | . 201 | ``` 202 | 203 | config.json 204 | =========== 205 | 206 | Riot supports a variety of settings to configure default servers, behaviour, themes, etc. 207 | See the [configuration docs](docs/config.md) for more details. 208 | 209 | Labs Features 210 | ============= 211 | 212 | Some features of Riot may be enabled by flags in the `Labs` section of the settings. 213 | Some of these features are described in [labs.md](https://github.com/vector-im/riot-web/blob/develop/docs/labs.md). 214 | 215 | Development 216 | =========== 217 | 218 | Before attempting to develop on Riot you **must** read the [developer guide 219 | for `matrix-react-sdk`](https://github.com/matrix-org/matrix-react-sdk), which 220 | also defines the design, architecture and style for Riot too. 221 | 222 | You should also familiarise yourself with the ["Here be Dragons" guide 223 | ](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM) 224 | to the tame & not-so-tame dragons (gotchas) which exist in the codebase. 225 | 226 | The idea of Riot is to be a relatively lightweight "skin" of customisations on 227 | top of the underlying `matrix-react-sdk`. `matrix-react-sdk` provides both the 228 | higher and lower level React components useful for building Matrix communication 229 | apps using React. 230 | 231 | After creating a new component you must run `yarn reskindex` to regenerate 232 | the `component-index.js` for the app (used in future for skinning). 233 | 234 | Please note that Riot is intended to run correctly without access to the public 235 | internet. So please don't depend on resources (JS libs, CSS, images, fonts) 236 | hosted by external CDNs or servers but instead please package all dependencies 237 | into Riot itself. 238 | 239 | Setting up a dev environment 240 | ============================ 241 | 242 | Much of the functionality in Riot is actually in the `matrix-react-sdk` and 243 | `matrix-js-sdk` modules. It is possible to set these up in a way that makes it 244 | easy to track the `develop` branches in git and to make local changes without 245 | having to manually rebuild each time. 246 | 247 | First clone and build `matrix-js-sdk`: 248 | 249 | ``` bash 250 | git clone https://github.com/matrix-org/matrix-js-sdk.git 251 | pushd matrix-js-sdk 252 | git checkout develop 253 | yarn link 254 | yarn install 255 | popd 256 | ``` 257 | 258 | Then similarly with `matrix-react-sdk`: 259 | 260 | ```bash 261 | git clone https://github.com/matrix-org/matrix-react-sdk.git 262 | pushd matrix-react-sdk 263 | git checkout develop 264 | yarn link 265 | yarn link matrix-js-sdk 266 | yarn install 267 | popd 268 | ``` 269 | 270 | Finally, build and start Riot itself: 271 | 272 | ```bash 273 | git clone https://github.com/vector-im/riot-web.git 274 | cd riot-web 275 | git checkout develop 276 | yarn link matrix-js-sdk 277 | yarn link matrix-react-sdk 278 | yarn install 279 | yarn start 280 | ``` 281 | 282 | Wait a few seconds for the initial build to finish; you should see something like: 283 | ``` 284 | Hash: b0af76309dd56d7275c8 285 | Version: webpack 1.12.14 286 | Time: 14533ms 287 | Asset Size Chunks Chunk Names 288 | bundle.js 4.2 MB 0 [emitted] main 289 | bundle.css 91.5 kB 0 [emitted] main 290 | bundle.js.map 5.29 MB 0 [emitted] main 291 | bundle.css.map 116 kB 0 [emitted] main 292 | + 1013 hidden modules 293 | ``` 294 | Remember, the command will not terminate since it runs the web server 295 | and rebuilds source files when they change. This development server also 296 | disables caching, so do NOT use it in production. 297 | 298 | Open http://127.0.0.1:8080/ in your browser to see your newly built Riot. 299 | 300 | ___ 301 | 302 | When you make changes to `matrix-react-sdk` or `matrix-js-sdk` they should be 303 | automatically picked up by webpack and built. 304 | 305 | If you add or remove any components from the Riot skin, you will need to rebuild 306 | the skin's index by running, `yarn reskindex`. 307 | 308 | If any of these steps error with, `file table overflow`, you are probably on a mac 309 | which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again. 310 | You'll need to do this in each new terminal you open before building Riot. 311 | 312 | Running the tests 313 | ----------------- 314 | 315 | There are a number of application-level tests in the `tests` directory; these 316 | are designed to run in a browser instance under the control of 317 | [karma](https://karma-runner.github.io). To run them: 318 | 319 | * Make sure you have Chrome installed (a recent version, like 59) 320 | * Make sure you have `matrix-js-sdk` and `matrix-react-sdk` installed and 321 | built, as above 322 | * `yarn test` 323 | 324 | The above will run the tests under Chrome in a `headless` mode. 325 | 326 | You can also tell karma to run the tests in a loop (every time the source 327 | changes), in an instance of Chrome on your desktop, with `yarn test-multi`. This also gives you the option of running the tests in 'debug' 328 | mode, which is useful for stepping through the tests in the developer tools. 329 | 330 | Translations 331 | ============ 332 | 333 | To add a new translation, head to the [translating doc](docs/translating.md). 334 | 335 | For a developer guide, see the [translating dev doc](docs/translating-dev.md). 336 | 337 | [translationsstatus](https://translate.riot.im/engage/riot-web/?utm_source=widget) 338 | 339 | Triaging issues 340 | =============== 341 | 342 | Issues will be triaged by the core team using the below set of tags. 343 | 344 | Tags are meant to be used in combination - e.g.: 345 | * P1 critical bug == really urgent stuff that should be next in the bugfixing todo list 346 | * "release blocker" == stuff which is blocking us from cutting the next release. 347 | * P1 feature type:voip == what VoIP features should we be working on next? 348 | 349 | priority: **compulsory** 350 | 351 | * P1: top priority - i.e. pool of stuff which we should be working on next 352 | * P2: still need to fix, but lower than P1 353 | * P3: non-urgent 354 | * P4: interesting idea - bluesky some day 355 | * P5: recorded for posterity/to avoid duplicates. No intention to resolves right now. 356 | 357 | bug or feature: **compulsory** 358 | 359 | * bug 360 | * feature 361 | 362 | bug severity: **compulsory, if bug** 363 | 364 | * critical - whole app doesn't work 365 | * major - entire feature doesn't work 366 | * minor - partially broken feature (but still usable) 367 | * cosmetic - feature works functionally but UI/UX is broken 368 | 369 | types 370 | * type:* - refers to a particular part of the app; used to filter bugs 371 | on a given topic - e.g. VOIP, signup, timeline, etc. 372 | 373 | additional categories (self-explanatory): 374 | 375 | * release blocker 376 | * ui/ux (think of this as cosmetic) 377 | * network (specific to network conditions) 378 | * platform specific 379 | * accessibility 380 | * maintenance 381 | * performance 382 | * i18n 383 | * blocked - whether this issue currently can't be progressed due to outside factors 384 | 385 | community engagement 386 | * easy 387 | * hacktoberfest 388 | * bounty? - proposal to be included in a bounty programme 389 | * bounty - included in Status Open Bounty 390 | -------------------------------------------------------------------------------- /lib/mdless/converter.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "yaml" 3 | 4 | module CLIMarkdown 5 | class Converter 6 | include Colors 7 | 8 | def version 9 | "#{CLIMarkdown::EXECUTABLE_NAME} #{CLIMarkdown::VERSION}" 10 | end 11 | 12 | def default(option, default) 13 | MDLess.options[option] = default if MDLess.options[option].nil? 14 | end 15 | 16 | def initialize(args) 17 | MDLess.log.level = Logger::WARN 18 | 19 | MDLess.options = {} 20 | config = File.expand_path("~/.config/mdless/config.yml") 21 | MDLess.options = YAML.load(IO.read(config)) if File.exist?(config) 22 | 23 | optparse = OptionParser.new do |opts| 24 | opts.banner = "#{version} by Brett Terpstra\n\n> Usage: #{CLIMarkdown::EXECUTABLE_NAME} [options] [path]\n\n" 25 | 26 | default(:color, true) 27 | opts.on("-c", "--[no-]color", "Colorize output (default on)") do |c| 28 | MDLess.options[:color] = c 29 | end 30 | 31 | opts.on("-d", "--debug LEVEL", "Level of debug messages to output (1-4, 4 to see all messages)") do |level| 32 | if level.to_i.positive? && level.to_i < 5 33 | MDLess.log.level = 5 - level.to_i 34 | else 35 | puts "Error: Debug level out of range (1-4)" 36 | Process.exit 1 37 | end 38 | end 39 | 40 | opts.on("-h", "--help", "Display this screen") do 41 | puts opts 42 | exit 43 | end 44 | 45 | default(:local_images, false) 46 | default(:remote_images, false) 47 | opts.on("-i", "--images=TYPE", 48 | "Include [local|remote (both)|none] images in output" \ 49 | " (requires chafa or imgcat, default none).") do |type| 50 | if exec_available("imgcat") || exec_available("chafa") 51 | case type 52 | when /^(r|b|a)/i 53 | MDLess.options[:local_images] = true 54 | MDLess.options[:remote_images] = true 55 | when /^l/i 56 | MDLess.options[:local_images] = true 57 | when /^n/ 58 | MDLess.options[:local_images] = false 59 | MDLess.options[:remote_images] = false 60 | end 61 | else 62 | MDLess.log.warn("images turned on but imgcat/chafa not found") 63 | end 64 | end 65 | 66 | opts.on("-I", "--all-images", "Include local and remote images in output" \ 67 | " (requires imgcat or chafa)") do 68 | if exec_available("imgcat") || exec_available("chafa") # && ENV['TERM_PROGRAM'] == 'iTerm.app' 69 | MDLess.options[:local_images] = true 70 | MDLess.options[:remote_images] = true 71 | else 72 | MDLess.log.warn("images turned on but imgcat/chafa not found") 73 | end 74 | end 75 | 76 | default(:list, false) 77 | opts.on("-l", "--list", "List headers in document and exit") do 78 | MDLess.options[:list] = true 79 | end 80 | 81 | default(:pager, true) 82 | opts.on("-p", "--[no-]pager", "Formatted output to pager (default on)") do |p| 83 | MDLess.options[:pager] = p 84 | end 85 | 86 | default(:pager, true) 87 | opts.on("-P", "Disable pager (same as --no-pager)") do 88 | MDLess.options[:pager] = false 89 | end 90 | 91 | default(:section, nil) 92 | opts.on("-s", "--section=NUMBER[,NUMBER]", 93 | "Output only a headline-based section of the input (numeric from --list or text match)") do |section| 94 | sections = section.split(/ *, */).map(&:strip) 95 | MDLess.options[:section] = sections.map do |sect| 96 | if sect =~ /^\d+$/ 97 | sect.to_i 98 | else 99 | sect 100 | end 101 | end 102 | end 103 | 104 | default(:theme, "default") 105 | opts.on("-t", "--theme=THEME_NAME", "Specify an alternate color theme to load") do |theme| 106 | MDLess.options[:theme] = theme 107 | end 108 | 109 | default(:at_tags, false) 110 | opts.on("-@", "--[no-]at-tags", "Highlight @tags and values in the document") do |opt| 111 | MDLess.options[:at_tags] = opt 112 | end 113 | 114 | opts.on("-v", "--version", "Display version number") do 115 | puts version 116 | exit 117 | end 118 | 119 | default(:width, 0) 120 | opts.on("-w", "--width=COLUMNS", "Column width to format for (default: 0 -> terminal width)") do |columns| 121 | columns = columns.to_i 122 | cols = TTY::Screen.cols 123 | MDLess.cols = columns > 2 ? columns - 2 : cols 124 | 125 | MDLess.options[:width] = columns > cols ? cols - 2 : columns - 2 126 | end 127 | 128 | MDLess.cols = MDLess.options[:width] 129 | MDLess.cols = TTY::Screen.cols - 2 if MDLess.cols.zero? 130 | 131 | default(:autolink, true) 132 | opts.on("--[no-]autolink", "Convert bare URLs and emails to ") do |p| 133 | MDLess.options[:autolink] = p 134 | end 135 | 136 | opts.on("--config", "Open the config file in default editor") do 137 | raise "No $EDITOR defined" unless ENV["EDITOR"] 138 | 139 | `#{ENV["EDITOR"]} '#{File.expand_path("~/.config/mdless/config.yml")}'` 140 | end 141 | 142 | opts.on("--changes", "Open the changelog to see recent updates") do 143 | changelog = File.join(File.dirname(__FILE__), "..", "..", "CHANGELOG.md") 144 | system "mdless --linebreaks '#{changelog}'" 145 | Process.exit 0 146 | end 147 | 148 | opts.on("--edit-theme", "Open the default/specified theme in default editor, " \ 149 | "populating a new theme if needed. Use after --theme in the command.") do 150 | raise "No $EDITOR defined" unless ENV["EDITOR"] 151 | 152 | theme = MDLess.options[:theme] =~ /default/ ? "mdless" : MDLess.options[:theme] 153 | theme = File.expand_path("~/.config/mdless/#{theme}.theme") 154 | File.open(theme, "w") { |f| f.puts(YAML.dump(MDLess.theme)) } unless File.exist?(theme) 155 | `#{ENV["EDITOR"]} '#{theme}'` 156 | Process.exit 0 157 | end 158 | 159 | default(:inline_footnotes, false) 160 | opts.on("--[no-]inline-footnotes", 161 | "Display footnotes immediately after the paragraph that references them") do |p| 162 | MDLess.options[:inline_footnotes] = p 163 | end 164 | 165 | default(:intra_emphasis, true) 166 | opts.on("--[no-]intra-emphasis", "Parse emphasis inside of words (e.g. Mark_down_)") do |opt| 167 | MDLess.options[:intra_emphasis] = opt 168 | end 169 | 170 | default(:lax_spacing, true) 171 | opts.on("--[no-]lax-spacing", "Allow lax spacing") do |opt| 172 | MDLess.options[:lax_spacing] = opt 173 | end 174 | 175 | default(:links, :inline) 176 | opts.on("--links=FORMAT", 177 | "Link style ([*inline, reference, paragraph]," \ 178 | ' "paragraph" will position reference links after each paragraph)') do |fmt| 179 | MDLess.options[:links] = case fmt 180 | when /^:?r/i 181 | :reference 182 | when /^:?p/i 183 | :paragraph 184 | else 185 | :inline 186 | end 187 | end 188 | 189 | default(:preserve_linebreaks, true) 190 | opts.on("--[no-]linebreaks", "Preserve line breaks") do |opt| 191 | MDLess.options[:preserve_linebreaks] = opt 192 | end 193 | 194 | default(:mmd_metadata, true) 195 | opts.on("--[no-]metadata", "Replace [%key] with values from metadata") do |opt| 196 | MDLess.options[:mmd_metadata] = opt 197 | end 198 | 199 | default(:syntax_higlight, false) 200 | opts.on("--[no-]syntax", "Syntax highlight code blocks") do |opt| 201 | MDLess.options[:syntax_higlight] = opt 202 | end 203 | 204 | MDLess.options[:taskpaper] = if MDLess.options[:taskpaper] 205 | case MDLess.options[:taskpaper].to_s 206 | when /^[ty1]/ 207 | true 208 | when /^a/ 209 | :auto 210 | else 211 | false 212 | end 213 | else 214 | false 215 | end 216 | opts.on("--taskpaper=OPTION", "Highlight TaskPaper format (true|false|auto)") do |tp| 217 | MDLess.options[:taskpaper] = case tp 218 | when /^[ty1]/ 219 | true 220 | when /^a/ 221 | :auto 222 | else 223 | false 224 | end 225 | end 226 | 227 | default(:transclude, true) 228 | opts.on("--[no-]transclude", "Transclude documents with {{filename}} syntax") do |opt| 229 | MDLess.options[:transclude] = opt 230 | end 231 | 232 | default(:update_config, false) 233 | opts.on("--update-config", "--update_config", 234 | "Update the configuration file with new keys and current command line options") do 235 | MDLess.options[:update_config] = true 236 | end 237 | 238 | default(:update_theme, false) 239 | opts.on("--update-theme", "Update the current theme file with all available keys") do 240 | MDLess.options[:update_theme] = true 241 | end 242 | 243 | default(:wiki_links, false) 244 | opts.on("--[no-]wiki-links", "Highlight [[wiki links]]") do |opt| 245 | MDLess.options[:wiki_links] = opt 246 | end 247 | end 248 | 249 | begin 250 | optparse.parse! 251 | rescue OptionParser::ParseError => e 252 | warn "error: #{e.message}" 253 | exit 1 254 | end 255 | 256 | if MDLess.options[:update_theme] 257 | FileUtils.mkdir_p(File.dirname(config)) 258 | 259 | theme = MDLess.options[:theme] =~ /default/ ? "mdless" : MDLess.options[:theme] 260 | theme = File.join(File.dirname(config), "#{theme}.theme") 261 | contents = YAML.dump(MDLess.theme) 262 | 263 | File.open(theme, "w") { |f| f.puts contents } 264 | Process.exit 0 265 | end 266 | 267 | if !File.exist?(config) || MDLess.options[:update_config] 268 | FileUtils.mkdir_p(File.dirname(config)) 269 | File.open(config, "w") do |f| 270 | opts = MDLess.options.dup 271 | opts.delete(:list) 272 | opts.delete(:section) 273 | opts.delete(:update_config) 274 | opts.delete(:update_theme) 275 | opts[:width] = 0 276 | opts = opts.keys.map(&:to_s).sort.map { |k| [k.to_sym, opts[k.to_sym]] }.to_h 277 | f.puts YAML.dump(opts) 278 | warn "Config file saved to #{config}" 279 | end 280 | end 281 | 282 | @output = "" 283 | @setheaders = [] 284 | 285 | input = "" 286 | @ref_links = {} 287 | @footnotes = {} 288 | 289 | renderer = Redcarpet::Render::Console.new 290 | 291 | markdown = Redcarpet::Markdown.new(renderer, 292 | no_intra_emphasis: !MDLess.options[:intra_emphasis], 293 | autolink: MDLess.options[:autolink], 294 | fenced_code_blocks: true, 295 | footnotes: true, 296 | hard_wrap: false, 297 | highlight: true, 298 | lax_spacing: MDLess.options[:lax_spacing], 299 | quote: false, 300 | space_after_headers: false, 301 | strikethrough: true, 302 | superscript: true, 303 | tables: true, 304 | underline: false) 305 | 306 | if !args.empty? 307 | files = args.delete_if { |f| !File.exist?(f) } 308 | @multifile = files.count > 1 309 | files.each do |file| 310 | spinner = TTY::Spinner.new("[:spinner] Processing #{File.basename(file)}...", format: :dots_3, clear: true) 311 | spinner.run do |spinner| 312 | MDLess.log.info(%(Processing "#{file}")) 313 | @output << "#{c(%i[b green])}[#{c(%i[b white])}#{file}#{c(%i[b green])}]#{xc}\n\n" if @multifile 314 | MDLess.file = file 315 | 316 | begin 317 | input = IO.read(file).force_encoding("utf-8") 318 | rescue StandardError 319 | input = IO.read(file) 320 | end 321 | raise "Nil input" if input.nil? 322 | 323 | input.scrub! 324 | input.gsub!(/\r?\n/, "\n") 325 | @headers = headers(input) 326 | if MDLess.options[:taskpaper] == :auto 327 | MDLess.options[:taskpaper] = if CLIMarkdown::TaskPaper.is_taskpaper?(input) 328 | MDLess.log.info("TaskPaper detected") 329 | true 330 | else 331 | false 332 | end 333 | end 334 | 335 | if MDLess.options[:list] 336 | @output << if MDLess.options[:taskpaper] 337 | CLIMarkdown::TaskPaper.list_projects(input) 338 | else 339 | list_headers(input) 340 | end 341 | elsif MDLess.options[:taskpaper] 342 | input = input.color_meta(MDLess.cols) 343 | input = CLIMarkdown::TaskPaper.highlight(input) 344 | @output << input.highlight_tags 345 | else 346 | @output << markdown.render(input) 347 | end 348 | @output << "\n\n" 349 | end 350 | end 351 | 352 | printout 353 | elsif !$stdin.isatty 354 | MDLess.log.info(%(Processing STDIN)) 355 | spinner = TTY::Spinner.new("[:spinner] Processing ...", format: :dots_3, clear: true) 356 | spinner.run do |spinner| 357 | MDLess.file = nil 358 | input = $stdin.read.scrub 359 | input.gsub!(/\r?\n/, "\n") 360 | 361 | if MDLess.options[:taskpaper] == :auto 362 | MDLess.options[:taskpaper] = if CLIMarkdown::TaskPaper.is_taskpaper?(input) 363 | MDLess.log.info("TaskPaper detected") 364 | true 365 | else 366 | false 367 | end 368 | end 369 | @headers = headers(input) 370 | 371 | if MDLess.options[:list] 372 | if MDLess.options[:taskpaper] 373 | puts CLIMarkdown::TaskPaper.list_projects(input) 374 | else 375 | puts list_headers(input) 376 | end 377 | Process.exit 0 378 | else 379 | if MDLess.options[:taskpaper] 380 | input = input.color_meta(MDLess.cols) 381 | input = CLIMarkdown::TaskPaper.highlight(input) 382 | @output = input.highlight_tags 383 | else 384 | @output = markdown.render(input) 385 | end 386 | end 387 | end 388 | printout 389 | else 390 | warn "No input" 391 | Process.exit 1 392 | end 393 | end 394 | 395 | def color(key) 396 | val = nil 397 | keys = key.split(/[ ,>]/) 398 | if MDLess.theme.key?(keys[0]) 399 | val = MDLess.theme[keys.shift] 400 | else 401 | MDLess.log.error("Invalid theme key: #{key}") unless keys[0] =~ /^text/ 402 | return c([:reset]) 403 | end 404 | keys.each do |k| 405 | if val.key?(k) 406 | val = val[k] 407 | else 408 | MDLess.log.error("Invalid theme key: #{k}") 409 | return c([:reset]) 410 | end 411 | end 412 | if val.is_a? String 413 | val = "x #{val}" 414 | res = val.split(/ /).map(&:to_sym) 415 | c(res) 416 | else 417 | c([:reset]) 418 | end 419 | end 420 | 421 | def headers(string) 422 | hs = [] 423 | input = string.remove_meta 424 | doc_headers = input.scan(/^((?!#!)(\#{1,6})\s*([^#]+?)(?: #+)?\s*|(\S.+)\n([=-]+))$/i) 425 | 426 | doc_headers.each do |h| 427 | hlevel = 6 428 | title = nil 429 | if h[4] =~ /=+/ 430 | hlevel = 1 431 | title = h[3] 432 | elsif h[4] =~ /-+/ 433 | hlevel = 2 434 | title = h[3] 435 | else 436 | hlevel = h[1].length 437 | title = h[2] 438 | end 439 | hs << [ 440 | "#" * hlevel, 441 | title, 442 | h[0], 443 | ] 444 | end 445 | 446 | hs 447 | end 448 | 449 | def list_headers(input) 450 | h_adjust = highest_header(input) - 1 451 | input.gsub!(/^(#+)/) do 452 | m = Regexp.last_match 453 | new_level = m[1].length - h_adjust 454 | new_level.positive? ? "#" * new_level : "" 455 | end 456 | 457 | last_level = 0 458 | headers_out = [] 459 | len = (@headers.count + 1).to_s.length 460 | @headers.each_with_index do |h, idx| 461 | level = h[0].length - 1 462 | title = h[1] 463 | 464 | level = last_level + 1 if level - 1 > last_level 465 | 466 | last_level = level 467 | 468 | subdoc = case level 469 | when 0 470 | "" 471 | when 1 472 | "- " 473 | when 2 474 | "+ " 475 | when 3 476 | "* " 477 | else 478 | " " 479 | end 480 | headers_out.push format("%s%#{len}d: %s", 481 | c: c(%i[magenta]), 482 | d: idx + 1, 483 | s: "#{c(%i[x black])}#{"." * level}#{c(%i[x yellow])}#{subdoc}#{title.strip}#{xc}") 484 | end 485 | 486 | headers_out.join("\n#{xc}") 487 | end 488 | 489 | def highest_header(input) 490 | top = 6 491 | @headers.each { |h| top = h[0].length if h[0].length < top } 492 | top 493 | end 494 | 495 | def clean_markers(input) 496 | input.gsub!(/^(\e\[[\d;]+m)?[%~] ?/, '\1') 497 | # input.gsub!(/^(\e\[[\d;]+m)*>(\e\[[\d;]+m)?( +)/, ' \3\1\2') 498 | # input.gsub!(/^(\e\[[\d;]+m)*>(\e\[[\d;]+m)?/, '\1\2') 499 | input.gsub!(/(\e\[[\d;]+m)?@@@(\e\[[\d;]+m)?$/, "") 500 | input 501 | end 502 | 503 | def clean_escapes(input) 504 | out = input.gsub(/\e\[m/, "") 505 | last_escape = "" 506 | out.gsub!(/\e\[(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+m/) do |m| 507 | if m == last_escape 508 | "" 509 | else 510 | last_escape = m 511 | m 512 | end 513 | end 514 | out.gsub(/\e\[0m/, "") 515 | end 516 | 517 | def update_inline_links(input) 518 | links = {} 519 | counter = 1 520 | input.gsub!(/(?<=\])\((.*?)\)(#{CLIMarkdown::MDTableCleanup::PAD_CHAR}*)/) do 521 | links[counter] = Regexp.last_match(1).uncolor 522 | space = Regexp.last_match(2) 523 | "[#{counter}]#{space}" 524 | end 525 | end 526 | 527 | def find_color(line, nullable: false) 528 | return line if line.nil? 529 | 530 | colors = line.scan(/\e\[[\d;]+m/) 531 | if colors.size&.positive? 532 | colors[-1] 533 | else 534 | nullable ? nil : xc 535 | end 536 | end 537 | 538 | def pad_max(block, eol = "") 539 | block.split(/\n/).map do |l| 540 | new_code_line = l.gsub(/\t/, " ") 541 | orig_length = new_code_line.size + 8 + eol.size 542 | pad_count = [MDLess.cols - orig_length, 0].max 543 | 544 | [ 545 | new_code_line, 546 | eol, 547 | " " * [pad_count - 1, 0].max, 548 | ].join 549 | end.join("\n") 550 | end 551 | 552 | def page(text, &callback) 553 | read_io, write_io = IO.pipe 554 | 555 | input = $stdin 556 | 557 | pid = Kernel.fork do 558 | write_io.close 559 | input.reopen(read_io) 560 | read_io.close 561 | 562 | # Wait until we have input before we start the pager 563 | IO.select [input] 564 | 565 | pager = which_pager 566 | MDLess.log.info("Using `#{pager.join(" ")}` as pager") 567 | begin 568 | exec(pager.join(" ")) 569 | rescue SystemCallError => e 570 | MDLess.log.error(e) 571 | exit 1 572 | end 573 | end 574 | 575 | begin 576 | read_io.close 577 | write_io.write(text) 578 | write_io.close 579 | rescue SystemCallError 580 | exit 1 581 | end 582 | 583 | _, status = Process.waitpid2(pid) 584 | status.success? 585 | end 586 | 587 | def printout 588 | out = @output 589 | 590 | # out = if MDLess.options[:taskpaper] 591 | # @output 592 | # else 593 | # # TODO: Figure out a word wrap that accounts for indentation 594 | # @output 595 | # # out = @output.rstrip.split(/\n/).map do |p| 596 | # # p.wrap(MDLess.cols, color('text')) 597 | # # end.join("\n") 598 | # end 599 | 600 | unless out.size&.positive? 601 | MDLess.log.warn "No results" 602 | Process.exit 603 | end 604 | 605 | out = clean_markers(out) 606 | out = clean_escapes(out) 607 | out = "#{out.gsub(/\n{2,}/m, "\n\n")}#{xc}" 608 | 609 | out.uncolor! unless MDLess.options[:color] 610 | 611 | if MDLess.options[:pager] 612 | page(out) 613 | else 614 | $stdout.print out.rstrip 615 | end 616 | end 617 | 618 | def which_pager 619 | # pagers = [ENV['PAGER'], ENV['GIT_PAGER']] 620 | pagers = ENV["PAGER"] ? [ENV["PAGER"]] : [] 621 | 622 | # if exec_available('git') 623 | # git_pager = `git config --get-all core.pager || true`.split.first 624 | # git_pager && pagers.push(git_pager) 625 | # end 626 | 627 | pagers.concat(["less", "more", "cat", "pager"]) 628 | 629 | pagers.delete_if { |pg| !TTY::Which.exist?(pg) } 630 | 631 | pagers.select! do |f| 632 | pg = f.split(/[ ]/)[0] 633 | return false unless pg 634 | 635 | if pg == "most" 636 | MDLess.log.warn("most not allowed as pager") 637 | false 638 | else 639 | TTY::Which.which(pg) 640 | end 641 | end 642 | 643 | pg = pagers.first 644 | args = case pg 645 | # when 'delta' 646 | # ' --pager="less -FXr"' 647 | when "less" 648 | "-FXr" 649 | # when 'bat' 650 | # ' -p --pager="less -FXr"' 651 | else 652 | "" 653 | end 654 | 655 | [pg, args] 656 | end 657 | 658 | def exec_available(cli) 659 | if TTY::Which.exist?(cli) 660 | TTY::Which.which(cli) 661 | else 662 | false 663 | end 664 | end 665 | end 666 | end 667 | -------------------------------------------------------------------------------- /lib/mdless/console.rb: -------------------------------------------------------------------------------- 1 | module Redcarpet 2 | module Render 3 | class Console < Base 4 | include CLIMarkdown::Colors 5 | include CLIMarkdown::Theme 6 | 7 | attr_accessor :headers 8 | attr_writer :file 9 | 10 | @@listitemid = 0 11 | @@listid = 0 12 | @@elementid = 0 13 | @@footnotes = [] 14 | @@links = [] 15 | @@footer_links = [] 16 | 17 | def pre_element 18 | @@elementid += 1 19 | "<>" 20 | end 21 | 22 | def post_element 23 | "<>" 24 | end 25 | 26 | def xc 27 | x + color('text') 28 | end 29 | 30 | def x 31 | c([:reset]) 32 | end 33 | 34 | def color_table(input) 35 | first = true 36 | input.split(/\n/).map do |line| 37 | if first 38 | if line =~ /^\+-+/ 39 | line.gsub!(/^/, color('table border')) 40 | else 41 | first = false 42 | line.gsub!(/\|/, "#{color('table border')}|#{color('table header')}") 43 | end 44 | elsif line.strip =~ /^[|:\- +]+$/ 45 | line.gsub!(/^(.*)$/, "#{color('table border')}\\1#{color('table color')}") 46 | line.gsub!(/([:\-+]+)/, "#{color('table divider')}\\1#{color('table border')}") 47 | else 48 | line.gsub!(/\|/, "#{color('table border')}|#{color('table color')}") 49 | end 50 | end.join("\n") 51 | end 52 | 53 | def exec_available(cli) 54 | if File.exist?(File.expand_path(cli)) 55 | File.executable?(File.expand_path(cli)) 56 | else 57 | TTY::Which.exist?(cli) 58 | end 59 | end 60 | 61 | def code_bg(input, width) 62 | input.split(/\n/).map do |line| 63 | tail = line.uncolor.length < width ? "\u00A0" * (width - line.uncolor.length) : '' 64 | "#{x}#{line}#{tail}#{x}" 65 | end.join("\n") 66 | end 67 | 68 | def hilite_code(code_block, language) 69 | longest_line = code_block.uncolor.split(/\n/).longest_element.length + 4 70 | longest_line = longest_line > MDLess.cols ? MDLess.cols : longest_line 71 | 72 | # if MDLess.options[:syntax_higlight] 73 | # formatter = Rouge::Formatters::Terminal256 74 | # lexer = if language 75 | # Object.const_get("Rouge::Lexers::#{language.capitalize}") rescue Rouge::Lexer.guess(source: code_block) 76 | # else 77 | # Rouge::Lexer.guess(source: code_block) 78 | # end 79 | # hilite = formatter.format(lexer.lex(code_block)) 80 | # hilite = xc + hilite.split(/\n/).map do |l| 81 | # [ 82 | # color('code_block marker'), 83 | # MDLess.theme['code_block']['character'], 84 | # "#{color('code_block bg')}#{l.rstrip}#{xc}" 85 | # ].join 86 | # end.join("\n").blackout(MDLess.theme['code_block']['bg']) + "#{xc}\n" 87 | # else 88 | # hilite = code_block.split(/\n/).map do |line| 89 | # [ 90 | # color('code_block marker'), 91 | # MDLess.theme['code_block']['character'], 92 | # color('code_block color'), 93 | # line, 94 | # xc 95 | # ].join 96 | # end.join("\n").blackout(MDLess.theme['code_block']['bg']) + "#{xc}\n" 97 | # end 98 | 99 | if MDLess.options[:syntax_higlight] && !exec_available('pygmentize') 100 | MDLess.log.error('Syntax highlighting requested by pygmentize is not available') 101 | MDLess.options[:syntax_higlight] = false 102 | end 103 | 104 | if MDLess.options[:syntax_higlight] 105 | pyg = TTY::Which.which('pygmentize') 106 | lexer = language&.valid_lexer? ? "-l #{language}" : '-g' 107 | begin 108 | pygments_theme = MDLess.options[:pygments_theme] || MDLess.theme['code_block']['pygments_theme'] 109 | 110 | unless pygments_theme.valid_pygments_theme? 111 | MDLess.log.error("Invalid Pygments theme #{pygments_theme}, defaulting to 'default' for highlighting") 112 | pygments_theme = 'default' 113 | end 114 | 115 | cmd = [ 116 | "#{pyg} -f terminal256", 117 | "-O style=#{pygments_theme}", 118 | lexer, 119 | '2> /dev/null' 120 | ].join(' ') 121 | hilite, s = Open3.capture2(cmd, 122 | stdin_data: code_block) 123 | 124 | if s.success? 125 | hilite = xc + hilite.split(/\n/).map do |l| 126 | [ 127 | color('code_block marker'), 128 | MDLess.theme['code_block']['character'], 129 | "#{color('code_block bg')}#{l}#{xc}" 130 | ].join 131 | end.join("\n").blackout(MDLess.theme['code_block']['bg']) + "#{xc}\n" 132 | end 133 | rescue StandardError => e 134 | MDLess.log.error(e) 135 | hilite = code_block 136 | end 137 | else 138 | hilite = code_block.split(/\n/).map do |line| 139 | [ 140 | color('code_block marker'), 141 | MDLess.theme['code_block']['character'], 142 | color('code_block color'), 143 | line, 144 | xc 145 | ].join 146 | end.join("\n").blackout(MDLess.theme['code_block']['bg']) + "#{xc}\n" 147 | end 148 | 149 | top_border = if language.nil? || language.empty? 150 | '-' * longest_line 151 | else 152 | len = (longest_line - 6 - language.length) > 0 ? longest_line - 6 - language.length : 0 153 | "--[ #{language} ]#{"-" * len}" 154 | end 155 | [ 156 | xc, 157 | color('code_block border'), 158 | top_border, 159 | xc, 160 | "\n", 161 | color('code_block color'), 162 | code_bg(hilite.chomp, longest_line), 163 | "\n", 164 | color('code_block border'), 165 | '-' * longest_line, 166 | xc 167 | ].join 168 | end 169 | 170 | def color(key) 171 | val = nil 172 | keys = key.split(/[ ,>]/) 173 | 174 | if MDLess.theme.key?(keys[0]) 175 | val = MDLess.theme[keys.shift] 176 | else 177 | MDLess.log.error("Invalid theme key~: #{key}") unless keys[0] =~ /^text/ 178 | return c([:reset]) 179 | end 180 | keys.each do |k| 181 | if val.key?(k) 182 | val = val[k] 183 | else 184 | MDLess.log.error("Invalid theme key*: #{k}") 185 | return c([:reset]) 186 | end 187 | end 188 | if val.is_a? String 189 | val = "x #{val}" 190 | res = val.split(/ /).map(&:to_sym) 191 | c(res) 192 | else 193 | c([:reset]) 194 | end 195 | end 196 | 197 | def block_code(code, language) 198 | "\n\n#{hilite_code(code, language)}#{xc}\n\n" 199 | end 200 | 201 | def block_quote(quote) 202 | ret = "\n\n" 203 | quote.strip.wrap(MDLess.cols, color('blockquote color')).split(/\n/).each do |line| 204 | ret += [ 205 | color('blockquote marker color'), 206 | MDLess.theme['blockquote']['marker']['character'], 207 | color('blockquote color'), 208 | ' ', 209 | line, 210 | "\n" 211 | ].join('') 212 | end 213 | "#{ret}\n\n" 214 | end 215 | 216 | def block_html(raw_html) 217 | "#{color('html color')}#{color_tags(raw_html)}#{xc}" 218 | end 219 | 220 | def header(text, header_level) 221 | pad = '' 222 | ansi = '' 223 | text.clean_header_ids! 224 | uncolored = text.uncolor.gsub(/<<(pre|post)\d+>>/, '') 225 | uncolored.sub!(/\[(.*?)\]\(.*?\)/, '[\1][xxx]') if MDLess.options[:links] != :inline 226 | 227 | text_length = uncolored.length 228 | case header_level 229 | when 1 230 | ansi = color('h1 color') 231 | pad = color('h1 pad') 232 | char = MDLess.theme['h1']['pad_char'] || '=' 233 | pad += text_length + 2 > MDLess.cols ? char * text_length : char * (MDLess.cols - (text_length + 1)) 234 | when 2 235 | ansi = color('h2 color') 236 | pad = color('h2 pad') 237 | char = MDLess.theme['h2']['pad_char'] || '-' 238 | pad += text_length + 2 > MDLess.cols ? char * text_length : char * (MDLess.cols - (text_length + 1)) 239 | when 3 240 | ansi = color('h3 color') 241 | when 4 242 | ansi = color('h4 color') 243 | when 5 244 | ansi = color('h5 color') 245 | else 246 | ansi = color('h6 color') 247 | end 248 | 249 | # If we're in iTerm and not paginating, add 250 | # iTerm Marks for navigation on h1-3 251 | if header_level < 4 && 252 | ENV['TERM_PROGRAM'] =~ /^iterm/i && 253 | MDLess.options[:pager] == false 254 | ansi = "\e]1337;SetMark\a#{ansi}" 255 | end 256 | 257 | "\n\n#{xc}#{ansi}#{text} #{pad}#{xc}\n\n" 258 | end 259 | 260 | def hrule 261 | "\n\n#{color('hr color')}#{'_' * MDLess.cols}#{xc}\n\n" 262 | end 263 | 264 | def paragraph(text) 265 | text.scrub! 266 | out = if MDLess.options[:preserve_linebreaks] 267 | "#{xc}#{text.gsub(/ +/, ' ').strip}#{xc}#{x}\n\n" 268 | else 269 | if text.uncolor =~ / {2,}$/ || text.uncolor =~ /^%/ 270 | "#{xc}#{text.gsub(/ +/, ' ').strip}#{xc}#{x}\n" 271 | else 272 | "#{xc}#{text.gsub(/ +/, ' ').gsub(/\n+(?![:-])/, ' ').strip}#{xc}#{x}\n\n" 273 | end 274 | end 275 | if MDLess.options[:at_tags] || MDLess.options[:taskpaper] 276 | highlight_tags(out) 277 | else 278 | out 279 | end 280 | end 281 | 282 | def uncolor_grafs(text) 283 | text.gsub(/#{Regexp.escape(color("text"))}/, color('list color')) 284 | end 285 | 286 | @table_cols = nil 287 | 288 | def table_header_row 289 | @header_row.map do |alignment| 290 | case alignment 291 | when :left 292 | '|:---' 293 | when :right 294 | '|---:' 295 | when :center 296 | '|:--:' 297 | else 298 | '|----' 299 | end 300 | end.join('') + '|' 301 | end 302 | 303 | def table(header, body) 304 | formatted = CLIMarkdown::MDTableCleanup.new([ 305 | header.to_s, 306 | table_header_row, 307 | "|\n", 308 | "#{body}\n\n" 309 | ].join('')) 310 | @header_row = [] 311 | res = formatted.to_md 312 | "#{color_table(res)}\n\n" 313 | # res 314 | end 315 | 316 | def table_row(content) 317 | @table_cols = content.scan(/\|/).count 318 | %(#{content}\n) 319 | end 320 | 321 | def table_cell(content, alignment) 322 | @header_row ||= [] 323 | @header_row << alignment if @table_cols && @header_row.count < @table_cols 324 | %(#{content} |) 325 | end 326 | 327 | def autolink(link, _) 328 | [ 329 | pre_element, 330 | color('link brackets'), 331 | '<', 332 | color('link url'), 333 | link, 334 | color('link brackets'), 335 | '>', 336 | xc, 337 | post_element 338 | ].join('') 339 | end 340 | 341 | def codespan(code) 342 | out = [ 343 | pre_element, 344 | color('code_span marker'), 345 | MDLess.theme['code_span']['character'], 346 | color('code_span color'), 347 | code, 348 | color('code_span marker'), 349 | MDLess.theme['code_span']['character'], 350 | xc, 351 | post_element 352 | ].join 353 | end 354 | 355 | def double_emphasis(text) 356 | [ 357 | pre_element, 358 | color('emphasis bold'), 359 | MDLess.theme['emphasis']['bold_character'], 360 | text, 361 | MDLess.theme['emphasis']['bold_character'], 362 | xc, 363 | post_element 364 | ].join 365 | end 366 | 367 | def emphasis(text) 368 | [ 369 | pre_element, 370 | color('emphasis italic'), 371 | MDLess.theme['emphasis']['italic_character'], 372 | text, 373 | MDLess.theme['emphasis']['italic_character'], 374 | xc, 375 | post_element 376 | ].join 377 | end 378 | 379 | def triple_emphasis(text) 380 | [ 381 | pre_element, 382 | color('emphasis bold-italic'), 383 | MDLess.theme['emphasis']['italic_character'], 384 | MDLess.theme['emphasis']['bold_character'], 385 | text, 386 | MDLess.theme['emphasis']['bold_character'], 387 | MDLess.theme['emphasis']['italic_character'], 388 | xc, 389 | post_element 390 | ].join 391 | end 392 | 393 | def highlight(text) 394 | "#{pre_element}#{color('highlight')}#{text}#{xc}#{post_element}" 395 | end 396 | 397 | def image(link, title, alt_text) 398 | "<>#{link}||#{title}||#{alt_text}<>" 399 | end 400 | 401 | def linebreak 402 | " \n" 403 | end 404 | 405 | def color_link(link, title, content) 406 | [ 407 | pre_element, 408 | color('link brackets'), 409 | '[', 410 | color('link text'), 411 | content, 412 | color('link brackets'), 413 | '](', 414 | color('link url'), 415 | link, 416 | title.nil? ? '' : %( "#{title}"), 417 | color('link brackets'), 418 | ')', 419 | xc, 420 | post_element 421 | ].join 422 | end 423 | 424 | def color_image_tag(link, title, alt_text) 425 | image = [ 426 | color('image brackets'), 427 | '[', 428 | color('image title'), 429 | alt_text, 430 | color('image brackets'), 431 | '](', 432 | color('image url'), 433 | link, 434 | title.nil? ? '' : %( "#{title}"), 435 | color('image brackets'), 436 | ')' 437 | ].join 438 | 439 | @@links << { 440 | link: image, 441 | url: link, 442 | title: title, 443 | content: alt_text, 444 | image: true 445 | } 446 | 447 | [ 448 | color('image bang'), 449 | '!', 450 | image, 451 | xc 452 | ].join 453 | end 454 | 455 | def color_link_reference(_link, idx, content) 456 | [ 457 | pre_element, 458 | color('link brackets'), 459 | '[', 460 | color('link text'), 461 | content, 462 | color('link brackets'), 463 | '][', 464 | color('link url'), 465 | idx, 466 | color('link brackets'), 467 | ']', 468 | xc, 469 | post_element 470 | ].join 471 | end 472 | 473 | def color_reference_link(link, title, content, image: false) 474 | [ 475 | color('link brackets'), 476 | '[', 477 | color('link text'), 478 | content, 479 | color('link brackets'), 480 | ']:', 481 | color('text'), 482 | ' ', 483 | image ? color('image url') : color('link url'), 484 | link, 485 | title.nil? ? '' : %( "#{title}"), 486 | xc 487 | ].join 488 | end 489 | 490 | def color_image_reference(idx, content) 491 | [ 492 | pre_element, 493 | color('image brackets'), 494 | '[', 495 | color('image title'), 496 | content, 497 | color('image brackets'), 498 | '][', 499 | color('link url'), 500 | idx, 501 | color('image brackets'), 502 | ']', 503 | xc, 504 | post_element 505 | ].join 506 | end 507 | 508 | def link(link, title, content) 509 | res = color_link(link, title&.strip, content&.strip) 510 | @@links << { 511 | link: res, 512 | url: link, 513 | title: title, 514 | content: content 515 | } 516 | res 517 | end 518 | 519 | def color_tags(html) 520 | html.gsub(%r{((?!<)]+)?>)}, "#{color('html brackets')}\\1#{xc}") 521 | end 522 | 523 | def raw_html(raw_html) 524 | "#{pre_element}#{color('html color')}#{color_tags(raw_html)}#{xc}#{post_element}" 525 | end 526 | 527 | def strikethrough(text) 528 | "#{pre_element}#{color('deletion')}#{text}#{xc}#{post_element}" 529 | end 530 | 531 | def superscript(text) 532 | "#{pre_element}#{color('super')}^#{text}#{xc}#{post_element}" 533 | end 534 | 535 | def footnotes(_text) 536 | # [ 537 | # color('footnote note'), 538 | # text, 539 | # "\n", 540 | # xc, 541 | # ].join('') 542 | nil 543 | end 544 | 545 | def color_footnote_def(idx) 546 | text = @@footnotes[idx] 547 | [ 548 | color('footnote brackets'), 549 | '[', 550 | color('footnote caret'), 551 | '^', 552 | color('footnote title'), 553 | idx, 554 | color('footnote brackets'), 555 | ']:', 556 | color('footnote note'), 557 | ' ', 558 | text.uncolor.strip, 559 | xc, 560 | "\n" 561 | ].join('') 562 | end 563 | 564 | def footnote_def(text, idx) 565 | @@footnotes[idx] = text 566 | end 567 | 568 | def footnote_ref(text) 569 | [ 570 | pre_element, 571 | color('footnote title'), 572 | "[^#{text}]", 573 | xc, 574 | post_element 575 | ].join('') 576 | end 577 | 578 | def insert_footnotes(input) 579 | input.split(/\n/).map do |line| 580 | notes = line.to_enum(:scan, /\[\^(?\d+)\]/).map { Regexp.last_match } 581 | if notes.count.positive? 582 | footnotes = notes.map { |n| color_footnote_def(n['ref'].to_i) }.join("\n") 583 | "#{line}\n\n#{footnotes}\n\n\n" 584 | else 585 | line 586 | end 587 | end.join("\n") 588 | end 589 | 590 | def list(contents, list_type) 591 | @@listid += 1 592 | "<>#{contents}<>" 593 | end 594 | 595 | def list_item(text, list_type) 596 | @@listitemid += 1 597 | case list_type 598 | when :unordered 599 | "<>#{text.strip}<>\n" 600 | when :ordered 601 | "<>#{text.strip}<>\n" 602 | end 603 | end 604 | 605 | def indent_lines(input) 606 | return nil if input.nil? 607 | 608 | lines = input.split(/\n/) 609 | line1 = lines.shift 610 | pre = ' ' 611 | 612 | body = lines.map { |l| "#{pre}#{l.rstrip}" }.join("\n") 613 | "#{line1}\n#{body}" 614 | end 615 | 616 | def color_list_item(indent, content, type, counter) 617 | out = case type 618 | when :unordered 619 | [ 620 | ' ' * indent, 621 | color('list bullet'), 622 | MDLess.theme['list']['ul_char'].strip, 623 | ' ', 624 | color('list color'), 625 | indent_lines(content).strip, 626 | xc 627 | ].join 628 | when :ordered 629 | [ 630 | ' ' * indent, 631 | color('list number'), 632 | "#{counter}. ", 633 | color('list color'), 634 | indent_lines(content).strip, 635 | xc 636 | ].join 637 | end 638 | if MDLess.options[:at_tags] || MDLess.options[:taskpaper] 639 | color_tags(out) 640 | else 641 | out 642 | end 643 | end 644 | 645 | def fix_lists(input) 646 | input = nest_lists(input) 647 | input = fix_list_spacing(input) 648 | fix_list_items(input) 649 | end 650 | 651 | def fix_list_spacing(input) 652 | input.gsub(/( *\n)+( *)<\d+)-(?.*?)>>(?.*?)<>>}m) do 657 | m = Regexp.last_match 658 | lines = m['content'].strip.split(/\n/) 659 | 660 | list = nest_lists(lines.map do |l| 661 | outdent = l.scan(%r{<>}).count 662 | indent += l.scan(/<>/).count 663 | indent -= outdent 664 | " #{l}" 665 | end.join("\n"), indent) 666 | next if list.nil? 667 | 668 | "<>#{list}<>\n\n" 669 | end 670 | 671 | input.gsub(%r{^(? +)<\d+)>>(?.*?)<>>}m) do 672 | m = Regexp.last_match 673 | "#{m['indent']}#{m['content']}" 674 | end 675 | end 676 | 677 | def normalize_indentation(line) 678 | line.gsub(/^([ \t]+)/) do |pre| 679 | pre.gsub(/\t/, ' ') 680 | end 681 | end 682 | 683 | def fix_items(content, last_indent = 0, levels = [0]) 684 | content.gsub(%r{^(? *)<\d+)-(?(?:un)?ordered)>>(?.*?)<>>}m) do 685 | m = Regexp.last_match 686 | 687 | indent = m['indent'].length 688 | if m['type'].to_sym == :ordered 689 | if indent == last_indent 690 | levels[indent] ||= 0 691 | levels[indent] += 1 692 | elsif indent < last_indent 693 | levels[last_indent] = 0 694 | levels[indent] += 1 695 | last_indent = indent 696 | else 697 | levels[indent] = 1 698 | last_indent = indent 699 | end 700 | end 701 | 702 | content = m['content'] =~ /<\d+)>>(?.*?)<>>}m) do 709 | m = Regexp.last_match 710 | fix_items(m['content']) 711 | end 712 | end 713 | 714 | def get_headers(input) 715 | unless @headers && !@headers.empty? 716 | @headers = [] 717 | headers = input.scan(/^((?!#!)(\#{1,6})\s*([^#]+?)(?: #+)?\s*|(\S.+)\n([=-]+))$/i) 718 | 719 | headers.each do |h| 720 | hlevel = 6 721 | title = nil 722 | if h[4] =~ /=+/ 723 | hlevel = 1 724 | title = h[3] 725 | elsif h[4] =~ /-+/ 726 | hlevel = 2 727 | title = h[3] 728 | else 729 | hlevel = h[1].length 730 | title = h[2] 731 | end 732 | @headers << [ 733 | '#' * hlevel, 734 | title, 735 | h[0] 736 | ] 737 | end 738 | end 739 | 740 | @headers 741 | end 742 | 743 | def color_meta(text) 744 | input = text.dup 745 | input.clean_empty_lines! 746 | MDLess.meta = {} 747 | first_line = input.split("\n").first 748 | if first_line =~ /(?i-m)^---[ \t]*?$/ 749 | MDLess.log.info('Found YAML') 750 | # YAML 751 | in_yaml = true 752 | input.sub!(/(?i-m)^---[ \t]*\n(?(?:[\s\S]*?))\n[-.]{3}[ \t]*\n/m) do 753 | m = Regexp.last_match 754 | MDLess.log.info('Processing YAML header') 755 | begin 756 | MDLess.meta = YAML.unsafe_load(m['content']).each_with_object({}) { |(k, v), h| h[k.downcase] = v } 757 | rescue Psych::DisallowedClass => e 758 | @log.error('Error reading YAML header') 759 | @log.error(e) 760 | MDLess.meta = {} 761 | rescue StandardError => e 762 | @log.error("StandardError: #{e}") 763 | end 764 | 765 | lines = m[0].split(/\n/) 766 | longest = lines.longest_element.length 767 | longest = longest < MDLess.cols ? longest + 1 : MDLess.cols 768 | lines.map do |line| 769 | if line =~ /^[-.]{3}\s*$/ 770 | line = "#{color('metadata marker')}#{'%' * longest}" 771 | else 772 | line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2') 773 | line = "#{color('metadata marker')}% #{color('metadata color')}#{line}" 774 | end 775 | if (longest - line.uncolor.strip.length).positive? 776 | line += "\u00A0" * (longest - line.uncolor.strip.length) 777 | end 778 | line + xc 779 | end.join("\n") + "#{xc}\n" 780 | end 781 | end 782 | 783 | if !in_yaml && first_line =~ /(?i-m)^[\w ]+:\s+\S+/ 784 | MDLess.log.info('Found MMD Headers') 785 | input.sub!(/(?i-m)^([\S ]+:[\s\S]*?)+(?=\n *\n)/) do |mmd| 786 | lines = mmd.split(/\n/) 787 | return mmd if lines.count > 20 788 | 789 | longest = lines.inject { |memo, word| memo.length > word.length ? memo : word }.length 790 | longest = longest < MDLess.cols ? longest + 1 : MDLess.cols 791 | 792 | lines.map do |line| 793 | line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2') 794 | parts = line.match(/^[ \t]*(\S.*?):[ \t]+(\S.*?)$/) 795 | if parts 796 | key = parts[1].gsub(/[^a-z0-9\-_]/i, '') 797 | value = parts[2].strip 798 | MDLess.meta[key] = value 799 | end 800 | line = "#{color('metadata marker')}%#{color('metadata color')}#{line}" 801 | if (longest - line.uncolor.strip.length).positive? 802 | line += "\u00A0" * (longest - line.uncolor.strip.length) 803 | end 804 | line + xc 805 | end.join("\n") + "#{xc}\n" 806 | end 807 | end 808 | 809 | input 810 | end 811 | 812 | def mmd_transclude(input) 813 | return input unless MDLess.file || MDLess.meta.key?('transcludebase') 814 | 815 | input.gsub(/^{{(.*?)}}/) do |m| 816 | filename = Regexp.last_match(1).strip 817 | file = if MDLess.meta.key?('transcludebase') 818 | File.join(File.expand_path(MDLess.meta['transcludebase']), filename) 819 | else 820 | File.join(File.dirname(MDLess.file), filename) 821 | end 822 | File.exist?(file) ? "\n\n#{mmd_transclude(IO.read(file).remove_meta)}\n\n" : m 823 | end 824 | end 825 | 826 | def mmd_metadata_replace(input) 827 | input.gsub(/\[%(.*?)\]/) do |m| 828 | key = Regexp.last_match(1) 829 | if MDLess.meta.key?(key) 830 | MDLess.meta[key] 831 | else 832 | m 833 | end 834 | end 835 | end 836 | 837 | def fix_image_attributes(input) 838 | input.gsub(/^( {0,3}\[[^^*>].*?\]: *\S+) +([^"].*?)$/, '\1') 839 | end 840 | 841 | def preprocess(input) 842 | input = color_meta(input) 843 | input = mmd_transclude(input) if MDLess.options[:transclude] 844 | input = mmd_metadata_replace(input) if MDLess.options[:mmd_metadata] 845 | input = fix_image_attributes(input) 846 | 847 | replaced_input = input.clone 848 | ## Replace setex headers with ATX 849 | replaced_input.gsub!(/^([^\n]+)\n={2,}\s*$/m, "# \\1\n") 850 | replaced_input.gsub!(/^([^\n]+?)\n-{2,}\s*$/m, "## \\1\n") 851 | 852 | @headers = get_headers(replaced_input) 853 | 854 | if MDLess.options[:section] 855 | new_content = [] 856 | MDLess.log.info("Matching section(s) #{MDLess.options[:section].join(', ')}") 857 | MDLess.options[:section].each do |sect| 858 | comparison = MDLess.options[:section][0].is_a?(String) ? :regex : :numeric 859 | 860 | in_section = false 861 | top_level = 1 862 | input.split(/\n/).each_with_index do |graf, _idx| 863 | if graf =~ /^(#+) *(.*?)( *#+)?$/ 864 | m = Regexp.last_match 865 | level = m[1].length 866 | title = m[2] 867 | if in_section 868 | if level >= top_level 869 | new_content.push(graf) 870 | else 871 | in_section = false 872 | break 873 | end 874 | elsif (comparison == :regex && title.downcase =~ sect.downcase.to_rx) || 875 | (comparison == :numeric && title.downcase == @headers[sect - 1][1].downcase) 876 | in_section = true 877 | top_level = level + 1 878 | new_content.push(graf) 879 | else 880 | next 881 | end 882 | elsif in_section 883 | new_content.push(graf) 884 | end 885 | end 886 | end 887 | input = new_content.join("\n") 888 | end 889 | 890 | # definition lists 891 | input.gsub!(/(?mi)(?<=\n|\A)(?(?!<:)[^\n]+)(?(\n+: [^\n]+)+)/) do 892 | m = Regexp.last_match 893 | "#{color('dd term')}#{m['term']}#{xc}#{color('dd color')}#{color_dd_def(m['def'])}" 894 | end 895 | 896 | # deletions 897 | input.gsub!(/(?mi)(?<=\n|\A)~~(?.*?)~~/) do 898 | m = Regexp.last_match 899 | "#{color('deletion')}#{m['text']}#{xc}" 900 | end 901 | 902 | # emojis 903 | input.gsub!(/(?<=\s|\A):(?\S+):/) do 904 | m = Regexp.last_match 905 | emoji = CLIMarkdown::Emoji.convert_emoji(m['emoji']) 906 | "#{emoji}#{xc}" 907 | end 908 | 909 | input 910 | end 911 | 912 | def color_dd_def(input) 913 | input.gsub(/(?<=\n|\A)(?::)\s+(.*)/) do 914 | m = Regexp.last_match 915 | [ 916 | color('dd marker'), 917 | ': ', 918 | color('dd color'), 919 | m[1], 920 | xc 921 | ].join 922 | end 923 | end 924 | 925 | def color_links(input) 926 | input.gsub(/(?mi)(?[^\[]+)\]\((?\S+)(?: +"(?.*?)")? *\)/) do 927 | m = Regexp.last_match 928 | color_link(m['url'].uncolor, m['title']&.uncolor, m['text'].uncolor) 929 | end 930 | end 931 | 932 | def reference_links(input) 933 | grafs = input.split(/\n{2,}/) 934 | counter = 1 935 | 936 | grafs.map! do |graf| 937 | return "\n" if graf =~ /^ *\n$/ 938 | 939 | links_added = false 940 | 941 | @@links.each do |link| 942 | next unless graf =~ /#{Regexp.escape(link[:link].gsub(/\n/, " "))}/ 943 | 944 | table = graf.uncolor =~ /^ *\|/ 945 | url = link[:url].uncolor 946 | content = link[:content] 947 | title = link[:title]&.uncolor 948 | image = link.key?(:image) && link[:image] ? true : false 949 | colored_link = image ? color_image_reference(counter, content) : color_link_reference(url, counter, content) 950 | if table 951 | diff = link[:link].gsub(/\n/, ' ').uncolor.length - colored_link.uncolor.gsub(/\n/, ' ').length 952 | graf.gsub!(/(?<=\|)([^\|]*?)#{Regexp.escape(link[:link].gsub(/\n/, " "))}(.*?)(?=\|)/) do 953 | before = Regexp.last_match(1) 954 | after = Regexp.last_match(2) 955 | surround = "#{before}#{after}" 956 | puts "**#{surround}**" 957 | diff += 2 if surround.uncolor.gsub(/#{CLIMarkdown::MDTableCleanup::PAD_CHAR}*/, '').strip.length.zero? 958 | "#{before}#{colored_link}#{after}#{' ' * diff}" 959 | end 960 | else 961 | graf.gsub!(/#{Regexp.escape(link[:link].gsub(/\n/, " "))}/, colored_link) 962 | end 963 | if MDLess.options[:links] == :paragraph 964 | if links_added 965 | graf += "\n#{color_reference_link(url, title, counter, image: image)}" 966 | else 967 | graf = "#{graf}\n\n#{color_reference_link(url, title, counter, image: image)}" 968 | links_added = true 969 | end 970 | else 971 | @@footer_links << color_reference_link(url, title, counter, image: image) 972 | end 973 | counter += 1 974 | end 975 | "\n#{graf}\n" 976 | end 977 | 978 | if MDLess.options[:links] == :paragraph 979 | grafs.join("\n") 980 | else 981 | grafs.join("\n") + "\n#{@@footer_links.join("\n")}\n" 982 | end 983 | end 984 | 985 | def fix_colors(input) 986 | input.gsub(/<<pre(?<id>\d+)>>(?<content>.*?)<<post\k<id>>>/m) do 987 | m = Regexp.last_match 988 | pre = m.pre_match.gsub(/<<pre(?<id>\d+)>>.*?<<post\k<id>>>/m, '') 989 | last_color = pre.last_color_code 990 | 991 | "#{fix_colors(m['content'])}#{last_color}" 992 | end.gsub(/<<(pre|post)\d+>>/, '') 993 | end 994 | 995 | def render_images(input) 996 | input.gsub(%r{<<img>>(.*?)<</img>>}) do 997 | link, title, alt_text = Regexp.last_match(1).split(/\|\|/) 998 | 999 | if (exec_available('imgcat') || exec_available('chafa')) && MDLess.options[:local_images] 1000 | if exec_available('imgcat') 1001 | MDLess.log.info('Using imgcat for image rendering') 1002 | elsif exec_available('chafa') 1003 | MDLess.log.info('Using chafa for image rendering') 1004 | end 1005 | img_path = link 1006 | if img_path =~ /^http/ && MDLess.options[:remote_images] 1007 | if exec_available('imgcat') 1008 | MDLess.log.info('Using imgcat for image rendering') 1009 | begin 1010 | res, s = Open3.capture2(%(curl -sS "#{img_path}" 2> /dev/null | imgcat)) 1011 | 1012 | if s.success? 1013 | pre = !alt_text.nil? ? " #{c(%i[d blue])}[#{alt_text.strip}]\n" : '' 1014 | post = !title.nil? ? "\n #{c(%i[b blue])}-- #{title} --" : '' 1015 | result = pre + res + post 1016 | end 1017 | rescue StandardError => e 1018 | MDLess.log.error(e) 1019 | end 1020 | elsif exec_available('chafa') 1021 | MDLess.log.info('Using chafa for image rendering') 1022 | term = '-f sixels' 1023 | term = ENV['TERMINAL_PROGRAM'] =~ /iterm/i ? '-f iterm' : term 1024 | term = ENV['TERMINAL_PROGRAM'] =~ /kitty/i ? '-f kitty' : term 1025 | FileUtils.rm_r '.mdless_tmp', force: true if File.directory?('.mdless_tmp') 1026 | Dir.mkdir('.mdless_tmp') 1027 | Dir.chdir('.mdless_tmp') 1028 | `curl -SsO #{img_path} 2> /dev/null` 1029 | tmp_img = File.basename(img_path) 1030 | img = `chafa #{term} "#{tmp_img}"` 1031 | pre = alt_text ? " #{c(%i[d blue])}[#{alt_text.strip}]\n" : '' 1032 | post = title ? "\n #{c(%i[b blue])}-- #{title} --" : '' 1033 | result = pre + img + post 1034 | Dir.chdir('..') 1035 | FileUtils.rm_r '.mdless_tmp', force: true 1036 | else 1037 | MDLess.log.warn('No viewer for remote images') 1038 | end 1039 | else 1040 | if img_path =~ %r{^[~/]} 1041 | img_path = File.expand_path(img_path) 1042 | elsif MDLess.file 1043 | base = File.expand_path(File.dirname(MDLess.file)) 1044 | img_path = File.join(base, img_path) 1045 | end 1046 | if File.exist?(img_path) 1047 | pre = !alt_text.nil? ? " #{c(%i[d blue])}[#{alt_text.strip}]\n" : '' 1048 | post = !title.nil? ? "\n #{c(%i[b blue])}-- #{title} --" : '' 1049 | if exec_available('imgcat') 1050 | img = `imgcat "#{img_path}"` 1051 | elsif exec_available('chafa') 1052 | term = '-f sixels' 1053 | term = ENV['TERMINAL_PROGRAM'] =~ /iterm/i ? '-f iterm' : term 1054 | term = ENV['TERMINAL_PROGRAM'] =~ /kitty/i ? '-f kitty' : term 1055 | img = `chafa #{term} "#{img_path}"` 1056 | end 1057 | result = pre + img + post 1058 | end 1059 | end 1060 | end 1061 | if result.nil? 1062 | color_image_tag(link, title, alt_text) 1063 | else 1064 | "#{pre_element}#{result}#{xc}#{post_element}" 1065 | end 1066 | end 1067 | end 1068 | 1069 | def fix_equations(input) 1070 | input.gsub(/((\\\\\[|\$\$)(.*?)(\\\\\]|\$\$)|(\\\\\(|\$)(.*?)(\\\\\)|\$))/) do 1071 | m = Regexp.last_match 1072 | if m[2] 1073 | brackets = [m[2], m[4]] 1074 | equat = m[3] 1075 | else 1076 | brackets = [m[5], m[7]] 1077 | equat = m[6] 1078 | end 1079 | [ 1080 | pre_element, 1081 | color('math brackets'), 1082 | brackets[0], 1083 | xc, 1084 | color('math equation'), 1085 | equat, 1086 | color('math brackets'), 1087 | brackets[1], 1088 | xc, 1089 | post_element 1090 | ].join 1091 | end 1092 | end 1093 | 1094 | def highlight_tags(input) 1095 | tag_color = color('at_tags tag') 1096 | value_color = color('at_tags value') 1097 | input.gsub(/(?<pre>\s|m)(?<tag>@[^ \]:;.?!,("'\n]+)(?:(?<lparen>\()(?<value>.*?)(?<rparen>\)))?/) do 1098 | m = Regexp.last_match 1099 | last_color = m.pre_match.last_color_code 1100 | [ 1101 | m['pre'], 1102 | tag_color, 1103 | m['tag'], 1104 | m['lparen'], 1105 | value_color, 1106 | m['value'], 1107 | tag_color, 1108 | m['rparen'], 1109 | xc, 1110 | last_color 1111 | ].join 1112 | end 1113 | end 1114 | 1115 | def highlight_wiki_links(input) 1116 | input.gsub(/\[\[(.*?)\]\]/) do 1117 | content = Regexp.last_match(1) 1118 | [ 1119 | pre_element, 1120 | color('link brackets'), 1121 | '[[', 1122 | color('link text'), 1123 | content, 1124 | color('link brackets'), 1125 | ']]', 1126 | xc, 1127 | post_element 1128 | ].join 1129 | end 1130 | end 1131 | 1132 | def postprocess(input) 1133 | input.scrub! 1134 | 1135 | input = highlight_wiki_links(input) if MDLess.options[:wiki_links] 1136 | 1137 | if MDLess.options[:inline_footnotes] 1138 | input = insert_footnotes(input) 1139 | else 1140 | footnotes = @@footnotes.map.with_index do |fn, i| 1141 | next if fn.nil? 1142 | 1143 | color_footnote_def(i) 1144 | end.join("\n") 1145 | input = "#{input}\n\n#{footnotes}" 1146 | end 1147 | # escaped characters 1148 | input.gsub!(/\\(\S)/, '\1') 1149 | # equations 1150 | input = fix_equations(input) 1151 | # misc html 1152 | input.gsub!(%r{<br */?>}, "#{pre_element}\n#{post_element}") 1153 | # render images 1154 | input = render_images(input) if MDLess.options[:local_images] 1155 | # format links 1156 | input = reference_links(input) if MDLess.options[:links] == :reference || MDLess.options[:links] == :paragraph 1157 | # lists 1158 | input = fix_lists(input) 1159 | fix_colors(input) 1160 | end 1161 | end 1162 | end 1163 | end 1164 | --------------------------------------------------------------------------------