Class: Rich::Syntax

Inherits:
Object
  • Object
show all
Defined in:
lib/rich/syntax.rb

Overview

Syntax highlighting for source code. Provides token-based syntax highlighting for multiple programming languages.

Constant Summary collapse

DEFAULT_THEME =

Default theme for syntax highlighting

{
  # Keywords
  keyword: Style.new(color: Color.parse("magenta"), bold: true),
  keyword_constant: Style.new(color: Color.parse("cyan"), bold: true),
  keyword_declaration: Style.new(color: Color.parse("magenta"), bold: true),
  keyword_namespace: Style.new(color: Color.parse("magenta"), bold: true),
  keyword_type: Style.new(color: Color.parse("cyan")),

  # Names
  name: Style.new(color: Color.parse("white")),
  name_builtin: Style.new(color: Color.parse("cyan")),
  name_class: Style.new(color: Color.parse("green"), bold: true),
  name_constant: Style.new(color: Color.parse("cyan")),
  name_decorator: Style.new(color: Color.parse("bright_magenta")),
  name_exception: Style.new(color: Color.parse("green"), bold: true),
  name_function: Style.new(color: Color.parse("green")),
  name_variable: Style.new(color: Color.parse("white")),
  name_tag: Style.new(color: Color.parse("bright_magenta")),
  name_attribute: Style.new(color: Color.parse("yellow")),

  # Literals
  string: Style.new(color: Color.parse("yellow")),
  string_doc: Style.new(color: Color.parse("yellow"), italic: true),
  string_escape: Style.new(color: Color.parse("bright_magenta")),
  string_interpol: Style.new(color: Color.parse("bright_magenta")),
  string_regex: Style.new(color: Color.parse("bright_yellow")),
  string_symbol: Style.new(color: Color.parse("bright_green")),

  number: Style.new(color: Color.parse("cyan")),
  number_float: Style.new(color: Color.parse("cyan")),
  number_hex: Style.new(color: Color.parse("cyan")),

  # Operators and Punctuation
  operator: Style.new(color: Color.parse("bright_magenta")),
  punctuation: Style.new(color: Color.parse("white")),

  # Comments
  comment: Style.new(color: Color.parse("bright_black"), italic: true),
  comment_doc: Style.new(color: Color.parse("bright_black"), italic: true),
  comment_preproc: Style.new(color: Color.parse("bright_magenta")),

  # Generic
  generic_deleted: Style.new(color: Color.parse("red")),
  generic_inserted: Style.new(color: Color.parse("green")),
  generic_heading: Style.new(color: Color.parse("bright_blue"), bold: true),
  generic_subheading: Style.new(color: Color.parse("bright_blue")),
  generic_error: Style.new(color: Color.parse("bright_red")),

  # Other
  text: Style.new,
  error: Style.new(color: Color.parse("bright_red"), bold: true)
}.freeze
MONOKAI_THEME =

Monokai theme

{
  keyword: Style.new(color: Color.parse("#f92672"), bold: true),
  keyword_constant: Style.new(color: Color.parse("#ae81ff")),
  keyword_type: Style.new(color: Color.parse("#66d9ef"), italic: true),
  name: Style.new(color: Color.parse("#f8f8f2")),
  name_builtin: Style.new(color: Color.parse("#66d9ef")),
  name_class: Style.new(color: Color.parse("#a6e22e")),
  name_function: Style.new(color: Color.parse("#a6e22e")),
  name_decorator: Style.new(color: Color.parse("#a6e22e")),
  string: Style.new(color: Color.parse("#e6db74")),
  string_doc: Style.new(color: Color.parse("#e6db74")),
  number: Style.new(color: Color.parse("#ae81ff")),
  operator: Style.new(color: Color.parse("#f92672")),
  comment: Style.new(color: Color.parse("#75715e"), italic: true),
  punctuation: Style.new(color: Color.parse("#f8f8f2")),
  text: Style.new(color: Color.parse("#f8f8f2")),
  error: Style.new(color: Color.parse("#f92672"), bold: true)
}.freeze
DRACULA_THEME =

