From ea3cd96e253f7f84bb5cc9433005eff13ab501b3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Aug 2024 23:53:34 +0300 Subject: [PATCH] format/mdext: add single-character bold, italic and strikethrough parsers --- format/mdext/shortemphasis.go | 96 +++++++++++++++++++++++++++++++++++ format/mdext/shortstrike.go | 76 +++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 format/mdext/shortemphasis.go create mode 100644 format/mdext/shortstrike.go diff --git a/format/mdext/shortemphasis.go b/format/mdext/shortemphasis.go new file mode 100644 index 00000000..62190326 --- /dev/null +++ b/format/mdext/shortemphasis.go @@ -0,0 +1,96 @@ +// Copyright (c) 2024 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mdext + +import ( + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +var ShortEmphasis goldmark.Extender = &shortEmphasisExtender{} + +type shortEmphasisExtender struct{} + +func (s *shortEmphasisExtender) Extend(m goldmark.Markdown) { + m.Parser().AddOptions(parser.WithInlineParsers( + util.Prioritized(&italicsParser{}, 500), + util.Prioritized(&boldParser{}, 500), + )) +} + +type italicsDelimiterProcessor struct{} + +func (p *italicsDelimiterProcessor) IsDelimiter(b byte) bool { + return b == '_' +} + +func (p *italicsDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool { + return opener.Char == closer.Char +} + +func (p *italicsDelimiterProcessor) OnMatch(consumes int) ast.Node { + return ast.NewEmphasis(1) +} + +var defaultItalicsDelimiterProcessor = &italicsDelimiterProcessor{} + +type italicsParser struct{} + +func (s *italicsParser) Trigger() []byte { + return []byte{'_'} +} + +func (s *italicsParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { + before := block.PrecendingCharacter() + line, segment := block.PeekLine() + node := parser.ScanDelimiter(line, before, 1, defaultItalicsDelimiterProcessor) + if node == nil || node.OriginalLength > 1 || before == '_' { + return nil + } + node.Segment = segment.WithStop(segment.Start + node.OriginalLength) + block.Advance(node.OriginalLength) + pc.PushDelimiter(node) + return node +} + +type boldDelimiterProcessor struct{} + +func (p *boldDelimiterProcessor) IsDelimiter(b byte) bool { + return b == '*' +} + +func (p *boldDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool { + return opener.Char == closer.Char +} + +func (p *boldDelimiterProcessor) OnMatch(consumes int) ast.Node { + return ast.NewEmphasis(2) +} + +var defaultBoldDelimiterProcessor = &boldDelimiterProcessor{} + +type boldParser struct{} + +func (s *boldParser) Trigger() []byte { + return []byte{'*'} +} + +func (s *boldParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { + before := block.PrecendingCharacter() + line, segment := block.PeekLine() + node := parser.ScanDelimiter(line, before, 1, defaultBoldDelimiterProcessor) + if node == nil || node.OriginalLength > 1 || before == '*' { + return nil + } + node.Segment = segment.WithStop(segment.Start + node.OriginalLength) + block.Advance(node.OriginalLength) + pc.PushDelimiter(node) + return node +} diff --git a/format/mdext/shortstrike.go b/format/mdext/shortstrike.go new file mode 100644 index 00000000..00328f22 --- /dev/null +++ b/format/mdext/shortstrike.go @@ -0,0 +1,76 @@ +// Copyright (c) 2024 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mdext + +import ( + "github.com/yuin/goldmark" + gast "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/extension/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +var ShortStrike goldmark.Extender = &shortStrikeExtender{length: 1} +var LongStrike goldmark.Extender = &shortStrikeExtender{length: 2} + +type shortStrikeExtender struct { + length int +} + +func (s *shortStrikeExtender) Extend(m goldmark.Markdown) { + m.Parser().AddOptions(parser.WithInlineParsers( + util.Prioritized(&strikethroughParser{length: s.length}, 500), + )) + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(extension.NewStrikethroughHTMLRenderer(), 500), + )) +} + +type strikethroughDelimiterProcessor struct{} + +func (p *strikethroughDelimiterProcessor) IsDelimiter(b byte) bool { + return b == '~' +} + +func (p *strikethroughDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool { + return opener.Char == closer.Char +} + +func (p *strikethroughDelimiterProcessor) OnMatch(consumes int) gast.Node { + return ast.NewStrikethrough() +} + +var defaultStrikethroughDelimiterProcessor = &strikethroughDelimiterProcessor{} + +type strikethroughParser struct { + length int +} + +func (s *strikethroughParser) Trigger() []byte { + return []byte{'~'} +} + +func (s *strikethroughParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node { + before := block.PrecendingCharacter() + line, segment := block.PeekLine() + node := parser.ScanDelimiter(line, before, 1, defaultStrikethroughDelimiterProcessor) + if node == nil || node.OriginalLength != s.length || before == '~' { + return nil + } + + node.Segment = segment.WithStop(segment.Start + node.OriginalLength) + block.Advance(node.OriginalLength) + pc.PushDelimiter(node) + return node +} + +func (s *strikethroughParser) CloseBlock(parent gast.Node, pc parser.Context) { + // nothing to do +}