Merge branch 'main' of github.com:ralsina/tartrazine

This commit is contained in:
2024-08-12 10:11:03 -03:00
18 changed files with 1427 additions and 165 deletions

View File

@ -1,5 +1,4 @@
require "./actions"
require "./constants"
require "./formatter"
require "./rules"
require "./styles"

1160
src/constants/lexers.cr Normal file

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ module Tartrazine
"GenericSubheading" => "gs",
"GenericTraceback" => "gt",
"GenericUnderline" => "gu",
"Highlight" => "hl",
"Keyword" => "k",
"KeywordConstant" => "kc",
"KeywordDeclaration" => "kd",

View File

@ -1,5 +1,4 @@
require "./actions"
require "./constants"
require "./formatter"
require "./rules"
require "./styles"

View File

@ -2,10 +2,16 @@ require "../formatter"
module Tartrazine
class Ansi < Formatter
property? line_numbers : Bool = false
def format(text : String, lexer : Lexer, theme : Theme) : String
output = String.build do |outp|
lexer.tokenize(text).each do |token|
outp << self.colorize(token[:value], token[:type], theme)
lexer.group_tokens_in_lines(lexer.tokenize(text)).each_with_index do |line, i|
label = line_numbers? ? "#{i + 1}".rjust(4).ljust(5) : ""
outp << label
line.each do |token|
outp << colorize(token[:value], token[:type], theme)
end
end
end
output

View File

@ -1,3 +1,4 @@
require "../constants/token_abbrevs.cr"
require "../formatter"
module Tartrazine
@ -36,10 +37,10 @@ module Tartrazine
end
def format_text(text : String, lexer : Lexer, theme : Theme) : String
lines = group_tokens_in_lines(lexer.tokenize(text))
lines = lexer.group_tokens_in_lines(lexer.tokenize(text))
output = String.build do |outp|
if surrounding_pre?
pre_style= wrap_long_lines? ? "style=\"white-space: pre-wrap; word-break: break-word;\"" : ""
pre_style = wrap_long_lines? ? "style=\"white-space: pre-wrap; word-break: break-word;\"" : ""
outp << "<pre class=\"#{get_css_class("Background", theme)}\" #{pre_style}>"
end
outp << "<code class=\"#{get_css_class("Background", theme)}\">"
@ -47,7 +48,7 @@ module Tartrazine
line_label = line_numbers? ? "#{i + 1}".rjust(4).ljust(5) : ""
line_class = highlighted?(i + 1) ? "class=\"#{get_css_class("LineHighlight", theme)}\"" : ""
line_id = linkable_line_numbers? ? "id=\"#{line_number_id_prefix}#{i + 1}\"" : ""
outp << "<span #{line_id} #{line_class}>#{line_label}</span>"
outp << "<span #{line_id} #{line_class} style=\"user-select: none;\">#{line_label} </span>"
line.each do |token|
fragment = "<span class=\"#{get_css_class(token[:type], theme)}\">#{token[:value]}</span>"
outp << fragment
@ -100,28 +101,5 @@ module Tartrazine
def highlighted?(line : Int) : Bool
highlight_lines.any?(&.includes?(line))
end
def group_tokens_in_lines(tokens : Array(Token)) : Array(Array(Token))
split_tokens = [] of Token
tokens.each do |token|
if token[:value].includes?("\n")
values = token[:value].split("\n")
values.each_with_index do |value, index|
value += "\n" if index < values.size - 1
split_tokens << {type: token[:type], value: value}
end
else
split_tokens << token
end
end
lines = [Array(Token).new]
split_tokens.each do |token|
lines.last << token
if token[:value].includes?("\n")
lines << Array(Token).new
end
end
lines
end
end
end

View File

