diff --git a/.drone.sec b/.drone.sec
deleted file mode 100644
index 7c5cf72..0000000
--- a/.drone.sec
+++ /dev/null
@@ -1 +0,0 @@
-eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.xT1t3tFZvZOhWHLDkvoABEebjQ_oyQ79Iko6UCYNIjzj8_-LOQdCJz93SyHriWHg7K3J5Q_PkPeZY5XtiJRBg62kAhpT4NxnWMQeXC3u4qfGqUJNFNRYwkz-yCHxAKLlKIPzqonPPo2acs1TWs5hO5yV14hVhNA7ahjhDPl59ciYvlUE6b1rt1Wjua52sla9mPkH4Tp73OWZRBEMr4KEi8TD4O2uWgZWzHYaYLvAlHwz-TPFYM1ZEvbukBSOjqSYIXRW4NWGbxdh9cmHcRFhmT53_rlgfuwKECdARE72SaI49HXSSp-l2rP7TK5U9bG8lVwo_9U3VDCZg90Br25K-w.YTWp3MKQoq40d1p4.YSjdjah29g9lAV42F24axBtHZZTU8Cdufn2ROCsgN0Ql-xL_Cx3YFqDHrw-s1l2YCvj5Mzhi7w49LKnKf_icMOC28cM8UcrL0_VTucqOssvNxSyRSiSNlgUkeTVpuMMpXfofgW4nHiCjVj-5c4nIELeMWoFE0LcRmVRJzDpE6MExIaybev1X-lwC9boBTDGleKJPteZYotkNKgA5CBzWucoD0wcm3igH_6-ZOLvltAnjWyk_KI4akseGLh9T112XcRhvmUtJRxvD3adU5zk2A5oYHYxFaRxy5Nr4xTIpMdMk_S9W6_4JgztkAtsVIjWRAB-_CqsLS7tHNBmrT7ciH6DQlpo6ZKJ42T4AWIWfO0LFL_dI39rn2YZSfz-dEWl9ZsNpc-42LB9CRKjxdWo8W48qnMwrdUL0ZgO6H0kyVTw.IOn4GNuz0vEgnHB8_WecDQ
\ No newline at end of file
diff --git a/.drone.yml b/.drone.yml
index 3a145d7..e4258a3 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -1,33 +1,25 @@
-build:
- image: golang:1.5
- environment:
- - CGO_ENABLED=0
- commands:
- - make deps
- - make vet
- - make build
- - make test
+workspace:
+ base: /go
+ path: src/github.com/drone-plugins/drone-email
-publish:
- coverage:
+pipeline:
+ test:
+ image: golang:1.6
+ environment:
+ - CGO_ENABLED=0
+ - GOPATH=/go
+ commands:
+ - go vet
+ - go test -cover -coverprofile=coverage.out
+ - go build -ldflags "-s -w -X main.build=$DRONE_BUILD_NUMBER" -a -tags netgo
+
+ latest:
+ image: plugins/docker
+ repo: plugins/email
+ tags: [ "latest", "1.0", "1" ]
when:
branch: master
- docker:
- username: $$DOCKER_USER
- password: $$DOCKER_PASS
- email: $$DOCKER_EMAIL
- repo: plugins/drone-email
- tag: latest
- when:
- branch: master
- docker:
- username: $$DOCKER_USER
- password: $$DOCKER_PASS
- email: $$DOCKER_EMAIL
- repo: plugins/drone-email
- tag: develop
- when:
- branch: develop
+ event: push
plugin:
name: Email
diff --git a/.gitignore b/.gitignore
index b3e9109..898401d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ _testmain.go
*.exe
*.test
*.prof
+.env
coverage.out
drone-email
diff --git a/DOCS.md b/DOCS.md
index 06e9af0..724dcf6 100644
--- a/DOCS.md
+++ b/DOCS.md
@@ -1,20 +1,27 @@
-Use this plugin for sending build status notifications via Email. You can
-override the default configuration with the following parameters:
+Use the Email plugin for sending build status notifications via email.
-* `from` - Send notifications from this address
-* `host` - SMTP server host
-* `port` - SMTP server port, defaults to `587`
-* `username` - SMTP username
-* `password` - SMTP password
-* `recipients` - List of recipients, defaults to commit email
+## Config
+You can configure the plugin using the following parameters:
+
+* **from** - Send notifications from this address
+* **host** - SMTP server host
+* **port** - SMTP server port, defaults to `587`
+* **username** - SMTP username
+* **password** - SMTP password
+* **skip_verify** - Skip verification of SSL certificates, defaults to `false`
+* **recipients** - List of recipients to send this mail to (besides the commit author)
+* **recipients_only** - Do not send mails to the commit author, but only to **recipients**, defaults to `false`
+* **subject** - The subject line template
+* **body** - The email body template
## Example
The following is a sample configuration in your .drone.yml file:
```yaml
-notify:
- email:
+pipeline:
+ notify:
+ image: plugins/email
from: noreply@github.com
host: smtp.mailgun.org
username: octocat
@@ -23,6 +30,45 @@ notify:
- octocat@github.com
```
+### Secrets
+The Email plugin supports reading credentials and other parameters from the Drone secret store. This is strongly recommended instead of storing credentials in the pipeline configuration in plain text.
+
+```diff
+pipeline:
+ notify:
+ image: plugins/email
+ from: noreply@github.com
+ host: smtp.mailgun.org
+- username: octocat
+- password: 12345
+ recipients:
+ - octocat@github.com
+```
+
+Use the command line utility to add the secrets to the store:
+
+```sh
+drone secret add --image=plugins/email \
+ octocat/hello-world EMAIL_USERNAME octocat
+drone secret add --image=plugins/email \
+ octocat/hello-world EMAIL_PASSWORD 12345
+```
+
+Then sign the YAML file after all secrets are added:
+
+```sh
+drone sign octocat/hello-world
+```
+
+The following secret values can be set to configure the plugin:
+* **EMAIL_HOST** - corresponds to **host**
+* **EMAIL_PORT** - corresponds to **port**
+* **EMAIL_USERNAME** - corresponds to **username**
+* **EMAIL_PASSWORD** - corresponds to **password**
+* **EMAIL_RECIPIENTS** - corresponds to **recipients**
+
+See [Secret Guide](http://readme.drone.io/usage/secret-guide/) for additional information on secrets.
+
### Custom Templates
In some cases you may want to customize the look and feel of the email message
@@ -30,24 +76,23 @@ so you can use custom templates. For the use case we expose the following
additional parameters, all of the accept a custom handlebars template, directly
provided as a string or as a remote URL which gets fetched and parsed:
-* `subject` - A handlebars template to create a custom subject. For more
+* **subject** - A handlebars template to create a custom subject. For more
details take a look at the [docs](http://handlebarsjs.com/). You can see the
- default template [here](https://github.com/drone-plugins/drone-email/blob/master/template.go#L4)
-* `template` - A handlebars template to create a custom template. For more
+ default template [here](https://github.com/drone-plugins/drone-email/blob/master/defaults.go#L14)
+* **body** - A handlebars template to create a custom template. For more
details take a look at the [docs](http://handlebarsjs.com/). You can see the
- default template [here](https://github.com/drone-plugins/drone-email/blob/master/template.go#L8-L292)
+ default template [here](https://github.com/drone-plugins/drone-email/blob/master/defaults.go#L19-L267)
Example configuration that generate a custom email:
```yaml
-notify:
- email:
+pipeline:
+ notify:
+ image: plugins/email
from: noreply@github.com
host: smtp.mailgun.org
username: octocat
password: 12345
- recipients:
- - octocat@github.com
subject: >
[{{ build.status }}]
{{ repo.owner }}/{{ repo.name }}
@@ -63,18 +108,17 @@ as it leads to an unsecure environment. Please use this option only within your
intranet and/or with truested resources. For this use case we expose the
following additional parameter:
-* `skip_verify` - Skip verification of SSL certificates
+* **skip_verify** - Skip verification of SSL certificates
Example configuration that skips SSL verification:
```yaml
-notify:
- email:
+pipeline:
+ notify:
+ image: plugins/email
from: noreply@github.com
host: smtp.mailgun.org
username: octocat
password: 12345
skip_verify: true
- recipients:
- - octocat@github.com
```
diff --git a/Dockerfile b/Dockerfile
index 896e484..04f6c2f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,14 +1,7 @@
-# Docker image for the Drone Email plugin
-#
-# cd $GOPATH/src/github.com/drone-plugins/drone-email
-# make deps build docker
-
-FROM alpine:3.2
+FROM alpine:3.4
RUN apk update && \
- apk add \
- ca-certificates && \
- rm -rf /var/cache/apk/*
+ apk add --no-cache ca-certificates
ADD drone-email /bin/
ENTRYPOINT ["/bin/drone-email"]
diff --git a/Dockerfile.armhf b/Dockerfile.armhf
new file mode 100644
index 0000000..5b483ae
--- /dev/null
+++ b/Dockerfile.armhf
@@ -0,0 +1,7 @@
+FROM armhfbuild/alpine:3.4
+
+RUN apk update && \
+ apk add --no-cache ca-certificates
+
+ADD drone-email /bin/
+ENTRYPOINT ["/bin/drone-email"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index ea2ec81..0000000
--- a/Makefile
+++ /dev/null
@@ -1,34 +0,0 @@
-.PHONY: all clean deps fmt vet test docker
-
-EXECUTABLE ?= drone-email
-IMAGE ?= plugins/$(EXECUTABLE)
-COMMIT ?= $(shell git rev-parse --short HEAD)
-
-LDFLAGS = -X "main.buildCommit=$(COMMIT)"
-PACKAGES = $(shell go list ./... | grep -v /vendor/)
-
-all: deps build test
-
-clean:
- go clean -i ./...
-
-deps:
- go get -t ./...
-
-fmt:
- go fmt $(PACKAGES)
-
-vet:
- go vet $(PACKAGES)
-
-test:
- @for PKG in $(PACKAGES); do go test -cover -coverprofile $$GOPATH/src/$$PKG/coverage.out $$PKG || exit 1; done;
-
-docker:
- GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-s -w $(LDFLAGS)'
- docker build --rm -t $(IMAGE) .
-
-$(EXECUTABLE): $(wildcard *.go)
- go build -ldflags '-s -w $(LDFLAGS)'
-
-build: $(EXECUTABLE)
diff --git a/README.md b/README.md
index 7c0f62f..d548da4 100644
--- a/README.md
+++ b/README.md
@@ -1,113 +1,57 @@
# drone-email
[![Build Status](http://beta.drone.io/api/badges/drone-plugins/drone-email/status.svg)](http://beta.drone.io/drone-plugins/drone-email)
-[![Coverage Status](https://aircover.co/badges/drone-plugins/drone-email/coverage.svg)](https://aircover.co/drone-plugins/drone-email)
-[![](https://badge.imagelayers.io/plugins/drone-email:latest.svg)](https://imagelayers.io/?images=plugins/drone-email:latest 'Get your own badge on imagelayers.io')
+[![Go Doc](https://godoc.org/github.com/drone-plugins/drone-email?status.svg)](http://godoc.org/github.com/drone-plugins/drone-email)
+[![Go Report](https://goreportcard.com/badge/github.com/drone-plugins/drone-email)](https://goreportcard.com/report/github.com/drone-plugins/drone-email)
+[![Join the chat at https://gitter.im/drone/drone](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/drone/drone)
+
Drone plugin to send build status notifications via Email. For the usage information and a listing of the available options please take a look at [the docs](DOCS.md).
## Binary
-Build the binary using `make`:
+Build the binary with the following command:
```
-make deps build
-```
-
-### Example
-
-```sh
-./drone-email <
+ Here is some more information:
+
+ Welcome to your new account on my service!
+
+
+
+
block. + if d != "" && d[0] == '\r' { + d = d[1:] + } + if d != "" && d[0] == '\n' { + d = d[1:] + } + } + } + d = strings.Replace(d, "\x00", "", -1) + if d == "" { + return true + } + p.reconstructActiveFormattingElements() + p.addText(d) + if p.framesetOK && strings.TrimLeft(d, whitespace) != "" { + // There were non-whitespace characters inserted. + p.framesetOK = false + } + case StartTagToken: + switch p.tok.DataAtom { + case a.Html: + copyAttributes(p.oe[0], p.tok) + case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title: + return inHeadIM(p) + case a.Body: + if len(p.oe) >= 2 { + body := p.oe[1] + if body.Type == ElementNode && body.DataAtom == a.Body { + p.framesetOK = false + copyAttributes(body, p.tok) + } + } + case a.Frameset: + if !p.framesetOK || len(p.oe) < 2 || p.oe[1].DataAtom != a.Body { + // Ignore the token. + return true + } + body := p.oe[1] + if body.Parent != nil { + body.Parent.RemoveChild(body) + } + p.oe = p.oe[:1] + p.addElement() + p.im = inFramesetIM + return true + case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Menu, a.Nav, a.Ol, a.P, a.Section, a.Summary, a.Ul: + p.popUntil(buttonScope, a.P) + p.addElement() + case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6: + p.popUntil(buttonScope, a.P) + switch n := p.top(); n.DataAtom { + case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6: + p.oe.pop() + } + p.addElement() + case a.Pre, a.Listing: + p.popUntil(buttonScope, a.P) + p.addElement() + // The newline, if any, will be dealt with by the TextToken case. + p.framesetOK = false + case a.Form: + if p.form == nil { + p.popUntil(buttonScope, a.P) + p.addElement() + p.form = p.top() + } + case a.Li: + p.framesetOK = false + for i := len(p.oe) - 1; i >= 0; i-- { + node := p.oe[i] + switch node.DataAtom { + case a.Li: + p.oe = p.oe[:i] + case a.Address, a.Div, a.P: + continue + default: + if !isSpecialElement(node) { + continue + } + } + break + } + p.popUntil(buttonScope, a.P) + p.addElement() + case a.Dd, a.Dt: + p.framesetOK = false + for i := len(p.oe) - 1; i >= 0; i-- { + node := p.oe[i] + switch node.DataAtom { + case a.Dd, a.Dt: + p.oe = p.oe[:i] + case a.Address, a.Div, a.P: + continue + default: + if !isSpecialElement(node) { + continue + } + } + break + } + p.popUntil(buttonScope, a.P) + p.addElement() + case a.Plaintext: + p.popUntil(buttonScope, a.P) + p.addElement() + case a.Button: + p.popUntil(defaultScope, a.Button) + p.reconstructActiveFormattingElements() + p.addElement() + p.framesetOK = false + case a.A: + for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- { + if n := p.afe[i]; n.Type == ElementNode && n.DataAtom == a.A { + p.inBodyEndTagFormatting(a.A) + p.oe.remove(n) + p.afe.remove(n) + break + } + } + p.reconstructActiveFormattingElements() + p.addFormattingElement() + case a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U: + p.reconstructActiveFormattingElements() + p.addFormattingElement() + case a.Nobr: + p.reconstructActiveFormattingElements() + if p.elementInScope(defaultScope, a.Nobr) { + p.inBodyEndTagFormatting(a.Nobr) + p.reconstructActiveFormattingElements() + } + p.addFormattingElement() + case a.Applet, a.Marquee, a.Object: + p.reconstructActiveFormattingElements() + p.addElement() + p.afe = append(p.afe, &scopeMarker) + p.framesetOK = false + case a.Table: + if !p.quirks { + p.popUntil(buttonScope, a.P) + } + p.addElement() + p.framesetOK = false + p.im = inTableIM + return true + case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr: + p.reconstructActiveFormattingElements() + p.addElement() + p.oe.pop() + p.acknowledgeSelfClosingTag() + if p.tok.DataAtom == a.Input { + for _, t := range p.tok.Attr { + if t.Key == "type" { + if strings.ToLower(t.Val) == "hidden" { + // Skip setting framesetOK = false + return true + } + } + } + } + p.framesetOK = false + case a.Param, a.Source, a.Track: + p.addElement() + p.oe.pop() + p.acknowledgeSelfClosingTag() + case a.Hr: + p.popUntil(buttonScope, a.P) + p.addElement() + p.oe.pop() + p.acknowledgeSelfClosingTag() + p.framesetOK = false + case a.Image: + p.tok.DataAtom = a.Img + p.tok.Data = a.Img.String() + return false + case a.Isindex: + if p.form != nil { + // Ignore the token. + return true + } + action := "" + prompt := "This is a searchable index. Enter search keywords: " + attr := []Attribute{{Key: "name", Val: "isindex"}} + for _, t := range p.tok.Attr { + switch t.Key { + case "action": + action = t.Val + case "name": + // Ignore the attribute. + case "prompt": + prompt = t.Val + default: + attr = append(attr, t) + } + } + p.acknowledgeSelfClosingTag() + p.popUntil(buttonScope, a.P) + p.parseImpliedToken(StartTagToken, a.Form, a.Form.String()) + if action != "" { + p.form.Attr = []Attribute{{Key: "action", Val: action}} + } + p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String()) + p.parseImpliedToken(StartTagToken, a.Label, a.Label.String()) + p.addText(prompt) + p.addChild(&Node{ + Type: ElementNode, + DataAtom: a.Input, + Data: a.Input.String(), + Attr: attr, + }) + p.oe.pop() + p.parseImpliedToken(EndTagToken, a.Label, a.Label.String()) + p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String()) + p.parseImpliedToken(EndTagToken, a.Form, a.Form.String()) + case a.Textarea: + p.addElement() + p.setOriginalIM() + p.framesetOK = false + p.im = textIM + case a.Xmp: + p.popUntil(buttonScope, a.P) + p.reconstructActiveFormattingElements() + p.framesetOK = false + p.addElement() + p.setOriginalIM() + p.im = textIM + case a.Iframe: + p.framesetOK = false + p.addElement() + p.setOriginalIM() + p.im = textIM + case a.Noembed, a.Noscript: + p.addElement() + p.setOriginalIM() + p.im = textIM + case a.Select: + p.reconstructActiveFormattingElements() + p.addElement() + p.framesetOK = false + p.im = inSelectIM + return true + case a.Optgroup, a.Option: + if p.top().DataAtom == a.Option { + p.oe.pop() + } + p.reconstructActiveFormattingElements() + p.addElement() + case a.Rp, a.Rt: + if p.elementInScope(defaultScope, a.Ruby) { + p.generateImpliedEndTags() + } + p.addElement() + case a.Math, a.Svg: + p.reconstructActiveFormattingElements() + if p.tok.DataAtom == a.Math { + adjustAttributeNames(p.tok.Attr, mathMLAttributeAdjustments) + } else { + adjustAttributeNames(p.tok.Attr, svgAttributeAdjustments) + } + adjustForeignAttributes(p.tok.Attr) + p.addElement() + p.top().Namespace = p.tok.Data + if p.hasSelfClosingToken { + p.oe.pop() + p.acknowledgeSelfClosingTag() + } + return true + case a.Caption, a.Col, a.Colgroup, a.Frame, a.Head, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr: + // Ignore the token. + default: + p.reconstructActiveFormattingElements() + p.addElement() + } + case EndTagToken: + switch p.tok.DataAtom { + case a.Body: + if p.elementInScope(defaultScope, a.Body) { + p.im = afterBodyIM + } + case a.Html: + if p.elementInScope(defaultScope, a.Body) { + p.parseImpliedToken(EndTagToken, a.Body, a.Body.String()) + return false + } + return true + case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul: + p.popUntil(defaultScope, p.tok.DataAtom) + case a.Form: + node := p.form + p.form = nil + i := p.indexOfElementInScope(defaultScope, a.Form) + if node == nil || i == -1 || p.oe[i] != node { + // Ignore the token. + return true + } + p.generateImpliedEndTags() + p.oe.remove(node) + case a.P: + if !p.elementInScope(buttonScope, a.P) { + p.parseImpliedToken(StartTagToken, a.P, a.P.String()) + } + p.popUntil(buttonScope, a.P) + case a.Li: + p.popUntil(listItemScope, a.Li) + case a.Dd, a.Dt: + p.popUntil(defaultScope, p.tok.DataAtom) + case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6: + p.popUntil(defaultScope, a.H1, a.H2, a.H3, a.H4, a.H5, a.H6) + case a.A, a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.Nobr, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U: + p.inBodyEndTagFormatting(p.tok.DataAtom) + case a.Applet, a.Marquee, a.Object: + if p.popUntil(defaultScope, p.tok.DataAtom) { + p.clearActiveFormattingElements() + } + case a.Br: + p.tok.Type = StartTagToken + return false + default: + p.inBodyEndTagOther(p.tok.DataAtom) + } + case CommentToken: + p.addChild(&Node{ + Type: CommentNode, + Data: p.tok.Data, + }) + } + + return true +} + +func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) { + // This is the "adoption agency" algorithm, described at + // https://html.spec.whatwg.org/multipage/syntax.html#adoptionAgency + + // TODO: this is a fairly literal line-by-line translation of that algorithm. + // Once the code successfully parses the comprehensive test suite, we should + // refactor this code to be more idiomatic. + + // Steps 1-4. The outer loop. + for i := 0; i < 8; i++ { + // Step 5. Find the formatting element. + var formattingElement *Node + for j := len(p.afe) - 1; j >= 0; j-- { + if p.afe[j].Type == scopeMarkerNode { + break + } + if p.afe[j].DataAtom == tagAtom { + formattingElement = p.afe[j] + break + } + } + if formattingElement == nil { + p.inBodyEndTagOther(tagAtom) + return + } + feIndex := p.oe.index(formattingElement) + if feIndex == -1 { + p.afe.remove(formattingElement) + return + } + if !p.elementInScope(defaultScope, tagAtom) { + // Ignore the tag. + return + } + + // Steps 9-10. Find the furthest block. + var furthestBlock *Node + for _, e := range p.oe[feIndex:] { + if isSpecialElement(e) { + furthestBlock = e + break + } + } + if furthestBlock == nil { + e := p.oe.pop() + for e != formattingElement { + e = p.oe.pop() + } + p.afe.remove(e) + return + } + + // Steps 11-12. Find the common ancestor and bookmark node. + commonAncestor := p.oe[feIndex-1] + bookmark := p.afe.index(formattingElement) + + // Step 13. The inner loop. Find the lastNode to reparent. + lastNode := furthestBlock + node := furthestBlock + x := p.oe.index(node) + // Steps 13.1-13.2 + for j := 0; j < 3; j++ { + // Step 13.3. + x-- + node = p.oe[x] + // Step 13.4 - 13.5. + if p.afe.index(node) == -1 { + p.oe.remove(node) + continue + } + // Step 13.6. + if node == formattingElement { + break + } + // Step 13.7. + clone := node.clone() + p.afe[p.afe.index(node)] = clone + p.oe[p.oe.index(node)] = clone + node = clone + // Step 13.8. + if lastNode == furthestBlock { + bookmark = p.afe.index(node) + 1 + } + // Step 13.9. + if lastNode.Parent != nil { + lastNode.Parent.RemoveChild(lastNode) + } + node.AppendChild(lastNode) + // Step 13.10. + lastNode = node + } + + // Step 14. Reparent lastNode to the common ancestor, + // or for misnested table nodes, to the foster parent. + if lastNode.Parent != nil { + lastNode.Parent.RemoveChild(lastNode) + } + switch commonAncestor.DataAtom { + case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: + p.fosterParent(lastNode) + default: + commonAncestor.AppendChild(lastNode) + } + + // Steps 15-17. Reparent nodes from the furthest block's children + // to a clone of the formatting element. + clone := formattingElement.clone() + reparentChildren(clone, furthestBlock) + furthestBlock.AppendChild(clone) + + // Step 18. Fix up the list of active formatting elements. + if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark { + // Move the bookmark with the rest of the list. + bookmark-- + } + p.afe.remove(formattingElement) + p.afe.insert(bookmark, clone) + + // Step 19. Fix up the stack of open elements. + p.oe.remove(formattingElement) + p.oe.insert(p.oe.index(furthestBlock)+1, clone) + } +} + +// inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM. +// "Any other end tag" handling from 12.2.5.5 The rules for parsing tokens in foreign content +// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inforeign +func (p *parser) inBodyEndTagOther(tagAtom a.Atom) { + for i := len(p.oe) - 1; i >= 0; i-- { + if p.oe[i].DataAtom == tagAtom { + p.oe = p.oe[:i] + break + } + if isSpecialElement(p.oe[i]) { + break + } + } +} + +// Section 12.2.5.4.8. +func textIM(p *parser) bool { + switch p.tok.Type { + case ErrorToken: + p.oe.pop() + case TextToken: + d := p.tok.Data + if n := p.oe.top(); n.DataAtom == a.Textarea && n.FirstChild == nil { + // Ignore a newline at the start of a