Merge branch 'github-dev' into feature-plugin

This commit is contained in:
LinkinStars 2023-02-08 12:01:27 +08:00
commit 2fe00d4630
18 changed files with 414 additions and 112 deletions

59
.github/Dockerfile vendored Normal file
View File

@ -0,0 +1,59 @@
FROM amd64/node AS node-builder
LABEL maintainer="mingcheng<mc@sf.com>"
COPY . /answer
WORKDIR /answer
RUN make install-ui-packages ui && mv ui/build /tmp
# stage2 build the main binary within static resource
FROM golang:1.19-alpine AS golang-builder
LABEL maintainer="aichy@sf.com"
ARG GOPROXY
ENV GOPROXY=https://goproxy.cn,direct
ENV GOPATH /go
ENV GOROOT /usr/local/go
ENV PACKAGE github.com/answerdev/answer
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
ARG TAGS="sqlite sqlite_unlock_notify"
ENV TAGS "bindata timetzdata $TAGS"
ARG CGO_EXTRA_CFLAGS
COPY . ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
RUN apk --no-cache add build-base git \
&& make clean build \
&& cp answer /usr/bin/answer
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
# stage3 copy the binary and resource files into fresh container
FROM alpine
LABEL maintainer="maintainers@sf.com"
ENV TZ "Asia/Shanghai"
RUN apk update \
&& apk --no-cache add \
bash \
ca-certificates \
curl \
dumb-init \
gettext \
openssh \
sqlite \
gnupg \
&& echo "Asia/Shanghai" > /etc/timezone
COPY --from=golang-builder /usr/bin/answer /usr/bin/answer
COPY --from=golang-builder /data /data
COPY /script/entrypoint.sh /entrypoint.sh
RUN chmod 755 /entrypoint.sh
VOLUME /data
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -1,8 +1,8 @@
name: Build Docker Hub Image
name: Build DockerHub Image
on:
push:
branches: [ "main" ]
branches: [ "main","githubaction","test" ]
tags:
- v2.*
- v1.*
@ -17,44 +17,43 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: answerdev/answer
tags: |
type=raw,value=latest
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
platforms: linux/amd64,linux/arm64
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v3
with:
images: answerdev/answer
tags: |
type=raw,value=latest
# branch event
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
# tag event
#type=ref,enable=true,priority=600,prefix=,suffix=,event=tag
type=semver,pattern={{version}}
# - name: Login to GitHub Container Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
file: ./.github/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,65 +0,0 @@
name: Build GitHub Image
on:
push:
branches: [ "main" ]
tags:
- 2.*
- 1.*
- 0.*
# pull_request:
# branches: [ "main" ]
env:
REGISTRY: ghcr.io
IMAGE: answerdev/answer
jobs:
build-and-push:
runs-on: [self-hosted, linux]
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
tags: |
type=raw,value=latest
- name: Build Img
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
builder: ${{ steps.buildx.outputs.name }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# - name: build to hub.docker
# run: |
# docker build -t answerdev/answer -f ./Dockerfile .
# - name: Login to hub.docker Registry
# run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
# - name: Push Image to hub.docker
# run: |
# docker push answerdev/answer

View File

@ -8,7 +8,7 @@ permissions:
contents: write
jobs:
build-and-push:
build-goreleaser:
runs-on: [self-hosted, linux]
steps:

View File

@ -2,13 +2,13 @@ name: Go Build Test
on:
push:
branches: [ "main" ]
branches: [ "main","githubaction","test" ]
pull_request:
branches: [ "main" ]
jobs:
build-and-push:
go-build-test:
runs-on: [self-hosted, linux]
steps:

View File

@ -2,12 +2,12 @@ name: Node Build Test
on:
push:
branches: [ "main" ]
branches: [ "main","githubaction","test"]
pull_request:
branches: [ "main" ]
jobs:
build-and-push:
node-build-test:
runs-on: [self-hosted, linux]
steps:
- name: Checkout

98
.github/workflows/uffizzi-build.yml vendored Normal file
View File

@ -0,0 +1,98 @@
name: Build PR Image
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
build-answer:
name: Build and push `Answer`
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.meta.outputs.tags }}
if: ${{ github.event.action != 'closed' }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Generate UUID image name
id: uuid
run: echo "UUID_WORKER=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: registry.uffizzi.com/${{ env.UUID_WORKER }}
tags: |
type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com - Uffizzi's ephemeral Registry
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
cache-from: type=gha
cache-to: type=gha, mode=max
render-compose-file:
name: Render Docker Compose File
# Pass output of this workflow to another triggered by `workflow_run` event.
runs-on: ubuntu-latest
needs:
- build-answer
outputs:
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
steps:
- name: Checkout git repo
uses: actions/checkout@v3
- name: Render Compose File
run: |
ANSWER_IMAGE=${{ needs.build-answer.outputs.tags }}
export ANSWER_IMAGE
export UFFIZZI_URL=\$UFFIZZI_URL
# Render simple template from environment variables.
envsubst < docker-compose.uffizzi.yml > docker-compose.rendered.yml
cat docker-compose.rendered.yml
- name: Upload Rendered Compose File as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2
delete-preview:
name: Call for Preview Deletion
runs-on: ubuntu-latest
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
retention-days: 2

88
.github/workflows/uffizzi-preview.yml vendored Normal file
View File

@ -0,0 +1,88 @@
name: Deploy Uffizzi Preview
on:
workflow_run:
workflows:
- "Build PR Image"
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.HASH }}
pr-number: ${{ env.PR_NUMBER }}
steps:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
with:
path: docker-compose.rendered.yml
key: ${{ env.HASH }}
- name: Read PR Number From Event Object
id: pr
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
- name: DEBUG - Print Job Outputs
if: ${{ runner.debug }}
run: |
echo "PR number: ${{ env.PR_NUMBER }}"
echo "Compose file hash: ${{ env.HASH }}"
cat event.json
deploy-uffizzi-preview:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
compose-file-cache-path: docker-compose.rendered.yml
server: https://app.uffizzi.com
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
permissions:
contents: read
pull-requests: write
id-token: write