@ -1,3 +1,5 @@
require "./constants/lexers"
module Tartrazine
class LexerFiles
extend BakedFileSystem
@ -5,6 +7,36 @@ module Tartrazine
bake_folder "../lexers", __DIR__
end
# Get the lexer object for a language name
# FIXME: support mimetypes
def self.lexer(name : String? = nil, filename : String? = nil) : Lexer
if name.nil? && filename.nil?
lexer_file_name = LEXERS_BY_NAME["plaintext"]
elsif name && name != "autodetect"
lexer_file_name = LEXERS_BY_NAME[name.downcase]
else
# Guess by filename
candidates = Set(String).new
LEXERS_BY_FILENAME.each do |k, v|
candidates += v.to_set if File.match?(k, File.basename(filename.to_s))
end
case candidates.size
when 0
lexer_file_name = LEXERS_BY_NAME["plaintext"]
when 1
lexer_file_name = candidates.first
else
raise Exception.new("Multiple lexers match the filename: #{candidates.to_a.join(", ")}")
end
end
Lexer.from_xml(LexerFiles.get("/#{lexer_file_name}.xml").gets_to_end)
end
# Return a list of all lexers
def self.lexers : Array(String)
LEXERS_BY_NAME.keys.sort!
end
# This implements a lexer for Pygments RegexLexers as expressed
# in Chroma's XML serialization.
#
@ -92,6 +124,30 @@ module Tartrazine
result
end
# Group tokens into lines, splitting them when a newline is found
def group_tokens_in_lines(tokens : Array(Token)) : Array(Array(Token))
split_tokens = [] of Token
tokens.each do |token|
if token[:value].includes?("\n")
values = token[:value].split("\n")
values.each_with_index do |value, index|
value += "\n" if index < values.size - 1
split_tokens << {type: token[:type], value: value}
end
else
split_tokens << token
end
end
lines = [Array(Token).new]
split_tokens.each do |token|
lines.last << token
if token[:value].includes?("\n")
lines << Array(Token).new
end
end
lines
end
# ameba:disable Metrics/CyclomaticComplexity
def self.from_xml(xml : String) : Lexer
l = Lexer.new
@ -173,8 +229,4 @@ module Tartrazine
# A token, the output of the tokenizer
alias Token = NamedTuple(type: String, value: String)
def self.lexer(name : String) : Lexer
Lexer.from_xml(LexerFiles.get("/#{name}.xml").gets_to_end)
end
end

View File

@ -1,34 +1,95 @@
require "docopt"
require "./**"
HELP = <<-HELP
tartrazine: a syntax highlighting tool
Usage:
tartrazine (-h, --help)
tartrazine FILE -f html [-t theme][--standalone][--line-numbers]
[-l lexer] [-o output][--css]
tartrazine FILE -f terminal [-t theme][-l lexer][-o output]
[-l lexer][-o output]
tartrazine -f html -t theme --css
tartrazine FILE -f terminal [-t theme][-l lexer][--line-numbers]
[-o output]
tartrazine FILE -f json [-o output]
tartrazine --list-themes
tartrazine --list-lexers
tartrazine --list-formatters
tartrazine --version
-f <formatter> Format to use (html, terminal, json)
-t <theme> Theme to use (see --list-themes)
-l <lexer> Lexer (language) to use (see --list-lexers)
-o <output> Output file (default: stdout)
--standalone Generate a standalone HTML file
--css Generate a CSS file for the theme
--line-numbers Include line numbers in the output
Options:
-f <formatter> Format to use (html, terminal, json)
-t <theme> Theme to use, see --list-themes [default: default-dark]
-l <lexer> Lexer (language) to use, see --list-lexers [default: autodetect]
-o <output> Output file. Default is stdout.
--standalone Generate a standalone HTML file, which includes
all style information. If not given, it will generate just
a HTML fragment ready to include in your own page.
--css Generate a CSS file for the theme called <theme>.css
--line-numbers Include line numbers in the output
-h, --help Show this screen
-v, --version Show version number
HELP
lexer = Tartrazine.lexer("crystal")
theme = Tartrazine.theme(ARGV[1])
formatter = Tartrazine::Json.new
# formatter.standalone = true
# formatter.class_prefix = "hl-"
# formatter.line_number_id_prefix = "ln-"
# formatter.line_numbers = true
# formatter.highlight_lines = [3..7, 20..30]
# formatter.linkable_line_numbers = false
# formatter.wrap_long_lines = false
puts formatter.format(File.read(ARGV[0]), lexer, theme)
options = Docopt.docopt(HELP, ARGV)
# Handle version manually
if options["--version"]
puts "tartrazine #{Tartrazine::VERSION}"
exit 0
end
if options["--list-themes"]
puts Tartrazine.themes.join("\n")
exit 0
end
if options["--list-lexers"]
puts Tartrazine.lexers.join("\n")
exit 0
end
if options["--list-formatters"]
puts "html\njson\nterminal"
exit 0
end
if options["-f"]
formatter = options["-f"].as(String)
case formatter
when "html"
formatter = Tartrazine::Html.new
formatter.standalone = options["--standalone"] != nil
formatter.line_numbers = options["--line-numbers"] != nil
when "terminal"
formatter = Tartrazine::Ansi.new
formatter.line_numbers = options["--line-numbers"] != nil
when "json"
formatter = Tartrazine::Json.new
else
puts "Invalid formatter: #{formatter}"
exit 1
end
theme = Tartrazine.theme(options["-t"].as(String))
if formatter.is_a?(Tartrazine::Html) && options["--css"]
File.open("#{options["-t"].as(String)}.css", "w") do |outf|
outf.puts formatter.get_style_defs(theme)
end
exit 0
end
lexer = Tartrazine.lexer(name: options["-l"].as(String), filename: options["FILE"].as(String))
input = File.open(options["FILE"].as(String)).gets_to_end
output = formatter.format(input, lexer, theme)
if options["-o"].nil?
puts output
else
File.open(options["-o"].as(String), "w") do |outf|
outf.puts output
end
end
end

