1
0
mirror of https://github.com/alecthomas/chroma.git synced 2025-03-19 21:10:15 +02:00

Support for highlighting ranges of lines.

This commit is contained in:
Alec Thomas 2017-09-20 14:15:06 +10:00
parent 3f230ec717
commit a5637e60b2
5 changed files with 165 additions and 118 deletions

View File

@ -40,8 +40,9 @@ var (
htmlInlineStyleFlag = kingpin.Flag("html-inline-styles", "Output HTML with inline styles (no classes).").Bool() htmlInlineStyleFlag = kingpin.Flag("html-inline-styles", "Output HTML with inline styles (no classes).").Bool()
htmlTabWidthFlag = kingpin.Flag("html-tab-width", "Set the HTML tab width.").Default("8").Int() htmlTabWidthFlag = kingpin.Flag("html-tab-width", "Set the HTML tab width.").Default("8").Int()
htmlLinesFlag = kingpin.Flag("html-line-numbers", "Include line numbers in output.").Bool() htmlLinesFlag = kingpin.Flag("html-line-numbers", "Include line numbers in output.").Bool()
htmlHighlightStyleFlag = kingpin.Flag("html-highlight-style", "Style used for highlighting lines.").Default("bg:#yellow").String() htmlLinesStyleFlag = kingpin.Flag("html-line-numbers-style", "Style for line numbers.").Default("#888").String()
htmlHighlightFlag = kingpin.Flag("html-highlight", "Highlight these ranges (N:M).").Strings() htmlHighlightFlag = kingpin.Flag("html-highlight", "Highlight these lines.").PlaceHolder("N[:M][,...]").String()
htmlHighlightStyleFlag = kingpin.Flag("html-highlight-style", "Style used for highlighting lines.").Default("bg:#282828").String()
filesArgs = kingpin.Arg("files", "Files to highlight.").ExistingFiles() filesArgs = kingpin.Arg("files", "Files to highlight.").ExistingFiles()
) )
@ -94,6 +95,16 @@ command, for Go.
if *htmlFlag { if *htmlFlag {
*formatterFlag = "html" *formatterFlag = "html"
} }
// Retrieve user-specified style, clone it, and add some overrides.
style := styles.Get(*styleFlag).Clone()
if *htmlHighlightStyleFlag != "" {
style.Add(chroma.LineHighlight, *htmlHighlightStyleFlag)
}
if *htmlLinesStyleFlag != "" {
style.Add(chroma.LineNumbers, *htmlLinesStyleFlag)
}
if *formatterFlag == "html" { if *formatterFlag == "html" {
options := []html.Option{html.TabWidth(*htmlTabWidthFlag)} options := []html.Option{html.TabWidth(*htmlTabWidthFlag)}
if *htmlPrefixFlag != "" { if *htmlPrefixFlag != "" {
@ -103,7 +114,7 @@ command, for Go.
// Dump styles. // Dump styles.
if *htmlStylesFlag { if *htmlStylesFlag {
formatter := html.New(html.WithClasses()) formatter := html.New(html.WithClasses())
formatter.WriteCSS(w, styles.Get(*styleFlag)) formatter.WriteCSS(w, style)
return return
} }
if !*htmlInlineStyleFlag { if !*htmlInlineStyleFlag {
@ -117,22 +128,25 @@ command, for Go.
} }
if len(*htmlHighlightFlag) > 0 { if len(*htmlHighlightFlag) > 0 {
ranges := [][2]int{} ranges := [][2]int{}
for _, span := range *htmlHighlightFlag { for _, span := range strings.Split(*htmlHighlightFlag, ",") {
parts := strings.Split(span, ":") parts := strings.Split(span, ":")
if len(parts) != 2 { if len(parts) > 2 {
kingpin.Fatalf("range should be N:M, not %q", span) kingpin.Fatalf("range should be N[:M], not %q", span)
} }
start, err := strconv.ParseInt(parts[0], 10, 64) start, err := strconv.ParseInt(parts[0], 10, 64)
kingpin.FatalIfError(err, "min value of range should be integer not %q", parts[0]) kingpin.FatalIfError(err, "min value of range should be integer not %q", parts[0])
end, err := strconv.ParseInt(parts[0], 10, 64) end := start
kingpin.FatalIfError(err, "max value of range should be integer not %q", parts[0]) if len(parts) == 2 {
end, err = strconv.ParseInt(parts[1], 10, 64)
kingpin.FatalIfError(err, "max value of range should be integer not %q", parts[1])
}
ranges = append(ranges, [2]int{int(start), int(end)}) ranges = append(ranges, [2]int{int(start), int(end)})
} }
options = append(options, html.HighlightLines(*htmlHighlightStyleFlag, ranges)) options = append(options, html.HighlightLines(ranges))
} }
formatters.Register("html", html.New(options...)) formatters.Register("html", html.New(options...))
} }
writer := getWriter(w) writer := getWriter(w, style)
if len(*filesArgs) == 0 { if len(*filesArgs) == 0 {
contents, err := ioutil.ReadAll(os.Stdin) contents, err := ioutil.ReadAll(os.Stdin)
kingpin.FatalIfError(err, "") kingpin.FatalIfError(err, "")
@ -201,8 +215,7 @@ func selexer(path, contents string) (lexer chroma.Lexer) {
return lexers.Analyse(contents) return lexers.Analyse(contents)
} }
func getWriter(w io.Writer) func(*chroma.Token) { func getWriter(w io.Writer, style *chroma.Style) func(*chroma.Token) {
style := styles.Get(*styleFlag)
formatter := formatters.Get(*formatterFlag) formatter := formatters.Get(*formatterFlag)
// formatter := formatters.TTY8 // formatter := formatters.TTY8
writer, err := formatter.Format(w, style) writer, err := formatter.Format(w, style)

View File

@ -32,12 +32,11 @@ func WithLineNumbers() Option {
} }
} }
// HighlightLines higlights the given line ranges. // HighlightLines higlights the given line ranges with the Highlight style.
// //
// A range is the beginning and ending of a range as 1-based line numbers, inclusive. // A range is the beginning and ending of a range as 1-based line numbers, inclusive.
func HighlightLines(style string, ranges [][2]int) Option { func HighlightLines(ranges [][2]int) Option {
return func(f *Formatter) { return func(f *Formatter) {
f.highlightStyle = style
f.highlightRanges = ranges f.highlightRanges = ranges
sort.Sort(f.highlightRanges) sort.Sort(f.highlightRanges)
} }
@ -59,7 +58,6 @@ type Formatter struct {
classes bool classes bool
tabWidth int tabWidth int
lineNumbers bool lineNumbers bool
highlightStyle string
highlightRanges highlightRanges highlightRanges highlightRanges
} }
@ -104,7 +102,21 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []*chroma
fmt.Fprintf(w, "<pre%s>\n", f.styleAttr(css, chroma.Background)) fmt.Fprintf(w, "<pre%s>\n", f.styleAttr(css, chroma.Background))
lines := splitTokensIntoLines(tokens) lines := splitTokensIntoLines(tokens)
lineDigits := len(fmt.Sprintf("%d", len(lines))) lineDigits := len(fmt.Sprintf("%d", len(lines)))
highlightIndex := 0
for line, tokens := range lines { for line, tokens := range lines {
highlight := false
for highlightIndex < len(f.highlightRanges) && line+1 > f.highlightRanges[highlightIndex][1] {
highlightIndex++
}
if highlightIndex < len(f.highlightRanges) {
hrange := f.highlightRanges[highlightIndex]
if line+1 >= hrange[0] && line+1 <= hrange[1] {
highlight = true
}
}
if highlight {
fmt.Fprintf(w, "<span class=\"hl\">")
}
if f.lineNumbers { if f.lineNumbers {
fmt.Fprintf(w, "<span class=\"ln\">%*d</span>", lineDigits, line+1) fmt.Fprintf(w, "<span class=\"ln\">%*d</span>", lineDigits, line+1)
} }
@ -117,6 +129,9 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []*chroma
} }
fmt.Fprint(w, html) fmt.Fprint(w, html)
} }
if highlight {
fmt.Fprintf(w, "</span>")
}
} }
fmt.Fprint(w, "</pre>\n") fmt.Fprint(w, "</pre>\n")
@ -134,7 +149,7 @@ func (f *Formatter) class(tt chroma.TokenType) string {
return "chroma" return "chroma"
case chroma.LineNumbers: case chroma.LineNumbers:
return "ln" return "ln"
case chroma.Highlight: case chroma.LineHighlight:
return "hl" return "hl"
} }
if tt < 0 { if tt < 0 {
@ -173,11 +188,6 @@ func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
if _, err := fmt.Fprintf(w, "/* %s */ .chroma { %s }\n", chroma.Background, css[chroma.Background]); err != nil { if _, err := fmt.Fprintf(w, "/* %s */ .chroma { %s }\n", chroma.Background, css[chroma.Background]); err != nil {
return err return err
} }
// No line-numbers, add a default.
if _, ok := css[chroma.LineNumbers]; !ok {
css[chroma.LineNumbers] = "color: #888"
}
css[chroma.LineNumbers] += "; margin-right: 0.5em"
tts := []int{} tts := []int{}
for tt := range css { for tt := range css {
tts = append(tts, int(tt)) tts = append(tts, int(tt))
@ -199,11 +209,6 @@ func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string { func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
bg := style.Get(chroma.Background) bg := style.Get(chroma.Background)
classes := map[chroma.TokenType]string{} classes := map[chroma.TokenType]string{}
// Insert highlight colour if needed.
if len(f.highlightRanges) > 0 {
highlight := chroma.ParseStyleEntry(bg, f.highlightStyle).Sub(bg)
classes[chroma.Highlight] = StyleEntryToCSS(highlight)
}
// Convert the style. // Convert the style.
for t := range style.Entries { for t := range style.Entries {
e := style.Entries[t] e := style.Entries[t]
@ -213,6 +218,8 @@ func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string
classes[t] = StyleEntryToCSS(e) classes[t] = StyleEntryToCSS(e)
} }
classes[chroma.Background] += f.tabWidthStyle() classes[chroma.Background] += f.tabWidthStyle()
classes[chroma.LineNumbers] += "; margin-right: 0.5em"
classes[chroma.LineHighlight] += "; display: block; width: 100%"
return classes return classes
} }

View File

@ -17,6 +17,13 @@ type StyleEntry struct {
Underline bool Underline bool
} }
// Clone this StyleEntry.
func (s *StyleEntry) Clone() *StyleEntry {
clone := &StyleEntry{}
*clone = *s
return clone
}
func (s *StyleEntry) String() string { func (s *StyleEntry) String() string {
out := []string{} out := []string{}
if s.Bold { if s.Bold {
@ -89,6 +96,18 @@ type Style struct {
Entries map[TokenType]*StyleEntry Entries map[TokenType]*StyleEntry
} }
// Clone this style. The clone can then be safely modified.
func (s *Style) Clone() *Style {
clone := &Style{
Name: s.Name,
Entries: map[TokenType]*StyleEntry{},
}
for tt, e := range s.Entries {
clone.Entries[tt] = e.Clone()
}
return clone
}
// Get a style entry. Will try sub-category or category if an exact match is not found, and // Get a style entry. Will try sub-category or category if an exact match is not found, and
// finally return the entry mapped to `InheritStyle`. // finally return the entry mapped to `InheritStyle`.
func (s *Style) Get(ttype TokenType) *StyleEntry { func (s *Style) Get(ttype TokenType) *StyleEntry {

View File

@ -4,102 +4,102 @@ package chroma
import "fmt" import "fmt"
const _TokenType_name = "EOFNoneOtherErrorEscapeHighlightLineNumbersBackgroundKeywordKeywordConstantKeywordDeclarationKeywordNamespaceKeywordPseudoKeywordReservedKeywordTypeNameNameAttributeNameBuiltinNameBuiltinPseudoNameClassNameConstantNameDecoratorNameEntityNameExceptionNameFunctionNameFunctionMagicNameKeywordNameLabelNameNamespaceNameOperatorNameOtherNamePseudoNamePropertyNameTagNameVariableNameVariableAnonymousNameVariableClassNameVariableGlobalNameVariableInstanceNameVariableMagicLiteralLiteralDateLiteralOtherLiteralStringLiteralStringAffixLiteralStringAtomLiteralStringBacktickLiteralStringBooleanLiteralStringCharLiteralStringDelimiterLiteralStringDocLiteralStringDoubleLiteralStringEscapeLiteralStringHeredocLiteralStringInterpolLiteralStringNameLiteralStringOtherLiteralStringRegexLiteralStringSingleLiteralStringSymbolLiteralNumberLiteralNumberBinLiteralNumberFloatLiteralNumberHexLiteralNumberIntegerLiteralNumberIntegerLongLiteralNumberOctOperatorOperatorWordPunctuationCommentCommentHashbangCommentMultilineCommentSingleCommentSpecialCommentPreprocCommentPreprocFileGenericGenericDeletedGenericEmphGenericErrorGenericHeadingGenericInsertedGenericOutputGenericPromptGenericStrongGenericSubheadingGenericTracebackGenericUnderlineTextTextWhitespaceTextSymbolTextPunctuation" const _TokenType_name = "EOFNoneOtherErrorHighlightLineHighlightLineNumbersBackgroundKeywordKeywordConstantKeywordDeclarationKeywordNamespaceKeywordPseudoKeywordReservedKeywordTypeNameNameAttributeNameBuiltinNameBuiltinPseudoNameClassNameConstantNameDecoratorNameEntityNameExceptionNameFunctionNameFunctionMagicNameKeywordNameLabelNameNamespaceNameOperatorNameOtherNamePseudoNamePropertyNameTagNameVariableNameVariableAnonymousNameVariableClassNameVariableGlobalNameVariableInstanceNameVariableMagicLiteralLiteralDateLiteralOtherLiteralStringLiteralStringAffixLiteralStringAtomLiteralStringBacktickLiteralStringBooleanLiteralStringCharLiteralStringDelimiterLiteralStringDocLiteralStringDoubleLiteralStringEscapeLiteralStringHeredocLiteralStringInterpolLiteralStringNameLiteralStringOtherLiteralStringRegexLiteralStringSingleLiteralStringSymbolLiteralNumberLiteralNumberBinLiteralNumberFloatLiteralNumberHexLiteralNumberIntegerLiteralNumberIntegerLongLiteralNumberOctOperatorOperatorWordPunctuationCommentCommentHashbangCommentMultilineCommentSingleCommentSpecialCommentPreprocCommentPreprocFileGenericGenericDeletedGenericEmphGenericErrorGenericHeadingGenericInsertedGenericOutputGenericPromptGenericStrongGenericSubheadingGenericTracebackGenericUnderlineTextTextWhitespaceTextSymbolTextPunctuation"
var _TokenType_map = map[TokenType]string{ var _TokenType_map = map[TokenType]string{
-8: _TokenType_name[0:3], -8: _TokenType_name[0:3],
-7: _TokenType_name[3:7], -7: _TokenType_name[3:7],
-6: _TokenType_name[7:12], -6: _TokenType_name[7:12],
-5: _TokenType_name[12:17], -5: _TokenType_name[12:17],
-4: _TokenType_name[17:23], -4: _TokenType_name[17:26],
-3: _TokenType_name[23:32], -3: _TokenType_name[26:39],
-2: _TokenType_name[32:43], -2: _TokenType_name[39:50],
-1: _TokenType_name[43:53], -1: _TokenType_name[50:60],
1000: _TokenType_name[53:60], 1000: _TokenType_name[60:67],
1001: _TokenType_name[60:75], 1001: _TokenType_name[67:82],
1002: _TokenType_name[75:93], 1002: _TokenType_name[82:100],
1003: _TokenType_name[93:109], 1003: _TokenType_name[100:116],
1004: _TokenType_name[109:122], 1004: _TokenType_name[116:129],
1005: _TokenType_name[122:137], 1005: _TokenType_name[129:144],
1006: _TokenType_name[137:148], 1006: _TokenType_name[144:155],
2000: _TokenType_name[148:152], 2000: _TokenType_name[155:159],
2001: _TokenType_name[152:165], 2001: _TokenType_name[159:172],
2002: _TokenType_name[165:176], 2002: _TokenType_name[172:183],
2003: _TokenType_name[176:193], 2003: _TokenType_name[183:200],
2004: _TokenType_name[193:202], 2004: _TokenType_name[200:209],
2005: _TokenType_name[202:214], 2005: _TokenType_name[209:221],
2006: _TokenType_name[214:227], 2006: _TokenType_name[221:234],
2007: _TokenType_name[227:237], 2007: _TokenType_name[234:244],
2008: _TokenType_name[237:250], 2008: _TokenType_name[244:257],
2009: _TokenType_name[250:262], 2009: _TokenType_name[257:269],
2010: _TokenType_name[262:279], 2010: _TokenType_name[269:286],
2011: _TokenType_name[279:290], 2011: _TokenType_name[286:297],
2012: _TokenType_name[290:299], 2012: _TokenType_name[297:306],
2013: _TokenType_name[299:312], 2013: _TokenType_name[306:319],
2014: _TokenType_name[312:324], 2014: _TokenType_name[319:331],
2015: _TokenType_name[324:333], 2015: _TokenType_name[331:340],
2016: _TokenType_name[333:343], 2016: _TokenType_name[340:350],
2017: _TokenType_name[343:355], 2017: _TokenType_name[350:362],
2018: _TokenType_name[355:362], 2018: _TokenType_name[362:369],
2019: _TokenType_name[362:374], 2019: _TokenType_name[369:381],
2020: _TokenType_name[374:395], 2020: _TokenType_name[381:402],
2021: _TokenType_name[395:412], 2021: _TokenType_name[402:419],
2022: _TokenType_name[412:430], 2022: _TokenType_name[419:437],
2023: _TokenType_name[430:450], 2023: _TokenType_name[437:457],
2024: _TokenType_name[450:467], 2024: _TokenType_name[457:474],
3000: _TokenType_name[467:474], 3000: _TokenType_name[474:481],
3001: _TokenType_name[474:485], 3001: _TokenType_name[481:492],
3002: _TokenType_name[485:497], 3002: _TokenType_name[492:504],
3100: _TokenType_name[497:510], 3100: _TokenType_name[504:517],
3101: _TokenType_name[510:528], 3101: _TokenType_name[517:535],
3102: _TokenType_name[528:545], 3102: _TokenType_name[535:552],
3103: _TokenType_name[545:566], 3103: _TokenType_name[552:573],
3104: _TokenType_name[566:586], 3104: _TokenType_name[573:593],
3105: _TokenType_name[586:603], 3105: _TokenType_name[593:610],
3106: _TokenType_name[603:625], 3106: _TokenType_name[610:632],
3107: _TokenType_name[625:641], 3107: _TokenType_name[632:648],
3108: _TokenType_name[641:660], 3108: _TokenType_name[648:667],
3109: _TokenType_name[660:679], 3109: _TokenType_name[667:686],
3110: _TokenType_name[679:699], 3110: _TokenType_name[686:706],
3111: _TokenType_name[699:720], 3111: _TokenType_name[706:727],
3112: _TokenType_name[720:737], 3112: _TokenType_name[727:744],
3113: _TokenType_name[737:755], 3113: _TokenType_name[744:762],
3114: _TokenType_name[755:773], 3114: _TokenType_name[762:780],
3115: _TokenType_name[773:792], 3115: _TokenType_name[780:799],
3116: _TokenType_name[792:811], 3116: _TokenType_name[799:818],
3200: _TokenType_name[811:824], 3200: _TokenType_name[818:831],
3201: _TokenType_name[824:840], 3201: _TokenType_name[831:847],
3202: _TokenType_name[840:858], 3202: _TokenType_name[847:865],
3203: _TokenType_name[858:874], 3203: _TokenType_name[865:881],
3204: _TokenType_name[874:894], 3204: _TokenType_name[881:901],
3205: _TokenType_name[894:918], 3205: _TokenType_name[901:925],
3206: _TokenType_name[918:934], 3206: _TokenType_name[925:941],
4000: _TokenType_name[934:942], 4000: _TokenType_name[941:949],
4001: _TokenType_name[942:954], 4001: _TokenType_name[949:961],
5000: _TokenType_name[954:965], 5000: _TokenType_name[961:972],
6000: _TokenType_name[965:972], 6000: _TokenType_name[972:979],
6001: _TokenType_name[972:987], 6001: _TokenType_name[979:994],
6002: _TokenType_name[987:1003], 6002: _TokenType_name[994:1010],
6003: _TokenType_name[1003:1016], 6003: _TokenType_name[1010:1023],
6004: _TokenType_name[1016:1030], 6004: _TokenType_name[1023:1037],
6100: _TokenType_name[1030:1044], 6100: _TokenType_name[1037:1051],
6101: _TokenType_name[1044:1062], 6101: _TokenType_name[1051:1069],
7000: _TokenType_name[1062:1069], 7000: _TokenType_name[1069:1076],
7001: _TokenType_name[1069:1083], 7001: _TokenType_name[1076:1090],
7002: _TokenType_name[1083:1094], 7002: _TokenType_name[1090:1101],
7003: _TokenType_name[1094:1106], 7003: _TokenType_name[1101:1113],
7004: _TokenType_name[1106:1120], 7004: _TokenType_name[1113:1127],
7005: _TokenType_name[1120:1135], 7005: _TokenType_name[1127:1142],
7006: _TokenType_name[1135:1148], 7006: _TokenType_name[1142:1155],
7007: _TokenType_name[1148:1161], 7007: _TokenType_name[1155:1168],
7008: _TokenType_name[1161:1174], 7008: _TokenType_name[1168:1181],
7009: _TokenType_name[1174:1191], 7009: _TokenType_name[1181:1198],
7010: _TokenType_name[1191:1207], 7010: _TokenType_name[1198:1214],
7011: _TokenType_name[1207:1223], 7011: _TokenType_name[1214:1230],
8000: _TokenType_name[1223:1227], 8000: _TokenType_name[1230:1234],
8001: _TokenType_name[1227:1241], 8001: _TokenType_name[1234:1248],
8002: _TokenType_name[1241:1251], 8002: _TokenType_name[1248:1258],
8003: _TokenType_name[1251:1266], 8003: _TokenType_name[1258:1273],
} }
func (i TokenType) String() string { func (i TokenType) String() string {

View File

@ -15,13 +15,21 @@ type TokenType int
// Meta token types. // Meta token types.
const ( const (
// Default background style.
Background TokenType = -1 - iota Background TokenType = -1 - iota
// Line numbers in output.
LineNumbers LineNumbers
// Line higlight style.
LineHighlight
// Character highlight style.
Highlight Highlight
Escape // Input that could not be tokenised.
Error Error
// Other is used by the Delegate lexer to indicate which tokens should be handled by the delegate.
Other Other
// No highlighting.
None None
// Final token.
EOF EOF
) )