View File

@ -0,0 +1,36 @@
version: "3"
# uffizzi integration
x-uffizzi:
ingress:
service: answer
port: 80
services:
answer:
image: "${ANSWER_IMAGE}"
volumes:
- answer-data:/data
deploy:
resources:
limits:
memory: 4000M
mysql:
image: mysql:latest
environment:
- MYSQL_DATABASE=answer
- MYSQL_ROOT_PASSWORD=password
- MYSQL_USER=mysql
- MYSQL_PASSWORD=mysql
volumes:
- sql_data:/var/lib/mysql
deploy:
resources:
limits:
memory: 500M
volumes:
answer-data:
sql_data:

4
go.mod
View File

@ -138,3 +138,7 @@ require (
modernc.org/token v1.0.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
// github action runner Sometimes it will time out.
replace gitee.com/travelliu/dm v1.8.11192 => github.com/aichy126/dm v1.8.11192
replace modernc.org/z v1.2.19 => github.com/aichy126/modernc.org_z v1.2.19

2
go.sum
View File

@ -38,7 +38,6 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -64,6 +63,7 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/aichy126/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=

View File

@ -150,6 +150,9 @@ backend:
install:
create_config_failed:
other: Can't create the config.yaml file.
upload:
unsupported_file_format:
other: Unsupported file format.
report:
spam:
name:

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"strings"
@ -53,6 +54,8 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
ctx.Next()
return
}
ext := strings.ToLower(path.Ext(filePath)[1:])
ctx.Header("content-type", fmt.Sprintf("image/%s", ext))
_, err = ctx.Writer.WriteString(string(avatarfile))
if err != nil {
log.Error(err)
@ -60,6 +63,17 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
ctx.Abort()
return
} else {
uUrl, err := url.Parse(u)
if err != nil {
ctx.Next()
return
}
_, urlfileName := filepath.Split(uUrl.Path)
uploadPath := am.serviceConfig.UploadPath
filePath := fmt.Sprintf("%s/%s", uploadPath, urlfileName)
ext := strings.ToLower(path.Ext(filePath)[1:])
ctx.Header("content-type", fmt.Sprintf("image/%s", ext))
}
ctx.Next()
}