View File

@ -1,5 +1,4 @@
require "./actions"
require "./constants"
require "./formatter"
require "./rules"
require "./styles"

View File

@ -1,5 +1,4 @@
require "./actions"
require "./constants"
require "./formatter"
require "./rules"
require "./styles"
@ -10,22 +9,34 @@ require "xml"
module Tartrazine
alias Color = Sixteen::Color
class ThemeFiles
extend BakedFileSystem
bake_folder "../styles", __DIR__
end
def self.theme(name : String) : Theme
begin
return Theme.from_base16(name)
return Theme.from_base16(name)
rescue ex : Exception
raise ex unless ex.message.try &.includes? "Theme not found"
end
begin
return Theme.from_xml(ThemeFiles.get("/#{name}.xml").gets_to_end)
Theme.from_xml(ThemeFiles.get("/#{name}.xml").gets_to_end)
rescue
raise Exception.new("Theme #{name} not found")
end
end
class ThemeFiles
extend BakedFileSystem
bake_folder "../styles", __DIR__
# Return a list of all themes
def self.themes
themes = Set(String).new
ThemeFiles.files.each do |file|
themes << file.path.split("/").last.split(".").first
end
Sixteen::DataFiles.files.each do |file|
themes << file.path.split("/").last.split(".").first
end
themes.to_a.sort!
end
class Style
@ -111,7 +122,7 @@ module Tartrazine
# The color assignments are adapted from
# https://github.com/mohd-akram/base16-pygments/
theme.styles["Background"] = Style.new(color: t["base05"], background: t["base00"])
theme.styles["Background"] = Style.new(color: t["base05"], background: t["base00"], bold: true)
theme.styles["LineHighlight"] = Style.new(color: t["base0D"], background: t["base01"])
theme.styles["Text"] = Style.new(color: t["base05"])
theme.styles["Error"] = Style.new(color: t["base08"])
@ -175,27 +186,22 @@ module Tartrazine
if !theme.styles.has_key?("LineHighlight")
theme.styles["LineHighlight"] = Style.new
theme.styles["LineHighlight"].background = make_highlight_color(theme.styles["Background"].background)
theme.styles["LineHighlight"].bold = true
end
theme
end
# If the color is dark, make it brighter and viceversa
def self.make_highlight_color(base_color)
# FIXME: do a proper luminance adjustment in the color class
return nil if base_color.nil?
color = Color.new(base_color.hex)
if base_color.light?
color.r = [(base_color.r - 40), 255].min.to_u8
color.g = [(base_color.g - 40), 255].min.to_u8
color.b = [(base_color.b - 40), 255].min.to_u8
else
color.r = [(base_color.r + 40), 255].min.to_u8
color.g = [(base_color.g + 40), 255].min.to_u8
color.b = [(base_color.b + 40), 255].min.to_u8
if base_color.nil?
# WHo knows
return Color.new(127, 127, 127)
end
if base_color.dark?
base_color.lighter(0.2)
else
base_color.darker(0.2)
end
# Bug in color, setting rgb doesn't update hex
color.hex = "#{color.r.to_s(16)}#{color.g.to_s(16)}#{color.b.to_s(16)}"
color
end
end
end

View File

@ -1,5 +1,4 @@
require "./actions"
require "./constants"
require "./formatter"
require "./rules"
require "./styles"
@ -12,7 +11,7 @@ require "xml"
module Tartrazine
extend self
VERSION = "0.1.1"
VERSION = "0.2.0"
Log = ::Log.for("tartrazine")
end