mirror of
https://github.com/yuin/goldmark
synced 2025-03-04 23:04:52 +00:00
Compare commits
42 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04410ff159 | ||
|
|
f7b2d24c09 | ||
|
|
ba49c5c69d | ||
|
|
c05fb087a4 | ||
|
|
b39daae79e | ||
|
|
d9c03f07f0 | ||
|
|
65dcf6cd0a | ||
|
|
ad1565131a | ||
|
|
bc993b4f59 | ||
|
|
41273a4d07 | ||
|
|
d80ac9397c | ||
|
|
15000ac6a1 | ||
|
|
14d91f957f | ||
|
|
3847ca20c6 | ||
|
|
697e44ce88 | ||
|
|
fa88006eee | ||
|
|
fe34ea5d96 | ||
|
|
fd14edc9bc | ||
|
|
e367755421 | ||
|
|
e44645afbb | ||
|
|
15ade8aace | ||
|
|
dc32f35808 | ||
|
|
25bdeb0fee | ||
|
|
a590622b15 | ||
|
|
fde4948b4d | ||
|
|
9c09ae0019 | ||
|
|
c15e394c27 | ||
|
|
e405d57be0 | ||
|
|
ce6424aa0e | ||
|
|
09afa2feba | ||
|
|
4f3074451e | ||
|
|
4675c66d3d | ||
|
|
848dc66530 | ||
|
|
b8d6d3a9b7 | ||
|
|
90c46e0829 | ||
|
|
4bade05173 | ||
|
|
2b845f2615 | ||
|
|
e3d8ed9725 | ||
|
|
697cd509b1 | ||
|
|
ff3285aa2a | ||
|
|
c2167685c1 | ||
|
|
39a50c623e |
39 changed files with 4499 additions and 3867 deletions
12
.github/workflows/test.yaml
vendored
12
.github/workflows/test.yaml
vendored
|
|
@ -5,7 +5,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.19.x, 1.20.x]
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
|
@ -16,16 +16,18 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run lints
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
if: "matrix.platform == 'ubuntu-latest'" # gofmt linter fails on Windows for CRLF problems
|
||||
- name: Run tests
|
||||
env:
|
||||
GOLDMARK_TEST_TIMEOUT_MULTIPLIER: 5
|
||||
run: go test -v ./... -covermode=count -coverprofile=coverage.out -coverpkg=./...
|
||||
- name: Install goveralls
|
||||
run: go install github.com/mattn/goveralls@latest
|
||||
- name: Send coverage
|
||||
if: "matrix.platform == 'ubuntu-latest'"
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
GO111MODULE=off go get github.com/mattn/goveralls
|
||||
$(go env GOPATH)/bin/goveralls -coverprofile=coverage.out -service=github
|
||||
run: goveralls -coverprofile=coverage.out -service=github
|
||||
|
|
|
|||
41
README.md
41
README.md
|
|
@ -2,13 +2,15 @@ goldmark
|
|||
==========================================
|
||||
|
||||
[](https://pkg.go.dev/github.com/yuin/goldmark)
|
||||
[](https://github.com/yuin/goldmark/actions?query=workflow:test)
|
||||
[](https://github.com/yuin/goldmark/actions?query=workflow:test)
|
||||
[](https://coveralls.io/github/yuin/goldmark)
|
||||
[](https://goreportcard.com/report/github.com/yuin/goldmark)
|
||||
|
||||
> A Markdown parser written in Go. Easy to extend, standards-compliant, well-structured.
|
||||
|
||||
goldmark is compliant with CommonMark 0.30.
|
||||
goldmark is compliant with CommonMark 0.31.2.
|
||||
|
||||
- [goldmark playground](https://yuin.github.io/goldmark/playground/) : Try goldmark online. This playground is built with WASM(5-10MB).
|
||||
|
||||
Motivation
|
||||
----------------------
|
||||
|
|
@ -260,7 +262,7 @@ You can override autolinking patterns via options.
|
|||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `extension.WithLinkifyAllowedProtocols` | `[][]byte` | List of allowed protocols such as `[][]byte{ []byte("http:") }` |
|
||||
| `extension.WithLinkifyAllowedProtocols` | `[][]byte \| []string` | List of allowed protocols such as `[]string{ "http:" }` |
|
||||
| `extension.WithLinkifyURLRegexp` | `*regexp.Regexp` | Regexp that defines URLs, including protocols |
|
||||
| `extension.WithLinkifyWWWRegexp` | `*regexp.Regexp` | Regexp that defines URL starting with `www.`. This pattern corresponds to [the extended www autolink](https://github.github.com/gfm/#extended-www-autolink) |
|
||||
| `extension.WithLinkifyEmailRegexp` | `*regexp.Regexp` | Regexp that defines email addresses` |
|
||||
|
|
@ -277,12 +279,12 @@ markdown := goldmark.New(
|
|||
),
|
||||
goldmark.WithExtensions(
|
||||
extension.NewLinkify(
|
||||
extension.WithLinkifyAllowedProtocols([][]byte{
|
||||
[]byte("http:"),
|
||||
[]byte("https:"),
|
||||
extension.WithLinkifyAllowedProtocols([]string{
|
||||
"http:",
|
||||
"https:",
|
||||
}),
|
||||
extension.WithLinkifyURLRegexp(
|
||||
xurls.Strict,
|
||||
xurls.Strict(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -297,13 +299,13 @@ This extension has some options:
|
|||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `extension.WithFootnoteIDPrefix` | `[]byte` | a prefix for the id attributes.|
|
||||
| `extension.WithFootnoteIDPrefix` | `[]byte \| string` | a prefix for the id attributes.|
|
||||
| `extension.WithFootnoteIDPrefixFunction` | `func(gast.Node) []byte` | a function that determines the id attribute for given Node.|
|
||||
| `extension.WithFootnoteLinkTitle` | `[]byte` | an optional title attribute for footnote links.|
|
||||
| `extension.WithFootnoteBacklinkTitle` | `[]byte` | an optional title attribute for footnote backlinks. |
|
||||
| `extension.WithFootnoteLinkClass` | `[]byte` | a class for footnote links. This defaults to `footnote-ref`. |
|
||||
| `extension.WithFootnoteBacklinkClass` | `[]byte` | a class for footnote backlinks. This defaults to `footnote-backref`. |
|
||||
| `extension.WithFootnoteBacklinkHTML` | `[]byte` | a class for footnote backlinks. This defaults to `↩︎`. |
|
||||
| `extension.WithFootnoteLinkTitle` | `[]byte \| string` | an optional title attribute for footnote links.|
|
||||
| `extension.WithFootnoteBacklinkTitle` | `[]byte \| string` | an optional title attribute for footnote backlinks. |
|
||||
| `extension.WithFootnoteLinkClass` | `[]byte \| string` | a class for footnote links. This defaults to `footnote-ref`. |
|
||||
| `extension.WithFootnoteBacklinkClass` | `[]byte \| string` | a class for footnote backlinks. This defaults to `footnote-backref`. |
|
||||
| `extension.WithFootnoteBacklinkHTML` | `[]byte \| string` | a class for footnote backlinks. This defaults to `↩︎`. |
|
||||
|
||||
Some options can have special substitutions. Occurrences of “^^” in the string will be replaced by the corresponding footnote number in the HTML output. Occurrences of “%%” will be replaced by a number for the reference (footnotes can have multiple references).
|
||||
|
||||
|
|
@ -319,7 +321,7 @@ for _, path := range files {
|
|||
markdown := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
NewFootnote(
|
||||
WithFootnoteIDPrefix([]byte(path)),
|
||||
WithFootnoteIDPrefix(path),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -379,7 +381,7 @@ This extension provides additional options for CJK users.
|
|||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `extension.WithEastAsianLineBreaks` | `...extension.EastAsianLineBreaksStyle` | Soft line breaks are rendered as a newline. Some asian users will see it as an unnecessary space. With this option, soft line breaks between east asian wide characters will be ignored. |
|
||||
| `extension.WithEastAsianLineBreaks` | `...extension.EastAsianLineBreaksStyle` | Soft line breaks are rendered as a newline. Some asian users will see it as an unnecessary space. With this option, soft line breaks between east asian wide characters will be ignored. This defaults to `EastAsianLineBreaksStyleSimple`. |
|
||||
| `extension.WithEscapedSpace` | `-` | Without spaces around an emphasis started with east asian punctuations, it is not interpreted as an emphasis(as defined in CommonMark spec). With this option, you can avoid this inconvenient behavior by putting 'not rendered' spaces around an emphasis like `太郎は\ **「こんにちわ」**\ といった`. |
|
||||
|
||||
#### Styles of Line Breaking
|
||||
|
|
@ -467,6 +469,7 @@ As you can see, goldmark's performance is on par with cmark's.
|
|||
|
||||
Extensions
|
||||
--------------------
|
||||
### List of extensions
|
||||
|
||||
- [goldmark-meta](https://github.com/yuin/goldmark-meta): A YAML metadata
|
||||
extension for the goldmark Markdown parser.
|
||||
|
|
@ -490,6 +493,14 @@ Extensions
|
|||
- [goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2): Adds support for [D2](https://d2lang.com/) diagrams.
|
||||
- [goldmark-katex](https://github.com/FurqanSoftware/goldmark-katex): Adds support for [KaTeX](https://katex.org/) math and equations.
|
||||
- [goldmark-img64](https://github.com/tenkoh/goldmark-img64): Adds support for embedding images into the document as DataURL (base64 encoded).
|
||||
- [goldmark-enclave](https://github.com/quailyquaily/goldmark-enclave): Adds support for embedding youtube/bilibili video, X's [oembed X](https://publish.x.com/), [tradingview chart](https://www.tradingview.com/widget/)'s chart, [quaily widget](https://quaily.com), [spotify embeds](https://developer.spotify.com/documentation/embeds), [dify embed](https://dify.ai/) and html audio into the document.
|
||||
- [goldmark-wiki-table](https://github.com/movsb/goldmark-wiki-table): Adds support for embedding Wiki Tables.
|
||||
- [goldmark-tgmd](https://github.com/Mad-Pixels/goldmark-tgmd): A Telegram markdown renderer that can be passed to `goldmark.WithRenderer()`.
|
||||
|
||||
### Loading extensions at runtime
|
||||
[goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) allows you to write a goldmark extension in Lua and load it at runtime without re-compilation.
|
||||
|
||||
Please refer to [goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) for details.
|
||||
|
||||
|
||||
goldmark internal(for extension developers)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ require (
|
|||
github.com/88250/lute v1.7.5
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/yuin/goldmark v1.2.1
|
||||
github.com/yuin/goldmark v0.0.0
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a
|
||||
)
|
||||
|
||||
|
|
@ -22,3 +22,4 @@ require (
|
|||
)
|
||||
|
||||
replace gopkg.in/russross/blackfriday.v2 v2.0.1 => github.com/russross/blackfriday/v2 v2.0.1
|
||||
replace github.com/yuin/goldmark v0.0.0 => ../../
|
||||
|
|
|
|||
|
|
@ -385,7 +385,8 @@ a* b c d *e*
|
|||
//- - - - - - - - -//
|
||||
x
|
||||
//- - - - - - - - -//
|
||||
<pre><code> x</code></pre>
|
||||
<pre><code> x
|
||||
</code></pre>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
26: NUL bytes must be replaced with U+FFFD
|
||||
|
|
@ -780,3 +781,65 @@ text](logo.png)
|
|||
<p><img src="logo.png" alt="alt
|
||||
text" /></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
62: Image alt with an escaped character
|
||||
//- - - - - - - - -//
|
||||

|
||||
//- - - - - - - - -//
|
||||
<p><img src="https://example.com/img.png" alt="`alt" /></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
63: Emphasis in link label
|
||||
//- - - - - - - - -//
|
||||
[*[a]*](b)
|
||||
//- - - - - - - - -//
|
||||
<p><a href="b"><em>[a]</em></a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
64: Nested list under an empty list item
|
||||
//- - - - - - - - -//
|
||||
-
|
||||
- foo
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>
|
||||
<ul>
|
||||
<li>foo</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
65: Nested fenced code block with tab
|
||||
//- - - - - - - - -//
|
||||
> ```
|
||||
> 0
|
||||
> ```
|
||||
//- - - - - - - - -//
|
||||
<blockquote>
|
||||
<pre><code> 0
|
||||
</code></pre>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
66: EOF should be rendered as a newline with an unclosed block(w/ TAB)
|
||||
//- - - - - - - - -//
|
||||
> ```
|
||||
> 0
|
||||
//- - - - - - - - -//
|
||||
<blockquote>
|
||||
<pre><code> 0
|
||||
</code></pre>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
67: EOF should be rendered as a newline with an unclosed block
|
||||
//- - - - - - - - -//
|
||||
> ```
|
||||
> 0
|
||||
//- - - - - - - - -//
|
||||
<blockquote>
|
||||
<pre><code> 0
|
||||
</code></pre>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
|
|
|||
3184
_test/spec.json
3184
_test/spec.json
File diff suppressed because it is too large
Load diff
15
ast/ast.go
15
ast/ast.go
|
|
@ -123,6 +123,12 @@ type Node interface {
|
|||
Dump(source []byte, level int)
|
||||
|
||||
// Text returns text values of this node.
|
||||
// This method is valid only for some inline nodes.
|
||||
// If this node is a block node, Text returns a text value as reasonable as possible.
|
||||
// Notice that there are no 'correct' text values for the block nodes.
|
||||
// Result for the block nodes may be different from your expectation.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. Pragraph.Lines, Text.Value).
|
||||
Text(source []byte) []byte
|
||||
|
||||
// HasBlankPreviousLines returns true if the row before this node is blank,
|
||||
|
|
@ -374,11 +380,18 @@ func (n *BaseNode) OwnerDocument() *Document {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Text implements Node.Text .
|
||||
// Text implements Node.Text .
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. Pragraph.Lines, Text.Value).
|
||||
func (n *BaseNode) Text(source []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
for c := n.firstChild; c != nil; c = c.NextSibling() {
|
||||
buf.Write(c.Text(source))
|
||||
if sb, ok := c.(interface {
|
||||
SoftLineBreak() bool
|
||||
}); ok && sb.SoftLineBreak() {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,21 +5,6 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestRemoveChildren(t *testing.T) {
|
||||
root := NewDocument()
|
||||
|
||||
node1 := NewDocument()
|
||||
|
||||
node2 := NewDocument()
|
||||
|
||||
root.AppendChild(root, node1)
|
||||
root.AppendChild(root, node2)
|
||||
|
||||
root.RemoveChildren(root)
|
||||
|
||||
t.Logf("%+v", node2.PreviousSibling())
|
||||
}
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
39
ast/block.go
39
ast/block.go
|
|
@ -130,6 +130,13 @@ func (n *TextBlock) Kind() NodeKind {
|
|||
return KindTextBlock
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. TextBlock.Lines).
|
||||
func (n *TextBlock) Text(source []byte) []byte {
|
||||
return n.Lines().Value(source)
|
||||
}
|
||||
|
||||
// NewTextBlock returns a new TextBlock node.
|
||||
func NewTextBlock() *TextBlock {
|
||||
return &TextBlock{
|
||||
|
|
@ -155,6 +162,13 @@ func (n *Paragraph) Kind() NodeKind {
|
|||
return KindParagraph
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. Paragraph.Lines).
|
||||
func (n *Paragraph) Text(source []byte) []byte {
|
||||
return n.Lines().Value(source)
|
||||
}
|
||||
|
||||
// NewParagraph returns a new Paragraph node.
|
||||
func NewParagraph() *Paragraph {
|
||||
return &Paragraph{
|
||||
|
|
@ -249,6 +263,13 @@ func (n *CodeBlock) Kind() NodeKind {
|
|||
return KindCodeBlock
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. CodeBlock.Lines).
|
||||
func (n *CodeBlock) Text(source []byte) []byte {
|
||||
return n.Lines().Value(source)
|
||||
}
|
||||
|
||||
// NewCodeBlock returns a new CodeBlock node.
|
||||
func NewCodeBlock() *CodeBlock {
|
||||
return &CodeBlock{
|
||||
|
|
@ -304,6 +325,13 @@ func (n *FencedCodeBlock) Kind() NodeKind {
|
|||
return KindFencedCodeBlock
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. FencedCodeBlock.Lines).
|
||||
func (n *FencedCodeBlock) Text(source []byte) []byte {
|
||||
return n.Lines().Value(source)
|
||||
}
|
||||
|
||||
// NewFencedCodeBlock return a new FencedCodeBlock node.
|
||||
func NewFencedCodeBlock(info *Text) *FencedCodeBlock {
|
||||
return &FencedCodeBlock{
|
||||
|
|
@ -498,6 +526,17 @@ func (n *HTMLBlock) Kind() NodeKind {
|
|||
return KindHTMLBlock
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. HTMLBlock.Lines).
|
||||
func (n *HTMLBlock) Text(source []byte) []byte {
|
||||
ret := n.Lines().Value(source)
|
||||
if n.HasClosure() {
|
||||
ret = append(ret, n.ClosureLine.Value(source)...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// NewHTMLBlock returns a new HTMLBlock node.
|
||||
func NewHTMLBlock(typ HTMLBlockType) *HTMLBlock {
|
||||
return &HTMLBlock{
|
||||
|
|
|
|||
|
|
@ -143,17 +143,25 @@ func (n *Text) Merge(node Node, source []byte) bool {
|
|||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. Text.Value).
|
||||
func (n *Text) Text(source []byte) []byte {
|
||||
return n.Segment.Value(source)
|
||||
}
|
||||
|
||||
// Value returns a value of this node.
|
||||
// SoftLineBreaks are not included in the returned value.
|
||||
func (n *Text) Value(source []byte) []byte {
|
||||
return n.Segment.Value(source)
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *Text) Dump(source []byte, level int) {
|
||||
fs := textFlagsString(n.flags)
|
||||
if len(fs) != 0 {
|
||||
fs = "(" + fs + ")"
|
||||
}
|
||||
fmt.Printf("%sText%s: \"%s\"\n", strings.Repeat(" ", level), fs, strings.TrimRight(string(n.Text(source)), "\n"))
|
||||
fmt.Printf("%sText%s: \"%s\"\n", strings.Repeat(" ", level), fs, strings.TrimRight(string(n.Value(source)), "\n"))
|
||||
}
|
||||
|
||||
// KindText is a NodeKind of the Text node.
|
||||
|
|
@ -258,6 +266,8 @@ func (n *String) SetCode(v bool) {
|
|||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. String.Value).
|
||||
func (n *String) Text(source []byte) []byte {
|
||||
return n.Value
|
||||
}
|
||||
|
|
@ -492,15 +502,22 @@ func (n *AutoLink) URL(source []byte) []byte {
|
|||
ret := make([]byte, 0, len(n.Protocol)+s.Len()+3)
|
||||
ret = append(ret, n.Protocol...)
|
||||
ret = append(ret, ':', '/', '/')
|
||||
ret = append(ret, n.value.Text(source)...)
|
||||
ret = append(ret, n.value.Value(source)...)
|
||||
return ret
|
||||
}
|
||||
return n.value.Text(source)
|
||||
return n.value.Value(source)
|
||||
}
|
||||
|
||||
// Label returns a label of this node.
|
||||
func (n *AutoLink) Label(source []byte) []byte {
|
||||
return n.value.Text(source)
|
||||
return n.value.Value(source)
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. AutoLink.Label).
|
||||
func (n *AutoLink) Text(source []byte) []byte {
|
||||
return n.value.Value(source)
|
||||
}
|
||||
|
||||
// NewAutoLink returns a new AutoLink node.
|
||||
|
|
@ -541,6 +558,13 @@ func (n *RawHTML) Kind() NodeKind {
|
|||
return KindRawHTML
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. RawHTML.Segments).
|
||||
func (n *RawHTML) Text(source []byte) []byte {
|
||||
return n.Segments.Value(source)
|
||||
}
|
||||
|
||||
// NewRawHTML returns a new RawHTML node.
|
||||
func NewRawHTML() *RawHTML {
|
||||
return &RawHTML{
|
||||
|
|
|
|||
204
ast_test.go
Normal file
204
ast_test.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package goldmark_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
. "github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
"github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
func TestASTBlockNodeText(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Name string
|
||||
Source string
|
||||
T1 string
|
||||
T2 string
|
||||
C bool
|
||||
}{
|
||||
{
|
||||
Name: "AtxHeading",
|
||||
Source: `# l1
|
||||
|
||||
a
|
||||
|
||||
# l2`,
|
||||
T1: `l1`,
|
||||
T2: `l2`,
|
||||
},
|
||||
{
|
||||
Name: "SetextHeading",
|
||||
Source: `l1
|
||||
l2
|
||||
===============
|
||||
|
||||
a
|
||||
|
||||
l3
|
||||
l4
|
||||
==============`,
|
||||
T1: `l1
|
||||
l2`,
|
||||
T2: `l3
|
||||
l4`,
|
||||
},
|
||||
{
|
||||
Name: "CodeBlock",
|
||||
Source: ` l1
|
||||
l2
|
||||
|
||||
a
|
||||
|
||||
l3
|
||||
l4`,
|
||||
T1: `l1
|
||||
l2
|
||||
`,
|
||||
T2: `l3
|
||||
l4
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "FencedCodeBlock",
|
||||
Source: "```" + `
|
||||
l1
|
||||
l2
|
||||
` + "```" + `
|
||||
|
||||
a
|
||||
|
||||
` + "```" + `
|
||||
l3
|
||||
l4`,
|
||||
T1: `l1
|
||||
l2
|
||||
`,
|
||||
T2: `l3
|
||||
l4
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "Blockquote",
|
||||
Source: `> l1
|
||||
> l2
|
||||
|
||||
a
|
||||
|
||||
> l3
|
||||
> l4`,
|
||||
T1: `l1
|
||||
l2`,
|
||||
T2: `l3
|
||||
l4`,
|
||||
},
|
||||
{
|
||||
Name: "List",
|
||||
Source: `- l1
|
||||
l2
|
||||
|
||||
a
|
||||
|
||||
- l3
|
||||
l4`,
|
||||
T1: `l1
|
||||
l2`,
|
||||
T2: `l3
|
||||
l4`,
|
||||
C: true,
|
||||
},
|
||||
{
|
||||
Name: "HTMLBlock",
|
||||
Source: `<div>
|
||||
l1
|
||||
l2
|
||||
</div>
|
||||
|
||||
a
|
||||
|
||||
<div>
|
||||
l3
|
||||
l4`,
|
||||
T1: `<div>
|
||||
l1
|
||||
l2
|
||||
</div>
|
||||
`,
|
||||
T2: `<div>
|
||||
l3
|
||||
l4`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
s := []byte(cs.Source)
|
||||
md := New()
|
||||
n := md.Parser().Parse(text.NewReader(s))
|
||||
c1 := n.FirstChild()
|
||||
c2 := c1.NextSibling().NextSibling()
|
||||
if cs.C {
|
||||
c1 = c1.FirstChild()
|
||||
c2 = c2.FirstChild()
|
||||
}
|
||||
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s unmatch: %s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
if !bytes.Equal(c2.Text(s), []byte(cs.T2)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s(EOF) unmatch: %s", cs.Name, testutil.DiffPretty(c2.Text(s), []byte(cs.T2))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestASTInlineNodeText(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Name string
|
||||
Source string
|
||||
T1 string
|
||||
}{
|
||||
{
|
||||
Name: "CodeSpan",
|
||||
Source: "`c1`",
|
||||
T1: `c1`,
|
||||
},
|
||||
{
|
||||
Name: "Emphasis",
|
||||
Source: `*c1 **c2***`,
|
||||
T1: `c1 c2`,
|
||||
},
|
||||
{
|
||||
Name: "Link",
|
||||
Source: `[label](url)`,
|
||||
T1: `label`,
|
||||
},
|
||||
{
|
||||
Name: "AutoLink",
|
||||
Source: `<http://url>`,
|
||||
T1: `http://url`,
|
||||
},
|
||||
{
|
||||
Name: "RawHTML",
|
||||
Source: `<span>c1</span>`,
|
||||
T1: `<span>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
s := []byte(cs.Source)
|
||||
md := New()
|
||||
n := md.Parser().Parse(text.NewReader(s))
|
||||
c1 := n.FirstChild().FirstChild()
|
||||
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
|
||||
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package goldmark_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/yuin/goldmark"
|
||||
|
|
@ -20,7 +20,7 @@ type commonmarkSpecTestCase struct {
|
|||
}
|
||||
|
||||
func TestSpec(t *testing.T) {
|
||||
bs, err := ioutil.ReadFile("_test/spec.json")
|
||||
bs, err := os.ReadFile("_test/spec.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,8 @@ on two lines.</p>
|
|||
//- - - - - - - - -//
|
||||
<dl>
|
||||
<dt>0</dt>
|
||||
<dd><pre><code> 0</code></pre>
|
||||
<dd><pre><code> 0
|
||||
</code></pre>
|
||||
</dd>
|
||||
</dl>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
<p><del>Hi</del> Hello, world!</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
This ~~has a
|
||||
|
|
@ -16,3 +14,26 @@ new paragraph~~.
|
|||
<p>This ~~has a</p>
|
||||
<p>new paragraph~~.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
~Hi~ Hello, world!
|
||||
//- - - - - - - - -//
|
||||
<p><del>Hi</del> Hello, world!</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
4: Three or more tildes do not create a strikethrough
|
||||
//- - - - - - - - -//
|
||||
This will ~~~not~~~ strike.
|
||||
//- - - - - - - - -//
|
||||
<p>This will ~~~not~~~ strike.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
5: Leading three or more tildes do not create a strikethrough, create a code block
|
||||
//- - - - - - - - -//
|
||||
~~~Hi~~~ Hello, world!
|
||||
//- - - - - - - - -//
|
||||
<pre><code class="language-Hi~~~"></code></pre>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
|
|
|||
123
extension/ast_test.go
Normal file
123
extension/ast_test.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
"github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
func TestASTBlockNodeText(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Name string
|
||||
Source string
|
||||
T1 string
|
||||
T2 string
|
||||
C bool
|
||||
}{
|
||||
{
|
||||
Name: "DefinitionList",
|
||||
Source: `c1
|
||||
: c2
|
||||
c3
|
||||
|
||||
a
|
||||
|
||||
c4
|
||||
: c5
|
||||
c6`,
|
||||
T1: `c1c2
|
||||
c3`,
|
||||
T2: `c4c5
|
||||
c6`,
|
||||
},
|
||||
{
|
||||
Name: "Table",
|
||||
Source: `| h1 | h2 |
|
||||
| -- | -- |
|
||||
| c1 | c2 |
|
||||
|
||||
a
|
||||
|
||||
|
||||
| h3 | h4 |
|
||||
| -- | -- |
|
||||
| c3 | c4 |`,
|
||||
|
||||
T1: `h1h2c1c2`,
|
||||
T2: `h3h4c3c4`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
s := []byte(cs.Source)
|
||||
md := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
DefinitionList,
|
||||
Table,
|
||||
),
|
||||
)
|
||||
n := md.Parser().Parse(text.NewReader(s))
|
||||
c1 := n.FirstChild()
|
||||
c2 := c1.NextSibling().NextSibling()
|
||||
if cs.C {
|
||||
c1 = c1.FirstChild()
|
||||
c2 = c2.FirstChild()
|
||||
}
|
||||
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
if !bytes.Equal(c2.Text(s), []byte(cs.T2)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s(EOF) unmatch: %s", cs.Name, testutil.DiffPretty(c2.Text(s), []byte(cs.T2))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestASTInlineNodeText(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Name string
|
||||
Source string
|
||||
T1 string
|
||||
}{
|
||||
{
|
||||
Name: "Strikethrough",
|
||||
Source: `~c1 *c2*~`,
|
||||
T1: `c1 c2`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
s := []byte(cs.Source)
|
||||
md := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Strikethrough,
|
||||
),
|
||||
)
|
||||
n := md.Parser().Parse(text.NewReader(s))
|
||||
c1 := n.FirstChild().FirstChild()
|
||||
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -382,8 +382,8 @@ func (o *withFootnoteIDPrefix) SetFootnoteOption(c *FootnoteConfig) {
|
|||
}
|
||||
|
||||
// WithFootnoteIDPrefix is a functional option that is a prefix for the id attributes generated by footnotes.
|
||||
func WithFootnoteIDPrefix(a []byte) FootnoteOption {
|
||||
return &withFootnoteIDPrefix{a}
|
||||
func WithFootnoteIDPrefix[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteIDPrefix{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction"
|
||||
|
|
@ -420,8 +420,8 @@ func (o *withFootnoteLinkTitle) SetFootnoteOption(c *FootnoteConfig) {
|
|||
}
|
||||
|
||||
// WithFootnoteLinkTitle is a functional option that is an optional title attribute for footnote links.
|
||||
func WithFootnoteLinkTitle(a []byte) FootnoteOption {
|
||||
return &withFootnoteLinkTitle{a}
|
||||
func WithFootnoteLinkTitle[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteLinkTitle{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle"
|
||||
|
|
@ -439,8 +439,8 @@ func (o *withFootnoteBacklinkTitle) SetFootnoteOption(c *FootnoteConfig) {
|
|||
}
|
||||
|
||||
// WithFootnoteBacklinkTitle is a functional option that is an optional title attribute for footnote backlinks.
|
||||
func WithFootnoteBacklinkTitle(a []byte) FootnoteOption {
|
||||
return &withFootnoteBacklinkTitle{a}
|
||||
func WithFootnoteBacklinkTitle[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkTitle{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass"
|
||||
|
|
@ -458,8 +458,8 @@ func (o *withFootnoteLinkClass) SetFootnoteOption(c *FootnoteConfig) {
|
|||
}
|
||||
|
||||
// WithFootnoteLinkClass is a functional option that is a class for footnote links.
|
||||
func WithFootnoteLinkClass(a []byte) FootnoteOption {
|
||||
return &withFootnoteLinkClass{a}
|
||||
func WithFootnoteLinkClass[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteLinkClass{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass"
|
||||
|
|
@ -477,8 +477,8 @@ func (o *withFootnoteBacklinkClass) SetFootnoteOption(c *FootnoteConfig) {
|
|||
}
|
||||
|
||||
// WithFootnoteBacklinkClass is a functional option that is a class for footnote backlinks.
|
||||
func WithFootnoteBacklinkClass(a []byte) FootnoteOption {
|
||||
return &withFootnoteBacklinkClass{a}
|
||||
func WithFootnoteBacklinkClass[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkClass{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML"
|
||||
|
|
@ -496,8 +496,8 @@ func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) {
|
|||
}
|
||||
|
||||
// WithFootnoteBacklinkHTML is an HTML content for footnote backlinks.
|
||||
func WithFootnoteBacklinkHTML(a []byte) FootnoteOption {
|
||||
return &withFootnoteBacklinkHTML{a}
|
||||
func WithFootnoteBacklinkHTML[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkHTML{[]byte(a)}
|
||||
}
|
||||
|
||||
// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
|
|
|
|||
|
|
@ -38,12 +38,12 @@ func TestFootnoteOptions(t *testing.T) {
|
|||
),
|
||||
goldmark.WithExtensions(
|
||||
NewFootnote(
|
||||
WithFootnoteIDPrefix([]byte("article12-")),
|
||||
WithFootnoteLinkClass([]byte("link-class")),
|
||||
WithFootnoteBacklinkClass([]byte("backlink-class")),
|
||||
WithFootnoteLinkTitle([]byte("link-title-%%-^^")),
|
||||
WithFootnoteBacklinkTitle([]byte("backlink-title")),
|
||||
WithFootnoteBacklinkHTML([]byte("^")),
|
||||
WithFootnoteIDPrefix("article12-"),
|
||||
WithFootnoteLinkClass("link-class"),
|
||||
WithFootnoteBacklinkClass("backlink-class"),
|
||||
WithFootnoteLinkTitle("link-title-%%-^^"),
|
||||
WithFootnoteBacklinkTitle("backlink-title"),
|
||||
WithFootnoteBacklinkHTML("^"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,10 +66,12 @@ func (o *withLinkifyAllowedProtocols) SetLinkifyOption(p *LinkifyConfig) {
|
|||
// WithLinkifyAllowedProtocols is a functional option that specify allowed
|
||||
// protocols in autolinks. Each protocol must end with ':' like
|
||||
// 'http:' .
|
||||
func WithLinkifyAllowedProtocols(value [][]byte) LinkifyOption {
|
||||
return &withLinkifyAllowedProtocols{
|
||||
value: value,
|
||||
func WithLinkifyAllowedProtocols[T []byte | string](value []T) LinkifyOption {
|
||||
opt := &withLinkifyAllowedProtocols{}
|
||||
for _, v := range value {
|
||||
opt.value = append(opt.value, []byte(v))
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
type withLinkifyURLRegexp struct {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ func TestLinkifyWithAllowedProtocols(t *testing.T) {
|
|||
),
|
||||
goldmark.WithExtensions(
|
||||
NewLinkify(
|
||||
WithLinkifyAllowedProtocols([][]byte{
|
||||
[]byte("ssh:"),
|
||||
WithLinkifyAllowedProtocols([]string{
|
||||
"ssh:",
|
||||
}),
|
||||
WithLinkifyURLRegexp(
|
||||
regexp.MustCompile(`\w+://[^\s]+`),
|
||||
|
|
|
|||
|
|
@ -46,10 +46,11 @@ func (s *strikethroughParser) Trigger() []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, 2, defaultStrikethroughDelimiterProcessor)
|
||||
if node == nil {
|
||||
node := parser.ScanDelimiter(line, before, 1, defaultStrikethroughDelimiterProcessor)
|
||||
if node == nil || node.OriginalLength > 2 || before == '~' {
|
||||
return nil
|
||||
}
|
||||
|
||||
node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
|
||||
block.Advance(node.OriginalLength)
|
||||
pc.PushDelimiter(node)
|
||||
|
|
|
|||
|
|
@ -184,11 +184,11 @@ func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.
|
|||
func (b *tableParagraphTransformer) parseRow(segment text.Segment,
|
||||
alignments []ast.Alignment, isHeader bool, reader text.Reader, pc parser.Context) *ast.TableRow {
|
||||
source := reader.Source()
|
||||
segment = segment.TrimLeftSpace(source)
|
||||
segment = segment.TrimRightSpace(source)
|
||||
line := segment.Value(source)
|
||||
pos := 0
|
||||
pos += util.TrimLeftSpaceLength(line)
|
||||
limit := len(line)
|
||||
limit -= util.TrimRightSpaceLength(line)
|
||||
row := ast.NewTableRow(alignments)
|
||||
if len(line) > 0 && line[pos] == '|' {
|
||||
pos++
|
||||
|
|
@ -492,7 +492,7 @@ func (r *TableHTMLRenderer) renderTableCell(
|
|||
tag = "th"
|
||||
}
|
||||
if entering {
|
||||
fmt.Fprintf(w, "<%s", tag)
|
||||
_, _ = fmt.Fprintf(w, "<%s", tag)
|
||||
if n.Alignment != ast.AlignNone {
|
||||
amethod := r.TableConfig.TableCellAlignMethod
|
||||
if amethod == TableCellAlignDefault {
|
||||
|
|
@ -505,7 +505,7 @@ func (r *TableHTMLRenderer) renderTableCell(
|
|||
switch amethod {
|
||||
case TableCellAlignAttribute:
|
||||
if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden
|
||||
fmt.Fprintf(w, ` align="%s"`, n.Alignment.String())
|
||||
_, _ = fmt.Fprintf(w, ` align="%s"`, n.Alignment.String())
|
||||
}
|
||||
case TableCellAlignStyle:
|
||||
v, ok := n.AttributeString("style")
|
||||
|
|
@ -528,7 +528,7 @@ func (r *TableHTMLRenderer) renderTableCell(
|
|||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
fmt.Fprintf(w, "</%s>\n", tag)
|
||||
_, _ = fmt.Fprintf(w, "</%s>\n", tag)
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,3 +355,40 @@ bar | baz
|
|||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTableFuzzedPanics(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "This should not panic",
|
||||
Markdown: "* 0\n-|\n\t0",
|
||||
Expected: `<ul>
|
||||
<li>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>0</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ul>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,10 +115,10 @@ func (o *withTypographicSubstitutions) SetTypographerOption(p *TypographerConfig
|
|||
|
||||
// WithTypographicSubstitutions is a functional otpion that specify replacement text
|
||||
// for punctuations.
|
||||
func WithTypographicSubstitutions(values map[TypographicPunctuation][]byte) TypographerOption {
|
||||
func WithTypographicSubstitutions[T []byte | string](values map[TypographicPunctuation]T) TypographerOption {
|
||||
replacements := newDefaultSubstitutions()
|
||||
for k, v := range values {
|
||||
replacements[k] = v
|
||||
replacements[k] = []byte(v)
|
||||
}
|
||||
|
||||
return &withTypographicSubstitutions{replacements}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package fuzz
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
|
|
@ -42,7 +42,7 @@ func fuzz(f *testing.F) {
|
|||
}
|
||||
|
||||
func FuzzDefault(f *testing.F) {
|
||||
bs, err := ioutil.ReadFile("../_test/spec.json")
|
||||
bs, err := os.ReadFile("../_test/spec.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -1,3 +1,3 @@
|
|||
module github.com/yuin/goldmark
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
package goldmark
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"io"
|
||||
)
|
||||
|
||||
// DefaultParser returns a new Parser that is configured by default values.
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ func (b *blockquoteParser) process(reader text.Reader) bool {
|
|||
reader.Advance(pos)
|
||||
return true
|
||||
}
|
||||
if line[pos] == ' ' || line[pos] == '\t' {
|
||||
pos++
|
||||
}
|
||||
reader.Advance(pos)
|
||||
if line[pos-1] == '\t' {
|
||||
reader.SetPadding(2)
|
||||
if line[pos] == ' ' || line[pos] == '\t' {
|
||||
padding := 0
|
||||
if line[pos] == '\t' {
|
||||
padding = util.TabWidth(reader.LineOffset()) - 1
|
||||
}
|
||||
reader.AdvanceAndSetPadding(1, padding)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ func (b *codeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context)
|
|||
if segment.Padding != 0 {
|
||||
preserveLeadingTabInCodeBlock(&segment, reader, 0)
|
||||
}
|
||||
segment.ForceNewline = true
|
||||
node.Lines().Append(segment)
|
||||
reader.Advance(segment.Len() - 1)
|
||||
return node, NoChildren
|
||||
|
|
@ -59,6 +60,7 @@ func (b *codeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context
|
|||
preserveLeadingTabInCodeBlock(&segment, reader, 0)
|
||||
}
|
||||
|
||||
segment.ForceNewline = true
|
||||
node.Lines().Append(segment)
|
||||
reader.Advance(segment.Len() - 1)
|
||||
return Continue | NoChildren
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc C
|
|||
if padding != 0 {
|
||||
preserveLeadingTabInCodeBlock(&seg, reader, fdata.indent)
|
||||
}
|
||||
seg.ForceNewline = true // EOF as newline
|
||||
node.Lines().Append(seg)
|
||||
reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding)
|
||||
return Continue | NoChildren
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ var allowedBlockTags = map[string]bool{
|
|||
"option": true,
|
||||
"p": true,
|
||||
"param": true,
|
||||
"search": true,
|
||||
"section": true,
|
||||
"source": true,
|
||||
"summary": true,
|
||||
"table": true,
|
||||
"tbody": true,
|
||||
|
|
|
|||
|
|
@ -126,13 +126,13 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
if line[0] == '!' {
|
||||
if len(line) > 1 && line[1] == '[' {
|
||||
block.Advance(1)
|
||||
pc.Set(linkBottom, pc.LastDelimiter())
|
||||
pushLinkBottom(pc)
|
||||
return processLinkLabelOpen(block, segment.Start+1, true, pc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if line[0] == '[' {
|
||||
pc.Set(linkBottom, pc.LastDelimiter())
|
||||
pushLinkBottom(pc)
|
||||
return processLinkLabelOpen(block, segment.Start, false, pc)
|
||||
}
|
||||
|
||||
|
|
@ -143,6 +143,7 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
}
|
||||
last := tlist.(*linkLabelState).Last
|
||||
if last == nil {
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
block.Advance(1)
|
||||
|
|
@ -151,11 +152,13 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
// > A link label can have at most 999 characters inside the square brackets.
|
||||
if linkLabelStateLength(tlist.(*linkLabelState)) > 998 {
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !last.IsImage && s.containsLink(last) { // a link in a link text is not allowed
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +172,7 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
link, hasValue = s.parseReferenceLink(parent, last, block, pc)
|
||||
if link == nil && hasValue {
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -182,12 +186,14 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
// > A link label can have at most 999 characters inside the square brackets.
|
||||
if len(maybeReference) > 999 {
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
|
||||
ref, ok := pc.Reference(util.ToLinkReference(maybeReference))
|
||||
if !ok {
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
link = ast.NewLink()
|
||||
|
|
@ -230,11 +236,7 @@ func processLinkLabelOpen(block text.Reader, pos int, isImage bool, pc Context)
|
|||
}
|
||||
|
||||
func (s *linkParser) processLinkLabel(parent ast.Node, link *ast.Link, last *linkLabelState, pc Context) {
|
||||
var bottom ast.Node
|
||||
if v := pc.Get(linkBottom); v != nil {
|
||||
bottom = v.(ast.Node)
|
||||
}
|
||||
pc.Set(linkBottom, nil)
|
||||
bottom := popLinkBottom(pc)
|
||||
ProcessDelimiters(bottom, pc)
|
||||
for c := last.NextSibling(); c != nil; {
|
||||
next := c.NextSibling()
|
||||
|
|
@ -395,6 +397,43 @@ func parseLinkTitle(block text.Reader) ([]byte, bool) {
|
|||
return nil, false
|
||||
}
|
||||
|
||||
func pushLinkBottom(pc Context) {
|
||||
bottoms := pc.Get(linkBottom)
|
||||
b := pc.LastDelimiter()
|
||||
if bottoms == nil {
|
||||
pc.Set(linkBottom, b)
|
||||
return
|
||||
}
|
||||
if s, ok := bottoms.([]ast.Node); ok {
|
||||
pc.Set(linkBottom, append(s, b))
|
||||
return
|
||||
}
|
||||
pc.Set(linkBottom, []ast.Node{bottoms.(ast.Node), b})
|
||||
}
|
||||
|
||||
func popLinkBottom(pc Context) ast.Node {
|
||||
bottoms := pc.Get(linkBottom)
|
||||
if bottoms == nil {
|
||||
return nil
|
||||
}
|
||||
if v, ok := bottoms.(ast.Node); ok {
|
||||
pc.Set(linkBottom, nil)
|
||||
return v
|
||||
}
|
||||
s := bottoms.([]ast.Node)
|
||||
v := s[len(s)-1]
|
||||
n := s[0 : len(s)-1]
|
||||
switch len(n) {
|
||||
case 0:
|
||||
pc.Set(linkBottom, nil)
|
||||
case 1:
|
||||
pc.Set(linkBottom, n[0])
|
||||
default:
|
||||
pc.Set(linkBottom, s[0:len(s)-1])
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (s *linkParser) CloseBlock(parent ast.Node, block text.Reader, pc Context) {
|
||||
pc.Set(linkBottom, nil)
|
||||
tlist := pc.Get(linkLabelStateKey)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ func (b *listItemParser) Continue(node ast.Node, reader text.Reader, pc Context)
|
|||
}
|
||||
|
||||
offset := lastOffset(node.Parent())
|
||||
isEmpty := node.ChildCount() == 0
|
||||
isEmpty := node.ChildCount() == 0 && pc.Get(emptyListItemWithBlankLines) != nil
|
||||
indent, _ := util.IndentWidth(line, reader.LineOffset())
|
||||
if (isEmpty || indent < offset) && indent < 4 {
|
||||
_, typ := matchesListItem(line, true)
|
||||
|
|
|
|||
|
|
@ -882,6 +882,7 @@ func (p *parser) Parse(reader text.Reader, opts ...ParseOption) ast.Node {
|
|||
for _, at := range p.astTransformers {
|
||||
at.Transform(root, reader, pc)
|
||||
}
|
||||
|
||||
// root.Dump(reader.Source(), 0)
|
||||
return root
|
||||
}
|
||||
|
|
@ -1256,4 +1257,5 @@ func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context)
|
|||
for _, ip := range p.closeBlockers {
|
||||
ip.CloseBlock(parent, block, pc)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,47 +58,38 @@ var closeProcessingInstruction = []byte("?>")
|
|||
var openCDATA = []byte("<![CDATA[")
|
||||
var closeCDATA = []byte("]]>")
|
||||
var closeDecl = []byte(">")
|
||||
var emptyComment = []byte("<!---->")
|
||||
var invalidComment1 = []byte("<!-->")
|
||||
var invalidComment2 = []byte("<!--->")
|
||||
var emptyComment1 = []byte("<!-->")
|
||||
var emptyComment2 = []byte("<!--->")
|
||||
var openComment = []byte("<!--")
|
||||
var closeComment = []byte("-->")
|
||||
var doubleHyphen = []byte("--")
|
||||
|
||||
func (s *rawHTMLParser) parseComment(block text.Reader, pc Context) ast.Node {
|
||||
savedLine, savedSegment := block.Position()
|
||||
node := ast.NewRawHTML()
|
||||
line, segment := block.PeekLine()
|
||||
if bytes.HasPrefix(line, emptyComment) {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment)))
|
||||
block.Advance(len(emptyComment))
|
||||
if bytes.HasPrefix(line, emptyComment1) {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment1)))
|
||||
block.Advance(len(emptyComment1))
|
||||
return node
|
||||
}
|
||||
if bytes.HasPrefix(line, invalidComment1) || bytes.HasPrefix(line, invalidComment2) {
|
||||
return nil
|
||||
if bytes.HasPrefix(line, emptyComment2) {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment2)))
|
||||
block.Advance(len(emptyComment2))
|
||||
return node
|
||||
}
|
||||
offset := len(openComment)
|
||||
line = line[offset:]
|
||||
for {
|
||||
hindex := bytes.Index(line, doubleHyphen)
|
||||
if hindex > -1 {
|
||||
hindex += offset
|
||||
}
|
||||
index := bytes.Index(line, closeComment) + offset
|
||||
if index > -1 && hindex == index {
|
||||
if index == 0 || len(line) < 2 || line[index-offset-1] != '-' {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + index + len(closeComment)))
|
||||
block.Advance(index + len(closeComment))
|
||||
return node
|
||||
}
|
||||
}
|
||||
if hindex > 0 {
|
||||
break
|
||||
index := bytes.Index(line, closeComment)
|
||||
if index > -1 {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + offset + index + len(closeComment)))
|
||||
block.Advance(offset + index + len(closeComment))
|
||||
return node
|
||||
}
|
||||
offset = 0
|
||||
node.Segments.Append(segment)
|
||||
block.AdvanceLine()
|
||||
line, segment = block.PeekLine()
|
||||
offset = 0
|
||||
if line == nil {
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -445,7 +445,7 @@ func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, en
|
|||
_ = w.WriteByte('<')
|
||||
_, _ = w.WriteString(tag)
|
||||
if n.IsOrdered() && n.Start != 1 {
|
||||
fmt.Fprintf(w, " start=\"%d\"", n.Start)
|
||||
_, _ = fmt.Fprintf(w, " start=\"%d\"", n.Start)
|
||||
}
|
||||
if n.Attributes() != nil {
|
||||
RenderAttributes(w, n, ListAttributeFilter)
|
||||
|
|
@ -680,7 +680,7 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, e
|
|||
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||
}
|
||||
_, _ = w.WriteString(`" alt="`)
|
||||
_, _ = w.Write(nodeToHTMLText(n, source))
|
||||
r.renderTexts(w, source, n)
|
||||
_ = w.WriteByte('"')
|
||||
if n.Title != nil {
|
||||
_, _ = w.WriteString(` title="`)
|
||||
|
|
@ -737,7 +737,7 @@ func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, en
|
|||
if r.EastAsianLineBreaks != EastAsianLineBreaksNone && len(value) != 0 {
|
||||
sibling := node.NextSibling()
|
||||
if sibling != nil && sibling.Kind() == ast.KindText {
|
||||
if siblingText := sibling.(*ast.Text).Text(source); len(siblingText) != 0 {
|
||||
if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
|
||||
thisLastRune := util.ToRune(value, len(value)-1)
|
||||
siblingFirstRune, _ := utf8.DecodeRune(siblingText)
|
||||
if r.EastAsianLineBreaks.softLineBreak(thisLastRune, siblingFirstRune) {
|
||||
|
|
@ -770,6 +770,18 @@ func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node,
|
|||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTexts(w util.BufWriter, source []byte, n ast.Node) {
|
||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
if s, ok := c.(*ast.String); ok {
|
||||
_, _ = r.renderString(w, source, s, true)
|
||||
} else if t, ok := c.(*ast.Text); ok {
|
||||
_, _ = r.renderText(w, source, t, true)
|
||||
} else {
|
||||
r.renderTexts(w, source, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dataPrefix = []byte("data-")
|
||||
|
||||
// RenderAttributes renders given node's attributes.
|
||||
|
|
@ -786,7 +798,14 @@ func RenderAttributes(w util.BufWriter, node ast.Node, filter util.BytesFilter)
|
|||
_, _ = w.Write(attr.Name)
|
||||
_, _ = w.WriteString(`="`)
|
||||
// TODO: convert numeric values to strings
|
||||
_, _ = w.Write(util.EscapeHTML(attr.Value.([]byte)))
|
||||
var value []byte
|
||||
switch typed := attr.Value.(type) {
|
||||
case []byte:
|
||||
value = typed
|
||||
case string:
|
||||
value = util.StringToReadOnlyBytes(typed)
|
||||
}
|
||||
_, _ = w.Write(util.EscapeHTML(value))
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
}
|
||||
|
|
@ -1000,20 +1019,3 @@ func IsDangerousURL(url []byte) bool {
|
|||
return hasPrefix(url, bJs) || hasPrefix(url, bVb) ||
|
||||
hasPrefix(url, bFile) || hasPrefix(url, bData)
|
||||
}
|
||||
|
||||
func nodeToHTMLText(n ast.Node, source []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
if s, ok := c.(*ast.String); ok && s.IsCode() {
|
||||
buf.Write(s.Text(source))
|
||||
} else if !c.HasChildren() {
|
||||
buf.Write(util.EscapeHTML(c.Text(source)))
|
||||
if t, ok := c.(*ast.Text); ok && t.SoftLineBreak() {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
} else {
|
||||
buf.Write(nodeToHTMLText(c, source))
|
||||
}
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package text
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
|
|
@ -18,6 +19,20 @@ type Segment struct {
|
|||
|
||||
// Padding is a padding length of the segment.
|
||||
Padding int
|
||||
|
||||
// ForceNewline is true if the segment should be ended with a newline.
|
||||
// Some elements(i.e. CodeBlock, FencedCodeBlock) does not trim trailing
|
||||
// newlines. Spec defines that EOF is treated as a newline, so we need to
|
||||
// add a newline to the end of the segment if it is not empty.
|
||||
//
|
||||
// i.e.:
|
||||
//
|
||||
// ```go
|
||||
// const test = "test"
|
||||
//
|
||||
// This code does not close the code block and ends with EOF. In this case,
|
||||
// we need to add a newline to the end of the last line like `const test = "test"\n`.
|
||||
ForceNewline bool
|
||||
}
|
||||
|
||||
// NewSegment return a new Segment.
|
||||
|
|
@ -40,12 +55,18 @@ func NewSegmentPadding(start, stop, n int) Segment {
|
|||
|
||||
// Value returns a value of the segment.
|
||||
func (t *Segment) Value(buffer []byte) []byte {
|
||||
var result []byte
|
||||
if t.Padding == 0 {
|
||||
return buffer[t.Start:t.Stop]
|
||||
result = buffer[t.Start:t.Stop]
|
||||
} else {
|
||||
result = make([]byte, 0, t.Padding+t.Stop-t.Start+1)
|
||||
result = append(result, bytes.Repeat(space, t.Padding)...)
|
||||
result = append(result, buffer[t.Start:t.Stop]...)
|
||||
}
|
||||
result := make([]byte, 0, t.Padding+t.Stop-t.Start+1)
|
||||
result = append(result, bytes.Repeat(space, t.Padding)...)
|
||||
return append(result, buffer[t.Start:t.Stop]...)
|
||||
if t.ForceNewline && len(result) > 0 && result[len(result)-1] != '\n' {
|
||||
result = append(result, '\n')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Len returns a length of the segment.
|
||||
|
|
@ -207,3 +228,12 @@ func (s *Segments) Unshift(v Segment) {
|
|||
s.values = append(s.values[0:1], s.values[0:]...)
|
||||
s.values[0] = v
|
||||
}
|
||||
|
||||
// Value returns a string value of the collection.
|
||||
func (s *Segments) Value(buffer []byte) []byte {
|
||||
var result []byte
|
||||
for _, v := range s.values {
|
||||
result = append(result, v.Value(buffer)...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -166,7 +166,13 @@ func IndentPositionPadding(bs []byte, currentPos, paddingv, width int) (pos, pad
|
|||
w := 0
|
||||
i := 0
|
||||
l := len(bs)
|
||||
p := paddingv
|
||||
for ; i < l; i++ {
|
||||
if p > 0 {
|
||||
p--
|
||||
w++
|
||||
continue
|
||||
}
|
||||
if bs[i] == '\t' && w < width {
|
||||
w += TabWidth(currentPos + w)
|
||||
} else if bs[i] == ' ' && w < width {
|
||||
|
|
@ -808,7 +814,7 @@ func IsPunct(c byte) bool {
|
|||
|
||||
// IsPunctRune returns true if the given rune is a punctuation, otherwise false.
|
||||
func IsPunctRune(r rune) bool {
|
||||
return int32(r) <= 256 && IsPunct(byte(r)) || unicode.IsPunct(r)
|
||||
return unicode.IsSymbol(r) || unicode.IsPunct(r)
|
||||
}
|
||||
|
||||
// IsSpace returns true if the given character is a space, otherwise false.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//go:build !appengine && !js
|
||||
// +build !appengine,!js
|
||||
//go:build !appengine && !js && !go1.21
|
||||
// +build !appengine,!js,!go1.21
|
||||
|
||||
package util
|
||||
|
||||
18
util/util_unsafe_go121.go
Normal file
18
util/util_unsafe_go121.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//go:build !appengine && !js && go1.21
|
||||
// +build !appengine,!js,go1.21
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// BytesToReadOnlyString returns a string converted from given bytes.
|
||||
func BytesToReadOnlyString(b []byte) string {
|
||||
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||
}
|
||||
|
||||
// StringToReadOnlyBytes returns bytes converted from given string.
|
||||
func StringToReadOnlyBytes(s string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||
}
|
||||
Loading…
Reference in a new issue