Dracula theme

{
  keyword: Style.new(color: Color.parse("#ff79c6"), bold: true),
  keyword_constant: Style.new(color: Color.parse("#bd93f9")),
  keyword_type: Style.new(color: Color.parse("#8be9fd"), italic: true),
  name: Style.new(color: Color.parse("#f8f8f2")),
  name_builtin: Style.new(color: Color.parse("#8be9fd")),
  name_class: Style.new(color: Color.parse("#50fa7b")),
  name_function: Style.new(color: Color.parse("#50fa7b")),
  name_decorator: Style.new(color: Color.parse("#50fa7b")),
  string: Style.new(color: Color.parse("#f1fa8c")),
  string_doc: Style.new(color: Color.parse("#6272a4")),
  number: Style.new(color: Color.parse("#bd93f9")),
  operator: Style.new(color: Color.parse("#ff79c6")),
  comment: Style.new(color: Color.parse("#6272a4"), italic: true),
  punctuation: Style.new(color: Color.parse("#f8f8f2")),
  text: Style.new(color: Color.parse("#f8f8f2")),
  error: Style.new(color: Color.parse("#ff5555"), bold: true)
}.freeze
THEMES =
{
  default: DEFAULT_THEME,
  monokai: MONOKAI_THEME,
  dracula: DRACULA_THEME
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(code, language: "text", theme: :default, line_numbers: false, start_line: 1, highlight_lines: nil, word_wrap: false, background_style: nil, tab_size: 4) ⇒ Syntax

Returns a new instance of Syntax.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/rich/syntax.rb', line 139

def initialize(
  code,
  language: "text",
  theme: :default,
  line_numbers: false,
  start_line: 1,
  highlight_lines: nil,
  word_wrap: false,
  background_style: nil,
  tab_size: 4
)
  @code = code.to_s
  @language = language.to_s.downcase
  @theme = theme.is_a?(Hash) ? theme : (THEMES[theme] || DEFAULT_THEME)
  @line_numbers = line_numbers
  @start_line = start_line
  @highlight_lines = highlight_lines
  @word_wrap = word_wrap
  @background_style = background_style
  @tab_size = tab_size
end

Instance Attribute Details

#background_styleStyle? (readonly)

Returns Background style.

Returns:

  • (Style, nil)

    Background style



134
135
136
# File 'lib/rich/syntax.rb', line 134

def background_style
  @background_style
end

#codeString (readonly)

Returns Source code.

Returns:

  • (String)

    Source code



113
114
115
# File 'lib/rich/syntax.rb', line 113

def code
  @code
end

#highlight_linesArray<Integer>? (readonly)

Returns Lines to highlight.

Returns:

  • (Array<Integer>, nil)

    Lines to highlight



128
129
130
# File 'lib/rich/syntax.rb', line 128

def highlight_lines
  @highlight_lines
end

#languageString (readonly)

Returns Language name.

Returns:

  • (String)

    Language name



116
117
118
# File 'lib/rich/syntax.rb', line 116

def language
  @language
end

#line_numbersBoolean (readonly)

Returns Show line numbers.

Returns:

  • (Boolean)

    Show line numbers



122
123
124
# File 'lib/rich/syntax.rb', line 122

def line_numbers
  @line_numbers
end

#start_lineInteger? (readonly)

Returns Starting line number.

Returns:

  • (Integer, nil)

    Starting line number



125
126
127
# File 'lib/rich/syntax.rb', line 125

def start_line
  @start_line
end

#tab_sizeInteger (readonly)

Returns Tab size.

Returns:

  • (Integer)

    Tab size



137
138
139
# File 'lib/rich/syntax.rb', line 137

def tab_size
  @tab_size
end

#themeHash (readonly)

Returns Theme styles.

Returns:

  • (Hash)

    Theme styles



119
120
121
# File 'lib/rich/syntax.rb', line 119

def theme
  @theme
end

#word_wrapBoolean (readonly)

Returns Word wrap.

Returns:

  • (Boolean)

    Word wrap



131
132
133
# File 'lib/rich/syntax.rb', line 131

def word_wrap
  @word_wrap
end

Class Method Details

.detect_language(path) ⇒ String

Detect language from file extension

Parameters:

  • path (String)

    File path

Returns:

  • (String)


241
242
243
244
# File 'lib/rich/syntax.rb', line 241

def detect_language(path)
  ext = File.extname(path).downcase.delete(".")
  EXTENSION_MAP[ext] || "text"
end

.from_file(path, **kwargs) ⇒ Syntax

Create syntax from file

Parameters:

  • path (String)

    File path

  • kwargs (Hash)

    Options

Returns:



232
233
234
235
236
# File 'lib/rich/syntax.rb', line 232

def from_file(path, **kwargs)
  code = File.read(path)
  language = kwargs.delete(:language) || detect_language(path)
  new(code, language: language, **kwargs)
end

.supported_languagesArray<String>

List supported languages

Returns:

  • (Array<String>)


248
249
250
# File 'lib/rich/syntax.rb', line 248

def supported_languages
  LEXERS.keys.sort
end

Instance Method Details

#highlight_line(line) ⇒ Array<Segment>

Highlight a single line

Parameters:

  • line (String)

    Line to highlight

Returns:



201
202
203
204
# File 'lib/rich/syntax.rb', line 201

def highlight_line(line)
  lexer = get_lexer(@language)
  lexer.tokenize(line, @theme)
end

#render(color_system: ColorSystem::TRUECOLOR) ⇒ String

Render to string with ANSI codes

Parameters:

  • color_system (Symbol) (defaults to: ColorSystem::TRUECOLOR)

    Color system

Returns:

  • (String)


209
210
211
# File 'lib/rich/syntax.rb', line 209

def render(color_system: ColorSystem::TRUECOLOR)
  Segment.render(to_segments, color_system: color_system)
end

#to_panel(title: nil, max_width: 80) ⇒ String

Render inside a panel

Parameters:

  • title (String, nil) (defaults to: nil)

    Panel title

Returns:

  • (String)


216
217
218
219
220
221
222
223
224
225
# File 'lib/rich/syntax.rb', line 216

def to_panel(title: nil, max_width: 80)
  title ||= @language.capitalize
  panel = Panel.new(
    render,
    title: title,
    border_style: "dim",
    padding: 0
  )
  panel.render(max_width: max_width)
end

#to_segmentsArray<Segment>

Highlight the code and return segments

Returns:



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/rich/syntax.rb', line 163

def to_segments
  segments = []
  lines = @code.gsub("\t", " " * @tab_size).split("\n", -1)

  # Calculate line number width
  line_num_width = (@start_line + lines.length - 1).to_s.length

  lines.each_with_index do |line, index|
    line_num = @start_line + index
    is_highlighted = @highlight_lines&.include?(line_num)

    # Line number
    if @line_numbers
      num_style = is_highlighted ? Style.new(color: Color.parse("yellow"), bold: true) : Style.new(color: Color.parse("bright_black"))
      segments << Segment.new(line_num.to_s.rjust(line_num_width), style: num_style)
      segments << Segment.new("", style: Style.new(color: Color.parse("bright_black")))
    end

    # Highlighted line background
    if is_highlighted
      bg_style = Style.new(bgcolor: Color.parse("color(237)"))
      segments.concat(highlight_line(line).map do |seg|
        combined_style = seg.style ? seg.style + bg_style : bg_style
        Segment.new(seg.text, style: combined_style)
      end)
    else
      segments.concat(highlight_line(line))
    end

    segments << Segment.new("\n") if index < lines.length - 1
  end

  segments
end