1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-28 13:48:49 +08:00
Dušan Borovčanin 3d3aa525a6
NOISSUE - Switch to Google Zanzibar Access control approach (#1919)
* Return Auth service

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update Compose to run with SpiceDB and Auth svc

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update auth gRPC API

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Remove Users' policies

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Move Groups to internal

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Use shared groups in Users

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Remove unused code

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Use pkg Groups in Things

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Remove Things groups

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Make imports consistent

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update Groups networking

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Remove things groups-specific API

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Move Things Clients to the root

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Move Clients to Users root

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Temporarily remove tracing

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Fix imports

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Add buffer config for gRPC

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update auth type for Things

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Use Auth for login

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Add temporary solution for refresh token

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update Tokenizer interface

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Updade tokens issuing

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Fix token issuing

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update JWT validator and refactor Tokenizer

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Rename access timeout

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Rename login to authenticate

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update Identify to use SubjectID

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Add Auth to Groups

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Use the Auth service for Groups

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update auth schema

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Fix Auth for Groups

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Add auth for addons (#14)

Signed-off-by: Arvindh <arvindh91@gmail.com>

Speparate Login and Refresh tokens

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Merge authN and authZ requests for things

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Add connect and disconnect

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update sharing

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Fix policies addition and removal

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Update relation with roels

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Add gRPC to Things

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

Assign and Unassign members to group and Listing of Group members (#15)

* add auth for addons

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add assign and unassign to group

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add group incomplete repo implementation

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users

Signed-off-by: Arvindh <arvindh91@gmail.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>

Move coap mqtt and ws policies to spicedb (#16)

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

Remove old policies

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

NOISSUE - Things authorize to return thingID (#18)

This commit modifies the authorize endpoint to the grpc endpoint to return thingID. The authorize endpoint allows adapters to get the publisher of the message.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

Add Groups to users service (#17)

* add assign and unassign to group

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add group incomplete repo implementation

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users stable 1

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users stable 2

Signed-off-by: Arvindh <arvindh91@gmail.com>

* groups for users & things

Signed-off-by: Arvindh <arvindh91@gmail.com>

* Amend signature

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix merge error

Signed-off-by: Arvindh <arvindh91@gmail.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Fix es code (#21)

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Fix Bugs (#20)

* fix bugs

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix bugs

Signed-off-by: Arvindh <arvindh91@gmail.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Test e2e (#19)

* fix: connect method

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* fix: e2e

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* fix changes in sdk and e2e

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* feat(docker): remove unnecessary port mapping

Remove the port mapping for MQTT broker in the docker-compose.yml file.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* Enable group listing

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* feat(responses): update ChannelsPage struct

The ChannelsPage struct in the responses.go file has been updated. The "Channels" field has been renamed to "Groups" to provide more accurate naming. This change ensures consistency and clarity in the codebase.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* feat(things): add UpdateClientSecret method

Add the UpdateClientSecret method to the things service. This method allows updating the client secret for a specific client identified by the provided token, id, and key parameters.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

---------

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Use smaller buffers for gRPC

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Clean up tests (#22)

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add Connect Disconnect endpoints (#23)

* fix bugs

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix bugs

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix list of things in a channel and Add connect disconnect endpoint

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix list of things in a channel and Add connect disconnect endpoint

Signed-off-by: Arvindh <arvindh91@gmail.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add: Things share with users (#25)

* fix list of things in a channel and Add connect disconnect endpoint

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add: things share with other users

Signed-off-by: Arvindh <arvindh91@gmail.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Rename gRPC Services (#24)

* Rename things and users auth service

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* docs: add authorization docs for gRPC services

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* Rename things and users grpc services

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* Remove mainflux.env package

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

---------

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add: Listing of things, channels, groups, users  (#26)

* add: listing of channels, users, groups, things

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add: listing of channels, users, groups, things

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add: listing of channels, users, groups, things

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add: listing of channels, users, groups, things

Signed-off-by: Arvindh <arvindh91@gmail.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Clean Up Users (#27)

* feat(groups): rename redis package to events

- Renamed the `redis` package to `events` in the `internal/groups` directory.
- Updated the file paths and names accordingly.
- This change reflects the more accurate purpose of the package and improves code organization.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* feat(auth): Modify identity method

Change request and response of identity method

Add accessToken and refreshToken to Token response

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* clean up users, remove dead code

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* feat(users): add unit tests for user service

This commit adds unit tests for the user service in the `users` package. The tests cover various scenarios and ensure the correct behavior of the service.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

---------

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Add: List of user groups & removed repeating code in groups (#29)

* removed repeating code in list groups

Signed-off-by: Arvindh <arvindh91@gmail.com>

* add: list of user group

Signed-off-by: Arvindh <arvindh91@gmail.com>

* fix: otel handler operator name for endpoints

Signed-off-by: Arvindh <arvindh91@gmail.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Clean Up Things Service (#28)

* Rework things service

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* add tests

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

---------

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Clean Up Auth Service (#30)

* clean up auth service

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

* feat(auth): remove unused import

Remove the unused import of `emptypb` in `auth.pb.go`. This import is not being used in the codebase and can be safely removed.

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>

---------

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* NOISSUE - Update API docs (#31)

Signed-off-by: rodneyosodo <blackd0t@protonmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Remove TODO comments and cleanup the code

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

* Update dependenices

Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>

---------

Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com>
Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
Signed-off-by: rodneyosodo <blackd0t@protonmail.com>
Co-authored-by: b1ackd0t <28790446+rodneyosodo@users.noreply.github.com>
Co-authored-by: Arvindh <30824765+arvindh123@users.noreply.github.com>
2023-10-15 22:02:13 +02:00

1828 lines
39 KiB
Go

package parser
import (
"bytes"
"html"
"regexp"
"strconv"
"unicode"
"github.com/gomarkdown/markdown/ast"
)
// Parsing block-level elements.
const (
charEntity = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});"
escapable = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]"
)
const (
captionTable = "Table: "
captionFigure = "Figure: "
captionQuote = "Quote: "
)
var (
reBackslashOrAmp = regexp.MustCompile(`[\&]`)
reEntityOrEscapedChar = regexp.MustCompile(`(?i)\\` + escapable + "|" + charEntity)
// blockTags is a set of tags that are recognized as HTML block tags.
// Any of these can be included in markdown text without special escaping.
blockTags = map[string]struct{}{
"blockquote": {},
"del": {},
"dd": {},
"div": {},
"dl": {},
"dt": {},
"fieldset": {},
"form": {},
"h1": {},
"h2": {},
"h3": {},
"h4": {},
"h5": {},
"h6": {},
// TODO: technically block but breaks Inline HTML (Simple).text
//"hr": {},
"iframe": {},
"ins": {},
"li": {},
"math": {},
"noscript": {},
"ol": {},
"pre": {},
"p": {},
"script": {},
"style": {},
"table": {},
"ul": {},
// HTML5
"address": {},
"article": {},
"aside": {},
"canvas": {},
"details": {},
"dialog": {},
"figcaption": {},
"figure": {},
"footer": {},
"header": {},
"hgroup": {},
"main": {},
"nav": {},
"output": {},
"progress": {},
"section": {},
"video": {},
}
)
// sanitizeHeadingID returns a sanitized anchor name for the given text.
// Taken from https://github.com/shurcooL/sanitized_anchor_name/blob/master/main.go#L14:1
func sanitizeHeadingID(text string) string {
var anchorName []rune
var futureDash = false
for _, r := range text {
switch {
case unicode.IsLetter(r) || unicode.IsNumber(r):
if futureDash && len(anchorName) > 0 {
anchorName = append(anchorName, '-')
}
futureDash = false
anchorName = append(anchorName, unicode.ToLower(r))
default:
futureDash = true
}
}
if len(anchorName) == 0 {
return "empty"
}
return string(anchorName)
}
// Parse Block-level data.
// Note: this function and many that it calls assume that
// the input buffer ends with a newline.
func (p *Parser) Block(data []byte) {
// this is called recursively: enforce a maximum depth
if p.nesting >= p.maxNesting {
return
}
p.nesting++
// parse out one block-level construct at a time
for len(data) > 0 {
// attributes that can be specific before a block element:
//
// {#id .class1 .class2 key="value"}
if p.extensions&Attributes != 0 {
data = p.attribute(data)
}
if p.extensions&Includes != 0 {
f := p.readInclude
path, address, consumed := p.isInclude(data)
if consumed == 0 {
path, address, consumed = p.isCodeInclude(data)
f = p.readCodeInclude
}
if consumed > 0 {
included := f(p.includeStack.Last(), path, address)
// if we find a caption below this, we need to include it in 'included', so
// that the caption will be part of the include text. (+1 to skip newline)
for _, caption := range []string{captionFigure, captionTable, captionQuote} {
if _, _, capcon := p.caption(data[consumed+1:], []byte(caption)); capcon > 0 {
included = append(included, data[consumed+1:consumed+1+capcon]...)
consumed += 1 + capcon
break // there can only be 1 caption.
}
}
p.includeStack.Push(path)
p.Block(included)
p.includeStack.Pop()
data = data[consumed:]
continue
}
}
// user supplied parser function
if p.Opts.ParserHook != nil {
node, blockdata, consumed := p.Opts.ParserHook(data)
if consumed > 0 {
data = data[consumed:]
if node != nil {
p.AddBlock(node)
if blockdata != nil {
p.Block(blockdata)
p.Finalize(node)
}
}
continue
}
}
// prefixed heading:
//
// # Heading 1
// ## Heading 2
// ...
// ###### Heading 6
if p.isPrefixHeading(data) {
data = data[p.prefixHeading(data):]
continue
}
// prefixed special heading:
// (there are no levels.)
//
// .# Abstract
if p.isPrefixSpecialHeading(data) {
data = data[p.prefixSpecialHeading(data):]
continue
}
// block of preformatted HTML:
//
// <div>
// ...
// </div>
if len(data) == 0 {
continue
}
if data[0] == '<' {
if i := p.html(data, true); i > 0 {
data = data[i:]
continue
}
}
// title block
//
// % stuff
// % more stuff
// % even more stuff
if p.extensions&Titleblock != 0 {
if data[0] == '%' {
if i := p.titleBlock(data, true); i > 0 {
data = data[i:]
continue
}
}
}
// blank lines. note: returns the # of bytes to skip
if i := IsEmpty(data); i > 0 {
data = data[i:]
continue
}
// indented code block:
//
// func max(a, b int) int {
// if a > b {
// return a
// }
// return b
// }
if p.codePrefix(data) > 0 {
data = data[p.code(data):]
continue
}
// fenced code block:
//
// ``` go
// func fact(n int) int {
// if n <= 1 {
// return n
// }
// return n * fact(n-1)
// }
// ```
if p.extensions&FencedCode != 0 {
if i := p.fencedCodeBlock(data, true); i > 0 {
data = data[i:]
continue
}
}
// horizontal rule:
//
// ------
// or
// ******
// or
// ______
if isHRule(data) {
i := skipUntilChar(data, 0, '\n')
hr := ast.HorizontalRule{}
hr.Literal = bytes.Trim(data[:i], " \n")
p.AddBlock(&hr)
data = data[i:]
continue
}
// block quote:
//
// > A big quote I found somewhere
// > on the web
if p.quotePrefix(data) > 0 {
data = data[p.quote(data):]
continue
}
// aside:
//
// A> The proof is too large to fit
// A> in the margin.
if p.extensions&Mmark != 0 {
if p.asidePrefix(data) > 0 {
data = data[p.aside(data):]
continue
}
}
// figure block:
//
// !---
// ![Alt Text](img.jpg "This is an image")
// ![Alt Text](img2.jpg "This is a second image")
// !---
if p.extensions&Mmark != 0 {
if i := p.figureBlock(data, true); i > 0 {
data = data[i:]
continue
}
}
if p.extensions&Tables != 0 {
if i := p.table(data); i > 0 {
data = data[i:]
continue
}
}
// an itemized/unordered list:
//
// * Item 1
// * Item 2
//
// also works with + or -
if p.uliPrefix(data) > 0 {
data = data[p.list(data, 0, 0, '.'):]
continue
}
// a numbered/ordered list:
//
// 1. Item 1
// 2. Item 2
if i := p.oliPrefix(data); i > 0 {
start := 0
delim := byte('.')
if i > 2 {
if p.extensions&OrderedListStart != 0 {
s := string(data[:i-2])
start, _ = strconv.Atoi(s)
if start == 1 {
start = 0
}
}
delim = data[i-2]
}
data = data[p.list(data, ast.ListTypeOrdered, start, delim):]
continue
}
// definition lists:
//
// Term 1
// : Definition a
// : Definition b
//
// Term 2
// : Definition c
if p.extensions&DefinitionLists != 0 {
if p.dliPrefix(data) > 0 {
data = data[p.list(data, ast.ListTypeDefinition, 0, '.'):]
continue
}
}
if p.extensions&MathJax != 0 {
if i := p.blockMath(data); i > 0 {
data = data[i:]
continue
}
}
// document matters:
//
// {frontmatter}/{mainmatter}/{backmatter}
if p.extensions&Mmark != 0 {
if i := p.documentMatter(data); i > 0 {
data = data[i:]
continue
}
}
// anything else must look like a normal paragraph
// note: this finds underlined headings, too
idx := p.paragraph(data)
data = data[idx:]
}
p.nesting--
}
func (p *Parser) AddBlock(n ast.Node) ast.Node {
p.closeUnmatchedBlocks()
if p.attr != nil {
if c := n.AsContainer(); c != nil {
c.Attribute = p.attr
}
if l := n.AsLeaf(); l != nil {
l.Attribute = p.attr
}
p.attr = nil
}
return p.addChild(n)
}
func (p *Parser) isPrefixHeading(data []byte) bool {
if len(data) > 0 && data[0] != '#' {
return false
}
if p.extensions&SpaceHeadings != 0 {
level := skipCharN(data, 0, '#', 6)
if level == len(data) || data[level] != ' ' {
return false
}
}
return true
}
func (p *Parser) prefixHeading(data []byte) int {
level := skipCharN(data, 0, '#', 6)
i := skipChar(data, level, ' ')
end := skipUntilChar(data, i, '\n')
skip := end
id := ""
if p.extensions&HeadingIDs != 0 {
j, k := 0, 0
// find start/end of heading id
for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ {
}
for k = j + 1; k < end && data[k] != '}'; k++ {
}
// extract heading id iff found
if j < end && k < end {
id = string(data[j+2 : k])
end = j
skip = k + 1
for end > 0 && data[end-1] == ' ' {
end--
}
}
}
for end > 0 && data[end-1] == '#' {
if isBackslashEscaped(data, end-1) {
break
}
end--
}
for end > 0 && data[end-1] == ' ' {
end--
}
if end > i {
block := &ast.Heading{
HeadingID: id,
Level: level,
}
if id == "" && p.extensions&AutoHeadingIDs != 0 {
block.HeadingID = sanitizeHeadingID(string(data[i:end]))
p.allHeadingsWithAutoID = append(p.allHeadingsWithAutoID, block)
}
block.Content = data[i:end]
p.AddBlock(block)
}
return skip
}
func (p *Parser) isPrefixSpecialHeading(data []byte) bool {
if p.extensions|Mmark == 0 {
return false
}
if len(data) < 4 {
return false
}
if data[0] != '.' {
return false
}
if data[1] != '#' {
return false
}
if data[2] == '#' { // we don't support level, so nack this.
return false
}
if p.extensions&SpaceHeadings != 0 {
if data[2] != ' ' {
return false
}
}
return true
}
func (p *Parser) prefixSpecialHeading(data []byte) int {
i := skipChar(data, 2, ' ') // ".#" skipped
end := skipUntilChar(data, i, '\n')
skip := end
id := ""
if p.extensions&HeadingIDs != 0 {
j, k := 0, 0
// find start/end of heading id
for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ {
}
for k = j + 1; k < end && data[k] != '}'; k++ {
}
// extract heading id iff found
if j < end && k < end {
id = string(data[j+2 : k])
end = j
skip = k + 1
for end > 0 && data[end-1] == ' ' {
end--
}
}
}
for end > 0 && data[end-1] == '#' {
if isBackslashEscaped(data, end-1) {
break
}
end--
}
for end > 0 && data[end-1] == ' ' {
end--
}
if end > i {
block := &ast.Heading{
HeadingID: id,
IsSpecial: true,
Level: 1, // always level 1.
}
if id == "" && p.extensions&AutoHeadingIDs != 0 {
block.HeadingID = sanitizeHeadingID(string(data[i:end]))
p.allHeadingsWithAutoID = append(p.allHeadingsWithAutoID, block)
}
block.Literal = data[i:end]
block.Content = data[i:end]
p.AddBlock(block)
}
return skip
}
func (p *Parser) isUnderlinedHeading(data []byte) int {
// test of level 1 heading
if data[0] == '=' {
i := skipChar(data, 1, '=')
i = skipChar(data, i, ' ')
if i < len(data) && data[i] == '\n' {
return 1
}
return 0
}
// test of level 2 heading
if data[0] == '-' {
i := skipChar(data, 1, '-')
i = skipChar(data, i, ' ')
if i < len(data) && data[i] == '\n' {
return 2
}
return 0
}
return 0
}
func (p *Parser) titleBlock(data []byte, doRender bool) int {
if data[0] != '%' {
return 0
}
splitData := bytes.Split(data, []byte("\n"))
var i int
for idx, b := range splitData {
if !bytes.HasPrefix(b, []byte("%")) {
i = idx // - 1
break
}
}
data = bytes.Join(splitData[0:i], []byte("\n"))
consumed := len(data)
data = bytes.TrimPrefix(data, []byte("% "))
data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1)
block := &ast.Heading{
Level: 1,
IsTitleblock: true,
}
block.Content = data
p.AddBlock(block)
return consumed
}
func (p *Parser) html(data []byte, doRender bool) int {
var i, j int
// identify the opening tag
if data[0] != '<' {
return 0
}
curtag, tagfound := p.htmlFindTag(data[1:])
// handle special cases
if !tagfound {
// check for an HTML comment
if size := p.htmlComment(data, doRender); size > 0 {
return size
}
// check for an <hr> tag
if size := p.htmlHr(data, doRender); size > 0 {
return size
}
// no special case recognized
return 0
}
// look for an unindented matching closing tag
// followed by a blank line
found := false
/*
closetag := []byte("\n</" + curtag + ">")
j = len(curtag) + 1
for !found {
// scan for a closing tag at the beginning of a line
if skip := bytes.Index(data[j:], closetag); skip >= 0 {
j += skip + len(closetag)
} else {
break
}
// see if it is the only thing on the line
if skip := IsEmpty(data[j:]); skip > 0 {
// see if it is followed by a blank line/eof
j += skip
if j >= len(data) {
found = true
i = j
} else {
if skip := IsEmpty(data[j:]); skip > 0 {
j += skip
found = true
i = j
}
}
}
}
*/
// if not found, try a second pass looking for indented match
// but not if tag is "ins" or "del" (following original Markdown.pl)
if !found && curtag != "ins" && curtag != "del" {
i = 1
for i < len(data) {
i++
for i < len(data) && !(data[i-1] == '<' && data[i] == '/') {
i++
}
if i+2+len(curtag) >= len(data) {
break
}
j = p.htmlFindEnd(curtag, data[i-1:])
if j > 0 {
i += j - 1
found = true
break
}
}
}
if !found {
return 0
}
// the end of the block has been found
if doRender {
// trim newlines
end := backChar(data, i, '\n')
htmlBLock := &ast.HTMLBlock{Leaf: ast.Leaf{Content: data[:end]}}
p.AddBlock(htmlBLock)
finalizeHTMLBlock(htmlBLock)
}
return i
}
func finalizeHTMLBlock(block *ast.HTMLBlock) {
block.Literal = block.Content
block.Content = nil
}
// HTML comment, lax form
func (p *Parser) htmlComment(data []byte, doRender bool) int {
i := p.inlineHTMLComment(data)
// needs to end with a blank line
if j := IsEmpty(data[i:]); j > 0 {
size := i + j
if doRender {
// trim trailing newlines
end := backChar(data, size, '\n')
htmlBLock := &ast.HTMLBlock{Leaf: ast.Leaf{Content: data[:end]}}
p.AddBlock(htmlBLock)
finalizeHTMLBlock(htmlBLock)
}
return size
}
return 0
}
// HR, which is the only self-closing block tag considered
func (p *Parser) htmlHr(data []byte, doRender bool) int {
if len(data) < 4 {
return 0
}
if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') {
return 0
}
if data[3] != ' ' && data[3] != '/' && data[3] != '>' {
// not an <hr> tag after all; at least not a valid one
return 0
}
i := 3
for i < len(data) && data[i] != '>' && data[i] != '\n' {
i++
}
if i < len(data) && data[i] == '>' {
i++
if j := IsEmpty(data[i:]); j > 0 {
size := i + j
if doRender {
// trim newlines
end := backChar(data, size, '\n')
htmlBlock := &ast.HTMLBlock{Leaf: ast.Leaf{Content: data[:end]}}
p.AddBlock(htmlBlock)
finalizeHTMLBlock(htmlBlock)
}
return size
}
}
return 0
}
func (p *Parser) htmlFindTag(data []byte) (string, bool) {
i := skipAlnum(data, 0)
key := string(data[:i])
if _, ok := blockTags[key]; ok {
return key, true
}
return "", false
}
func (p *Parser) htmlFindEnd(tag string, data []byte) int {
// assume data[0] == '<' && data[1] == '/' already tested
if tag == "hr" {
return 2
}
// check if tag is a match
closetag := []byte("</" + tag + ">")
if !bytes.HasPrefix(data, closetag) {
return 0
}
i := len(closetag)
// check that the rest of the line is blank
skip := 0
if skip = IsEmpty(data[i:]); skip == 0 {
return 0
}
i += skip
skip = 0
if i >= len(data) {
return i
}
if p.extensions&LaxHTMLBlocks != 0 {
return i
}
if skip = IsEmpty(data[i:]); skip == 0 {
// following line must be blank
return 0
}
return i + skip
}
func IsEmpty(data []byte) int {
// it is okay to call isEmpty on an empty buffer
if len(data) == 0 {
return 0
}
var i int
for i = 0; i < len(data) && data[i] != '\n'; i++ {
if data[i] != ' ' && data[i] != '\t' {
return 0
}
}
i = skipCharN(data, i, '\n', 1)
return i
}
func isHRule(data []byte) bool {
i := 0
// skip up to three spaces
for i < 3 && data[i] == ' ' {
i++
}
// look at the hrule char
if data[i] != '*' && data[i] != '-' && data[i] != '_' {
return false
}
c := data[i]
// the whole line must be the char or whitespace
n := 0
for i < len(data) && data[i] != '\n' {
switch {
case data[i] == c:
n++
case data[i] != ' ':
return false
}
i++
}
return n >= 3
}
// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data,
// and returns the end index if so, or 0 otherwise. It also returns the marker found.
// If syntax is not nil, it gets set to the syntax specified in the fence line.
func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) {
i, size := 0, 0
n := len(data)
// skip up to three spaces
for i < n && i < 3 && data[i] == ' ' {
i++
}
// check for the marker characters: ~ or `
if i >= n {
return 0, ""
}
if data[i] != '~' && data[i] != '`' {
return 0, ""
}
c := data[i]
// the whole line must be the same char or whitespace
for i < n && data[i] == c {
size++
i++
}
// the marker char must occur at least 3 times
if size < 3 {
return 0, ""
}
marker = string(data[i-size : i])
// if this is the end marker, it must match the beginning marker
if oldmarker != "" && marker != oldmarker {
return 0, ""
}
// if just read the beginning marker, read the syntax
if oldmarker == "" {
i = skipChar(data, i, ' ')
if i >= n {
if i == n {
return i, marker
}
return 0, ""
}
syntaxStart, syntaxLen := syntaxRange(data, &i)
if syntaxStart == 0 && syntaxLen == 0 {
return 0, ""
}
// caller wants the syntax
if syntax != nil {
*syntax = string(data[syntaxStart : syntaxStart+syntaxLen])
}
}
i = skipChar(data, i, ' ')
if i >= n || data[i] != '\n' {
if i == n {
return i, marker
}
return 0, ""
}
return i + 1, marker // Take newline into account.
}
func syntaxRange(data []byte, iout *int) (int, int) {
n := len(data)
syn := 0
i := *iout
syntaxStart := i
if data[i] == '{' {
i++
syntaxStart++
for i < n && data[i] != '}' && data[i] != '\n' {
syn++
i++
}
if i >= n || data[i] != '}' {
return 0, 0
}
// strip all whitespace at the beginning and the end
// of the {} block
for syn > 0 && IsSpace(data[syntaxStart]) {
syntaxStart++
syn--
}
for syn > 0 && IsSpace(data[syntaxStart+syn-1]) {
syn--
}
i++
} else {
for i < n && !IsSpace(data[i]) {
syn++
i++
}
}
*iout = i
return syntaxStart, syn
}
// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning,
// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects.
// If doRender is true, a final newline is mandatory to recognize the fenced code block.
func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int {
var syntax string
beg, marker := isFenceLine(data, &syntax, "")
if beg == 0 || beg >= len(data) {
return 0
}
var work bytes.Buffer
work.WriteString(syntax)
work.WriteByte('\n')
for {
// safe to assume beg < len(data)
// check for the end of the code block
fenceEnd, _ := isFenceLine(data[beg:], nil, marker)
if fenceEnd != 0 {
beg += fenceEnd
break
}
// copy the current line
end := skipUntilChar(data, beg, '\n') + 1
// did we reach the end of the buffer without a closing marker?
if end >= len(data) {
return 0
}
// verbatim copy to the working buffer
if doRender {
work.Write(data[beg:end])
}
beg = end
}
if doRender {
codeBlock := &ast.CodeBlock{
IsFenced: true,
}
codeBlock.Content = work.Bytes() // TODO: get rid of temp buffer
if p.extensions&Mmark == 0 {
p.AddBlock(codeBlock)
finalizeCodeBlock(codeBlock)
return beg
}
// Check for caption and if found make it a figure.
if captionContent, id, consumed := p.caption(data[beg:], []byte(captionFigure)); consumed > 0 {
figure := &ast.CaptionFigure{}
caption := &ast.Caption{}
figure.HeadingID = id
p.Inline(caption, captionContent)
p.AddBlock(figure)
codeBlock.AsLeaf().Attribute = figure.AsContainer().Attribute
p.addChild(codeBlock)
finalizeCodeBlock(codeBlock)
p.addChild(caption)
p.Finalize(figure)
beg += consumed
return beg
}
// Still here, normal block
p.AddBlock(codeBlock)
finalizeCodeBlock(codeBlock)
}
return beg
}
func unescapeChar(str []byte) []byte {
if str[0] == '\\' {
return []byte{str[1]}
}
return []byte(html.UnescapeString(string(str)))
}
func unescapeString(str []byte) []byte {
if reBackslashOrAmp.Match(str) {
return reEntityOrEscapedChar.ReplaceAllFunc(str, unescapeChar)
}
return str
}
func finalizeCodeBlock(code *ast.CodeBlock) {
c := code.Content
if code.IsFenced {
newlinePos := bytes.IndexByte(c, '\n')
firstLine := c[:newlinePos]
rest := c[newlinePos+1:]
code.Info = unescapeString(bytes.Trim(firstLine, "\n"))
code.Literal = rest
} else {
code.Literal = c
}
code.Content = nil
}
// returns blockquote prefix length
func (p *Parser) quotePrefix(data []byte) int {
i := 0
n := len(data)
for i < 3 && i < n && data[i] == ' ' {
i++
}
if i < n && data[i] == '>' {
if i+1 < n && data[i+1] == ' ' {
return i + 2
}
return i + 1
}
return 0
}
// blockquote ends with at least one blank line
// followed by something without a blockquote prefix
func (p *Parser) terminateBlockquote(data []byte, beg, end int) bool {
if IsEmpty(data[beg:]) <= 0 {
return false
}
if end >= len(data) {
return true
}
return p.quotePrefix(data[end:]) == 0 && IsEmpty(data[end:]) == 0
}
// parse a blockquote fragment
func (p *Parser) quote(data []byte) int {
var raw bytes.Buffer
beg, end := 0, 0
for beg < len(data) {
end = beg
// Step over whole lines, collecting them. While doing that, check for
// fenced code and if one's found, incorporate it altogether,
// irregardless of any contents inside it
for end < len(data) && data[end] != '\n' {
if p.extensions&FencedCode != 0 {
if i := p.fencedCodeBlock(data[end:], false); i > 0 {
// -1 to compensate for the extra end++ after the loop:
end += i - 1
break
}
}
end++
}
end = skipCharN(data, end, '\n', 1)
if pre := p.quotePrefix(data[beg:]); pre > 0 {
// skip the prefix
beg += pre
} else if p.terminateBlockquote(data, beg, end) {
break
}
// this line is part of the blockquote
raw.Write(data[beg:end])
beg = end
}
if p.extensions&Mmark == 0 {
block := p.AddBlock(&ast.BlockQuote{})
p.Block(raw.Bytes())
p.Finalize(block)
return end
}
if captionContent, id, consumed := p.caption(data[end:], []byte(captionQuote)); consumed > 0 {
figure := &ast.CaptionFigure{}
caption := &ast.Caption{}
figure.HeadingID = id
p.Inline(caption, captionContent)
p.AddBlock(figure) // this discard any attributes
block := &ast.BlockQuote{}
block.AsContainer().Attribute = figure.AsContainer().Attribute
p.addChild(block)
p.Block(raw.Bytes())
p.Finalize(block)
p.addChild(caption)
p.Finalize(figure)
end += consumed
return end
}
block := p.AddBlock(&ast.BlockQuote{})
p.Block(raw.Bytes())
p.Finalize(block)
return end
}
// returns prefix length for block code
func (p *Parser) codePrefix(data []byte) int {
n := len(data)
if n >= 1 && data[0] == '\t' {
return 1
}
if n >= 4 && data[3] == ' ' && data[2] == ' ' && data[1] == ' ' && data[0] == ' ' {
return 4
}
return 0
}
func (p *Parser) code(data []byte) int {
var work bytes.Buffer
i := 0
for i < len(data) {
beg := i
i = skipUntilChar(data, i, '\n')
i = skipCharN(data, i, '\n', 1)
blankline := IsEmpty(data[beg:i]) > 0
if pre := p.codePrefix(data[beg:i]); pre > 0 {
beg += pre
} else if !blankline {
// non-empty, non-prefixed line breaks the pre
i = beg
break
}
// verbatim copy to the working buffer
if blankline {
work.WriteByte('\n')
} else {
work.Write(data[beg:i])
}
}
// trim all the \n off the end of work
workbytes := work.Bytes()
eol := backChar(workbytes, len(workbytes), '\n')
if eol != len(workbytes) {
work.Truncate(eol)
}
work.WriteByte('\n')
codeBlock := &ast.CodeBlock{
IsFenced: false,
}
// TODO: get rid of temp buffer
codeBlock.Content = work.Bytes()
p.AddBlock(codeBlock)
finalizeCodeBlock(codeBlock)
return i
}
// returns unordered list item prefix
func (p *Parser) uliPrefix(data []byte) int {
// start with up to 3 spaces
i := skipCharN(data, 0, ' ', 3)
if i >= len(data)-1 {
return 0
}
// need one of {'*', '+', '-'} followed by a space or a tab
if (data[i] != '*' && data[i] != '+' && data[i] != '-') ||
(data[i+1] != ' ' && data[i+1] != '\t') {
return 0
}
return i + 2
}
// returns ordered list item prefix
func (p *Parser) oliPrefix(data []byte) int {
// start with up to 3 spaces
i := skipCharN(data, 0, ' ', 3)
// count the digits
start := i
for i < len(data) && data[i] >= '0' && data[i] <= '9' {
i++
}
if start == i || i >= len(data)-1 {
return 0
}
// we need >= 1 digits followed by a dot and a space or a tab
if data[i] != '.' && data[i] != ')' || !(data[i+1] == ' ' || data[i+1] == '\t') {
return 0
}
return i + 2
}
// returns definition list item prefix
func (p *Parser) dliPrefix(data []byte) int {
if len(data) < 2 {
return 0
}
// need a ':' followed by a space or a tab
if data[0] != ':' || !(data[1] == ' ' || data[1] == '\t') {
return 0
}
// TODO: this is a no-op (data[0] is ':' so not ' ').
// Maybe the intent was to eat spaces before ':' ?
// either way, no change in tests
i := skipChar(data, 0, ' ')
return i + 2
}
// TODO: maybe it was meant to be like below
// either way, no change in tests
/*
func (p *Parser) dliPrefix(data []byte) int {
i := skipChar(data, 0, ' ')
if i+len(data) < 2 {
return 0
}
// need a ':' followed by a space or a tab
if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') {
return 0
}
return i + 2
}
*/
// parse ordered or unordered list block
func (p *Parser) list(data []byte, flags ast.ListType, start int, delim byte) int {
i := 0
flags |= ast.ListItemBeginningOfList
list := &ast.List{
ListFlags: flags,
Tight: true,
Start: start,
Delimiter: delim,
}
block := p.AddBlock(list)
for i < len(data) {
skip := p.listItem(data[i:], &flags)
if flags&ast.ListItemContainsBlock != 0 {
list.Tight = false
}
i += skip
if skip == 0 || flags&ast.ListItemEndOfList != 0 {
break
}
flags &= ^ast.ListItemBeginningOfList
}
above := block.GetParent()
finalizeList(list)
p.tip = above
return i
}
// Returns true if the list item is not the same type as its parent list
func (p *Parser) listTypeChanged(data []byte, flags *ast.ListType) bool {
if p.dliPrefix(data) > 0 && *flags&ast.ListTypeDefinition == 0 {
return true
} else if p.oliPrefix(data) > 0 && *flags&ast.ListTypeOrdered == 0 {
return true
} else if p.uliPrefix(data) > 0 && (*flags&ast.ListTypeOrdered != 0 || *flags&ast.ListTypeDefinition != 0) {
return true
}
return false
}
// Returns true if block ends with a blank line, descending if needed
// into lists and sublists.
func endsWithBlankLine(block ast.Node) bool {
// TODO: figure this out. Always false now.
for block != nil {
//if block.lastLineBlank {
//return true
//}
switch block.(type) {
case *ast.List, *ast.ListItem:
block = ast.GetLastChild(block)
default:
return false
}
}
return false
}
func finalizeList(list *ast.List) {
items := list.Parent.GetChildren()
lastItemIdx := len(items) - 1
for i, item := range items {
isLastItem := i == lastItemIdx
// check for non-final list item ending with blank line:
if !isLastItem && endsWithBlankLine(item) {
list.Tight = false
break
}
// recurse into children of list item, to see if there are spaces
// between any of them:
subItems := item.GetParent().GetChildren()
lastSubItemIdx := len(subItems) - 1
for j, subItem := range subItems {
isLastSubItem := j == lastSubItemIdx
if (!isLastItem || !isLastSubItem) && endsWithBlankLine(subItem) {
list.Tight = false
break
}
}
}
}
// Parse a single list item.
// Assumes initial prefix is already removed if this is a sublist.
func (p *Parser) listItem(data []byte, flags *ast.ListType) int {
// keep track of the indentation of the first line
itemIndent := 0
if data[0] == '\t' {
itemIndent += 4
} else {
for itemIndent < 3 && data[itemIndent] == ' ' {
itemIndent++
}
}
var (
bulletChar byte = '*'
delimiter byte = '.'
)
i := p.uliPrefix(data)
if i == 0 {
i = p.oliPrefix(data)
if i > 0 {
delimiter = data[i-2]
}
} else {
bulletChar = data[i-2]
}
if i == 0 {
i = p.dliPrefix(data)
// reset definition term flag
if i > 0 {
*flags &= ^ast.ListTypeTerm
}
}
if i == 0 {
// if in definition list, set term flag and continue
if *flags&ast.ListTypeDefinition != 0 {
*flags |= ast.ListTypeTerm
} else {
return 0
}
}
// skip leading whitespace on first line
i = skipChar(data, i, ' ')
// find the end of the line
line := i
for i > 0 && i < len(data) && data[i-1] != '\n' {
i++
}
// get working buffer
var raw bytes.Buffer
// put the first line into the working buffer
raw.Write(data[line:i])
line = i
// process the following lines
containsBlankLine := false
sublist := 0
gatherlines:
for line < len(data) {
i++
// find the end of this line
for i < len(data) && data[i-1] != '\n' {
i++
}
// if it is an empty line, guess that it is part of this item
// and move on to the next line
if IsEmpty(data[line:i]) > 0 {
containsBlankLine = true
line = i
continue
}
// calculate the indentation
indent := 0
indentIndex := 0
if data[line] == '\t' {
indentIndex++
indent += 4
} else {
for indent < 4 && line+indent < i && data[line+indent] == ' ' {
indent++
indentIndex++
}
}
chunk := data[line+indentIndex : i]
// If there is a fence line (marking starting of a code block)
// without indent do not process it as part of the list.
if p.extensions&FencedCode != 0 {
fenceLineEnd, _ := isFenceLine(chunk, nil, "")
if fenceLineEnd > 0 && indent == 0 {
*flags |= ast.ListItemEndOfList
break gatherlines
}
}
// evaluate how this line fits in
switch {
// is this a nested list item?
case (p.uliPrefix(chunk) > 0 && !isHRule(chunk)) || p.oliPrefix(chunk) > 0 || p.dliPrefix(chunk) > 0:
// if indent is 4 or more spaces on unordered or ordered lists
// we need to add leadingWhiteSpaces + 1 spaces in the beginning of the chunk
if indentIndex >= 4 && p.dliPrefix(chunk) <= 0 {
leadingWhiteSpaces := skipChar(chunk, 0, ' ')
chunk = data[line+indentIndex-(leadingWhiteSpaces+1) : i]
}
// to be a nested list, it must be indented more
// if not, it is either a different kind of list
// or the next item in the same list
if indent <= itemIndent {
if p.listTypeChanged(chunk, flags) {
*flags |= ast.ListItemEndOfList
} else if containsBlankLine {
*flags |= ast.ListItemContainsBlock
}
break gatherlines
}
if containsBlankLine {
*flags |= ast.ListItemContainsBlock
}
// is this the first item in the nested list?
if sublist == 0 {
sublist = raw.Len()
// in the case of dliPrefix we are too late and need to search back for the definition item, which
// should be on the previous line, we then adjust sublist to start there.
if p.dliPrefix(chunk) > 0 {
sublist = backUntilChar(raw.Bytes(), raw.Len()-1, '\n')
}
}
// is this a nested prefix heading?
case p.isPrefixHeading(chunk), p.isPrefixSpecialHeading(chunk):
// if the heading is not indented, it is not nested in the list
// and thus ends the list
if containsBlankLine && indent < 4 {
*flags |= ast.ListItemEndOfList
break gatherlines
}
*flags |= ast.ListItemContainsBlock
// anything following an empty line is only part
// of this item if it is indented 4 spaces
// (regardless of the indentation of the beginning of the item)
case containsBlankLine && indent < 4:
if *flags&ast.ListTypeDefinition != 0 && i < len(data)-1 {
// is the next item still a part of this list?
next := skipUntilChar(data, i, '\n')
for next < len(data)-1 && data[next] == '\n' {
next++
}
if i < len(data)-1 && data[i] != ':' && next < len(data)-1 && data[next] != ':' {
*flags |= ast.ListItemEndOfList
}
} else {
*flags |= ast.ListItemEndOfList
}
break gatherlines
// a blank line means this should be parsed as a block
case containsBlankLine:
raw.WriteByte('\n')
*flags |= ast.ListItemContainsBlock
}
// if this line was preceded by one or more blanks,
// re-introduce the blank into the buffer
if containsBlankLine {
containsBlankLine = false
raw.WriteByte('\n')
}
// add the line into the working buffer without prefix
raw.Write(chunk)
line = i
}
rawBytes := raw.Bytes()
listItem := &ast.ListItem{
ListFlags: *flags,
Tight: false,
BulletChar: bulletChar,
Delimiter: delimiter,
}
p.AddBlock(listItem)
// render the contents of the list item
if *flags&ast.ListItemContainsBlock != 0 && *flags&ast.ListTypeTerm == 0 {
// intermediate render of block item, except for definition term
if sublist > 0 {
p.Block(rawBytes[:sublist])
p.Block(rawBytes[sublist:])
} else {
p.Block(rawBytes)
}
} else {
// intermediate render of inline item
para := &ast.Paragraph{}
if sublist > 0 {
para.Content = rawBytes[:sublist]
} else {
para.Content = rawBytes
}
p.addChild(para)
if sublist > 0 {
p.Block(rawBytes[sublist:])
}
}
return line
}
// render a single paragraph that has already been parsed out
func (p *Parser) renderParagraph(data []byte) {
if len(data) == 0 {
return
}
// trim leading spaces
beg := skipChar(data, 0, ' ')
end := len(data)
// trim trailing newline
if data[len(data)-1] == '\n' {
end--
}
// trim trailing spaces
for end > beg && data[end-1] == ' ' {
end--
}
para := &ast.Paragraph{}
para.Content = data[beg:end]
p.AddBlock(para)
}
// blockMath handle block surround with $$
func (p *Parser) blockMath(data []byte) int {
if len(data) <= 4 || data[0] != '$' || data[1] != '$' || data[2] == '$' {
return 0
}
// find next $$
var end int
for end = 2; end+1 < len(data) && (data[end] != '$' || data[end+1] != '$'); end++ {
}
// $$ not match
if end+1 == len(data) {
return 0
}
// render the display math
mathBlock := &ast.MathBlock{}
mathBlock.Literal = data[2:end]
p.AddBlock(mathBlock)
return end + 2
}
func (p *Parser) paragraph(data []byte) int {
// prev: index of 1st char of previous line
// line: index of 1st char of current line
// i: index of cursor/end of current line
var prev, line, i int
tabSize := tabSizeDefault
if p.extensions&TabSizeEight != 0 {
tabSize = tabSizeDouble
}
// keep going until we find something to mark the end of the paragraph
for i < len(data) {
// mark the beginning of the current line
prev = line
current := data[i:]
line = i
// did we find a reference or a footnote? If so, end a paragraph
// preceding it and report that we have consumed up to the end of that
// reference:
if refEnd := isReference(p, current, tabSize); refEnd > 0 {
p.renderParagraph(data[:i])
return i + refEnd
}
// did we find a blank line marking the end of the paragraph?
if n := IsEmpty(current); n > 0 {
// did this blank line followed by a definition list item?
if p.extensions&DefinitionLists != 0 {
if i < len(data)-1 && data[i+1] == ':' {
listLen := p.list(data[prev:], ast.ListTypeDefinition, 0, '.')
return prev + listLen
}
}
p.renderParagraph(data[:i])
return i + n
}
// an underline under some text marks a heading, so our paragraph ended on prev line
if i > 0 {
if level := p.isUnderlinedHeading(current); level > 0 {
// render the paragraph
p.renderParagraph(data[:prev])
// ignore leading and trailing whitespace
eol := i - 1
for prev < eol && data[prev] == ' ' {
prev++
}
for eol > prev && data[eol-1] == ' ' {
eol--
}
block := &ast.Heading{
Level: level,
}
if p.extensions&AutoHeadingIDs != 0 {
block.HeadingID = sanitizeHeadingID(string(data[prev:eol]))
p.allHeadingsWithAutoID = append(p.allHeadingsWithAutoID, block)
}
block.Content = data[prev:eol]
p.AddBlock(block)
// find the end of the underline
return skipUntilChar(data, i, '\n')
}
}
// if the next line starts a block of HTML, then the paragraph ends here
if p.extensions&LaxHTMLBlocks != 0 {
if data[i] == '<' && p.html(current, false) > 0 {
// rewind to before the HTML block
p.renderParagraph(data[:i])
return i
}
}
// if there's a prefixed heading or a horizontal rule after this, paragraph is over
if p.isPrefixHeading(current) || p.isPrefixSpecialHeading(current) || isHRule(current) {
p.renderParagraph(data[:i])
return i
}
// if there's a block quote, paragraph is over
if p.quotePrefix(current) > 0 {
p.renderParagraph(data[:i])
return i
}
// if there's a fenced code block, paragraph is over
if p.extensions&FencedCode != 0 {
if p.fencedCodeBlock(current, false) > 0 {
p.renderParagraph(data[:i])
return i
}
}
// if there's a figure block, paragraph is over
if p.extensions&Mmark != 0 {
if p.figureBlock(current, false) > 0 {
p.renderParagraph(data[:i])
return i
}
}
// if there's a table, paragraph is over
if p.extensions&Tables != 0 {
if j, _, _ := p.tableHeader(current, false); j > 0 {
p.renderParagraph(data[:i])
return i
}
}
// if there's a definition list item, prev line is a definition term
if p.extensions&DefinitionLists != 0 {
if p.dliPrefix(current) != 0 {
ret := p.list(data[prev:], ast.ListTypeDefinition, 0, '.')
return ret + prev
}
}
// if there's a list after this, paragraph is over
if p.extensions&NoEmptyLineBeforeBlock != 0 {
if p.uliPrefix(current) != 0 ||
p.oliPrefix(current) != 0 ||
p.quotePrefix(current) != 0 ||
p.codePrefix(current) != 0 {
p.renderParagraph(data[:i])
return i
}
}
// otherwise, scan to the beginning of the next line
nl := bytes.IndexByte(data[i:], '\n')
if nl >= 0 {
i += nl + 1
} else {
i += len(data[i:])
}
}
p.renderParagraph(data[:i])
return i
}
// skipChar advances i as long as data[i] == c
func skipChar(data []byte, i int, c byte) int {
n := len(data)
for i < n && data[i] == c {
i++
}
return i
}
// like skipChar but only skips up to max characters
func skipCharN(data []byte, i int, c byte, max int) int {
n := len(data)
for i < n && max > 0 && data[i] == c {
i++
max--
}
return i
}
// skipUntilChar advances i as long as data[i] != c
func skipUntilChar(data []byte, i int, c byte) int {
n := len(data)
for i < n && data[i] != c {
i++
}
return i
}
func skipAlnum(data []byte, i int) int {
n := len(data)
for i < n && IsAlnum(data[i]) {
i++
}
return i
}
func skipSpace(data []byte, i int) int {
n := len(data)
for i < n && IsSpace(data[i]) {
i++
}
return i
}
func backChar(data []byte, i int, c byte) int {
for i > 0 && data[i-1] == c {
i--
}
return i
}
func backUntilChar(data []byte, i int, c byte) int {
for i > 0 && data[i-1] != c {
i--
}
return i
}