View File

@ -54,6 +54,7 @@ const (
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"

View File

@ -8,6 +8,7 @@ import (
"github.com/google/wire"
myTran "github.com/segmentfault/pacman/contrib/i18n"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
"gopkg.in/yaml.v3"
)
@ -68,12 +69,14 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
content, err := yaml.Marshal(translation)
if err != nil {
return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err)
log.Debugf("marshal translation content failed: %s %s", file.Name(), err)
continue
}
// add translator use backend translation
if err = myTran.AddTranslator(content, file.Name()); err != nil {
return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err)
log.Debugf("add translator failed: %s %s", file.Name(), err)
continue
}
}
GlobalTrans = myTran.GlobalTrans

View File

@ -11,10 +11,10 @@ import (
"path/filepath"
"strings"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/dir"
"github.com/answerdev/answer/pkg/uid"
"github.com/disintegration/imaging"
@ -40,10 +40,10 @@ var (
".jpg": imaging.JPEG,
".jpeg": imaging.JPEG,
".png": imaging.PNG,
".gif": imaging.GIF,
".tif": imaging.TIFF,
".tiff": imaging.TIFF,
".bmp": imaging.BMP,
//".gif": imaging.GIF,
//".tif": imaging.TIFF,
//".tiff": imaging.TIFF,
//".bmp": imaging.BMP,
}
)
@ -74,13 +74,11 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
@ -147,13 +145,11 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
@ -167,14 +163,12 @@ func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
_, ok := FormatExts[fileExt]
if !ok && fileExt != ".ico" {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
@ -192,6 +186,17 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
src, err := file.Open()
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
defer src.Close()
if !checker.IsSupportedImageFile(src, filepath.Ext(fileSubPath)) {
return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat)
}
url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath)
return url, nil
}

29
pkg/checker/file_type.go Normal file
View File

@ -0,0 +1,29 @@
package checker
import (
"image/jpeg"
"image/png"
"io"
"strings"
)
// IsSupportedImageFile currently answers support image type is `image/jpeg,image/jpg,image/png`
func IsSupportedImageFile(file io.Reader, ext string) bool {
ext = strings.TrimPrefix(ext, ".")
var err error
switch strings.ToUpper(ext) {
case "JPG", "JPEG":
_, err = jpeg.Decode(file)
case "PNG":
_, err = png.Decode(file)
case "ICO":
// TODO: There is currently no good Golang library to parse whether the image is in ico format.
return true
default:
return false
}
if err != nil {
return false
}
return true
}

View File

@ -57,6 +57,8 @@ func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegis
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
reg.Register(ast.KindRawHTML, r.renderRawHTML)
reg.Register(ast.KindLink, r.renderLink)
reg.Register(ast.KindAutoLink, r.renderAutoLink)
}
func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@ -90,6 +92,7 @@ func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte,
}
func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Link)
if entering && r.renderLinkIsUrl(string(n.Destination)) {
_, _ = w.WriteString("<a href=\"")
@ -112,6 +115,31 @@ func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node
return ast.WalkContinue, nil
}
func (r *DangerousHTMLRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.AutoLink)
if !entering || !r.renderLinkIsUrl(string(n.URL(source))) {
return ast.WalkContinue, nil
}
_, _ = w.WriteString(`<a href="`)
url := n.URL(source)
label := n.Label(source)
if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
_, _ = w.WriteString("mailto:")
}
_, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
if n.Attributes() != nil {
_ = w.WriteByte('"')
html.RenderAttributes(w, n, html.LinkAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString(`">`)
}
_, _ = w.Write(util.EscapeHTML(label))
_, _ = w.WriteString(`</a>`)
return ast.WalkContinue, nil
}
func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyUrl string) bool {
return govalidator.IsURL(verifyUrl)
}