mirror of
https://github.com/drone-plugins/drone-manifest.git
synced 2026-06-04 18:24:08 +08:00
Implemented inital version
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
version: '{build}'
|
||||
image: 'Visual Studio 2017'
|
||||
platform: x64
|
||||
|
||||
clone_folder: 'c:\go\src\github.com\drone-plugins\drone-manifest'
|
||||
max_jobs: 1
|
||||
|
||||
environment:
|
||||
DOCKER_USERNAME:
|
||||
secure: '4YzzahbEiMZQJpOCOd1LAw=='
|
||||
DOCKER_PASSWORD:
|
||||
secure: 'VqO/G3Zfslu6zSLdwHKO+Q=='
|
||||
|
||||
install:
|
||||
- ps: |
|
||||
docker version
|
||||
go version
|
||||
|
||||
build_script:
|
||||
- ps: |
|
||||
if ( $env:APPVEYOR_REPO_TAG -eq 'false' ) {
|
||||
go build -ldflags "-X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o drone-manifest.exe
|
||||
} else {
|
||||
$version = $env:APPVEYOR_REPO_TAG_NAME.substring(1)
|
||||
go build -ldflags "-X main.version=$version -X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o drone-manifest.exe
|
||||
}
|
||||
|
||||
docker pull microsoft/nanoserver:10.0.14393.1593
|
||||
docker build -f Dockerfile.windows -t plugins/manifest:windows .
|
||||
|
||||
test_script:
|
||||
- ps: |
|
||||
docker run --rm plugins/manifest:windows --version
|
||||
|
||||
deploy_script:
|
||||
- ps: |
|
||||
$ErrorActionPreference = 'Stop';
|
||||
|
||||
if ( $env:APPVEYOR_PULL_REQUEST_NUMBER ) {
|
||||
Write-Host Nothing to deploy.
|
||||
} else {
|
||||
docker login --username $env:DOCKER_USERNAME --password $env:DOCKER_PASSWORD
|
||||
|
||||
if ( $env:APPVEYOR_REPO_TAG -eq 'true' ) {
|
||||
$major,$minor,$patch = $env:APPVEYOR_REPO_TAG_NAME.substring(1).split('.')
|
||||
|
||||
docker push plugins/manifest:windows
|
||||
|
||||
docker tag plugins/manifest:windows plugins/manifest:$major.$minor.$patch-windows
|
||||
docker push plugins/manifest:$major.$minor.$patch-windows
|
||||
|
||||
docker tag plugins/manifest:windows plugins/manifest:$major.$minor-windows
|
||||
docker push plugins/manifest:$major.$minor-windows
|
||||
|
||||
docker tag plugins/manifest:windows plugins/manifest:$major-windows
|
||||
docker push plugins/manifest:$major-windows
|
||||
} else {
|
||||
if ( $env:APPVEYOR_REPO_BRANCH -eq 'master' ) {
|
||||
docker push plugins/manifest:windows
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!release/
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/github.com/drone-plugins/drone-manifest
|
||||
|
||||
pipeline:
|
||||
test:
|
||||
image: golang:1.9
|
||||
pull: true
|
||||
commands:
|
||||
- go vet
|
||||
- |
|
||||
for PKG in $(go list ./... | grep -v /vendor/); do
|
||||
go test -cover -coverprofile $GOPATH/src/$PKG/coverage.out $PKG
|
||||
done
|
||||
|
||||
build_linux_amd64:
|
||||
image: golang:1.9
|
||||
pull: true
|
||||
group: build
|
||||
environment:
|
||||
- GOOS=linux
|
||||
- GOARCH=amd64
|
||||
- CGO_ENABLED=0
|
||||
commands:
|
||||
- |
|
||||
if test "${DRONE_TAG}" = ""; then
|
||||
go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/amd64/drone-manifest
|
||||
else
|
||||
go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/amd64/drone-manifest
|
||||
fi
|
||||
|
||||
build_linux_i386:
|
||||
image: golang:1.9
|
||||
pull: true
|
||||
group: build
|
||||
environment:
|
||||
- GOOS=linux
|
||||
- GOARCH=386
|
||||
- CGO_ENABLED=0
|
||||
commands:
|
||||
- |
|
||||
if test "${DRONE_TAG}" = ""; then
|
||||
go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/i386/drone-manifest
|
||||
else
|
||||
go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/i386/drone-manifest
|
||||
fi
|
||||
|
||||
build_linux_arm:
|
||||
image: golang:1.9
|
||||
pull: true
|
||||
group: build
|
||||
environment:
|
||||
- GOOS=linux
|
||||
- GOARCH=arm
|
||||
- CGO_ENABLED=0
|
||||
- GOARM=7
|
||||
commands:
|
||||
- |
|
||||
if test "${DRONE_TAG}" = ""; then
|
||||
go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm/drone-manifest
|
||||
else
|
||||
go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm/drone-manifest
|
||||
fi
|
||||
|
||||
build_linux_arm64:
|
||||
image: golang:1.9
|
||||
pull: true
|
||||
group: build
|
||||
environment:
|
||||
- GOOS=linux
|
||||
- GOARCH=arm64
|
||||
- CGO_ENABLED=0
|
||||
commands:
|
||||
- |
|
||||
if test "${DRONE_TAG}" = ""; then
|
||||
go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm64/drone-manifest
|
||||
else
|
||||
go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm64/drone-manifest
|
||||
fi
|
||||
|
||||
publish_linux_amd64:
|
||||
image: plugins/docker:17.05
|
||||
pull: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
group: docker
|
||||
repo: plugins/manifest
|
||||
auto_tag: true
|
||||
auto_tag_suffix: amd64
|
||||
dockerfile: Dockerfile
|
||||
when:
|
||||
event: [ push, tag ]
|
||||
|
||||
publish_linux_i386:
|
||||
image: plugins/docker:17.05
|
||||
pull: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
group: docker
|
||||
repo: plugins/manifest
|
||||
auto_tag: true
|
||||
auto_tag_suffix: i386
|
||||
dockerfile: Dockerfile.i386
|
||||
when:
|
||||
event: [ push, tag ]
|
||||
|
||||
publish_linux_arm:
|
||||
image: plugins/docker:17.05
|
||||
pull: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
group: docker
|
||||
repo: plugins/manifest
|
||||
auto_tag: true
|
||||
auto_tag_suffix: arm
|
||||
dockerfile: Dockerfile.arm
|
||||
when:
|
||||
event: [ push, tag ]
|
||||
|
||||
publish_linux_arm64:
|
||||
image: plugins/docker:17.05
|
||||
pull: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
group: docker
|
||||
repo: plugins/manifest
|
||||
auto_tag: true
|
||||
auto_tag_suffix: arm64
|
||||
dockerfile: Dockerfile.arm64
|
||||
when:
|
||||
event: [ push, tag ]
|
||||
|
||||
manifests:
|
||||
image: plugins/manifest:1
|
||||
pull: true
|
||||
secrets: [ docker_username, docker_password ]
|
||||
spec: manifest.tmpl
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
when:
|
||||
event: [ push, tag ]
|
||||
|
||||
microbadger:
|
||||
image: plugins/webhook:1
|
||||
pull: true
|
||||
secrets: [ webhook_url ]
|
||||
when:
|
||||
status: [ success ]
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
release/
|
||||
coverage.out
|
||||
drone-manifest
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
FROM alpine:3.6 as base
|
||||
|
||||
ENV MANIFEST_VERSION 0.7.0
|
||||
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-linux-amd64
|
||||
|
||||
RUN apk add --no-cache curl && \
|
||||
curl -sSLo /bin/manifest-tool ${MANIFEST_URL} && \
|
||||
chmod +x /bin/manifest-tool
|
||||
|
||||
FROM plugins/base:multiarch
|
||||
|
||||
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" \
|
||||
org.label-schema.name="Drone Manifest" \
|
||||
org.label-schema.vendor="Drone.IO Community" \
|
||||
org.label-schema.schema-version="1.0"
|
||||
|
||||
COPY --from=base /bin/manifest-tool /bin/
|
||||
|
||||
ADD release/linux/amd64/drone-manifest /bin/
|
||||
ENTRYPOINT ["/bin/drone-manifest"]
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM alpine:3.6 as base
|
||||
|
||||
ENV MANIFEST_VERSION 0.7.0
|
||||
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-linux-armv7
|
||||
|
||||
RUN apk add --no-cache curl && \
|
||||
curl -sSLo /bin/manifest-tool ${MANIFEST_URL} && \
|
||||
chmod +x /bin/manifest-tool
|
||||
|
||||
FROM plugins/base:multiarch
|
||||
|
||||
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" \
|
||||
org.label-schema.name="Drone Manifest" \
|
||||
org.label-schema.vendor="Drone.IO Community" \
|
||||
org.label-schema.schema-version="1.0"
|
||||
|
||||
ADD release/linux/arm/drone-manifest /bin/
|
||||
ENTRYPOINT ["/bin/drone-manifest"]
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM alpine:3.6 as base
|
||||
|
||||
ENV MANIFEST_VERSION 0.7.0
|
||||
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-linux-arm64
|
||||
|
||||
RUN apk add --no-cache curl && \
|
||||
curl -sSLo /bin/manifest-tool ${MANIFEST_URL} && \
|
||||
chmod +x /bin/manifest-tool
|
||||
|
||||
FROM plugins/base:multiarch
|
||||
|
||||
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" \
|
||||
org.label-schema.name="Drone Manifest" \
|
||||
org.label-schema.vendor="Drone.IO Community" \
|
||||
org.label-schema.schema-version="1.0"
|
||||
|
||||
ADD release/linux/arm64/drone-manifest /bin/
|
||||
ENTRYPOINT ["/bin/drone-manifest"]
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM alpine:3.6 as base
|
||||
|
||||
ENV MANIFEST_VERSION 0.7.0
|
||||
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-linux-386
|
||||
|
||||
RUN apk add --no-cache curl && \
|
||||
curl -sSLo /bin/manifest-tool ${MANIFEST_URL} && \
|
||||
chmod +x /bin/manifest-tool
|
||||
|
||||
FROM plugins/base:multiarch
|
||||
|
||||
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" \
|
||||
org.label-schema.name="Drone Manifest" \
|
||||
org.label-schema.vendor="Drone.IO Community" \
|
||||
org.label-schema.schema-version="1.0"
|
||||
|
||||
ADD release/linux/i386/drone-manifest /bin/
|
||||
ENTRYPOINT ["/bin/drone-manifest"]
|
||||
@@ -0,0 +1,16 @@
|
||||
# escape=`
|
||||
FROM microsoft/nanoserver:10.0.14393.1593
|
||||
|
||||
ENV MANIFEST_VERSION 0.7.0
|
||||
ENV MANIFEST_URL https://github.com/estesp/manifest-tool/releases/download/v${MANIFEST_VERSION}/manifest-tool-windows-amd64.exe
|
||||
|
||||
RUN powershell -Command $ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'; `
|
||||
Invoke-WebRequest -Uri $env:MANIFEST_URL -OutFile "C:\Windows\System32\manifest-tool.exe"
|
||||
|
||||
LABEL maintainer="Drone.IO Community <drone-dev@googlegroups.com>" `
|
||||
org.label-schema.name="Drone Manifest" `
|
||||
org.label-schema.vendor="Drone.IO Community" `
|
||||
org.label-schema.schema-version="1.0"
|
||||
|
||||
ADD drone-manifest.exe /drone-manifest.exe
|
||||
ENTRYPOINT [ "\\drone-manifest.exe" ]
|
||||
Generated
+33
@@ -0,0 +1,33 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/aymerick/raymond"
|
||||
packages = [".","ast","lexer","parser"]
|
||||
revision = "a2232af10b53ef1ae5a767f5178db3a6c1dab655"
|
||||
version = "v2.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/coreos/go-semver"
|
||||
packages = ["semver"]
|
||||
revision = "8ab6407b697782a06568d4b7f1db25550ec2e4c6"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/urfave/cli"
|
||||
packages = ["."]
|
||||
revision = "75104e932ac2ddb944a6ea19d9f9f26316ff1145"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "a89f1c3cf068c93fd8bf83840a119df2915c502193ca71a2b5ae4b6a6cfa4d70"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
[[constraint]]
|
||||
name = "github.com/aymerick/raymond"
|
||||
version = "2.0.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coreos/go-semver"
|
||||
version = "0.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/urfave/cli"
|
||||
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -1,2 +1,37 @@
|
||||
# drone-manifest-tool
|
||||
Drone plugin to push Docker manifests
|
||||
# drone-manifest
|
||||
|
||||
[](http://beta.drone.io/drone-plugins/drone-manifest)
|
||||
[](https://discourse.drone.io)
|
||||
[](https://stackoverflow.com/questions/tagged/drone.io)
|
||||
[](http://godoc.org/github.com/drone-plugins/drone-manifest)
|
||||
[](https://goreportcard.com/report/github.com/drone-plugins/drone-manifest)
|
||||
[](https://microbadger.com/images/plugins/manifest "Get your own image badge on microbadger.com")
|
||||
|
||||
Drone plugin to push Docker manifest to a registry for multi-architecture mappings. For the usage information and a listing of the available options please take a look at [the docs](http://plugins.drone.io/drone-plugins/drone-manifest/).
|
||||
|
||||
## Build
|
||||
|
||||
Build the binary with the following commands:
|
||||
|
||||
```
|
||||
go build
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Build the Docker image with the following commands:
|
||||
|
||||
```
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -tags netgo -o release/linux/amd64/drone-manifest
|
||||
docker build --rm -t plugins/manifest .
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
docker run --rm \
|
||||
-e PLUGIN_PLATFORMS=linux/amd64,linux/arm,linux/arm64 \
|
||||
-e PLUGIN_TEMPLATE=organization/project-ARCH:1.0.0 \
|
||||
-e PLUGIN_TARGET=organization/project:1.0.0 \
|
||||
plugins/manifest
|
||||
```
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
username string
|
||||
password string
|
||||
spec string
|
||||
platforms []string
|
||||
target string
|
||||
template string
|
||||
path string
|
||||
ignoreMissing bool
|
||||
}
|
||||
|
||||
func New(opts ...Option) *Command {
|
||||
c := &Command{}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Command) Exec() error {
|
||||
args := []string{}
|
||||
|
||||
if c.username != "" {
|
||||
args = append(args, fmt.Sprintf("--username=%s", c.username))
|
||||
}
|
||||
|
||||
if c.password != "" {
|
||||
args = append(args, fmt.Sprintf("--password=%s", c.password))
|
||||
}
|
||||
|
||||
args = append(args, "push")
|
||||
|
||||
if c.spec != "" {
|
||||
tmpfile, err := ioutil.TempFile(c.path, "manifest-")
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create tempfile")
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.Write([]byte(c.spec)); err != nil {
|
||||
return errors.Wrap(err, "failed to write tempfile")
|
||||
}
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
return errors.Wrap(err, "failed to close temp file")
|
||||
}
|
||||
|
||||
args = append(args, "from-spec")
|
||||
args = append(args, tmpfile.Name())
|
||||
} else {
|
||||
args = append(args, "from-args")
|
||||
|
||||
if len(c.platforms) != 0 {
|
||||
args = append(args, fmt.Sprintf("--platforms=%s", strings.Join(c.platforms, ",")))
|
||||
}
|
||||
|
||||
if c.target != "" {
|
||||
args = append(args, fmt.Sprintf("--target=%s", c.target))
|
||||
}
|
||||
|
||||
if c.template != "" {
|
||||
args = append(args, fmt.Sprintf("--template=%s", c.template))
|
||||
}
|
||||
}
|
||||
|
||||
if c.ignoreMissing {
|
||||
args = append(args, "--ignore-missing")
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
"manifest-tool",
|
||||
args...,
|
||||
)
|
||||
|
||||
buf := bytes.NewBufferString("")
|
||||
cmd.Stdout = io.MultiWriter(os.Stdout, buf)
|
||||
cmd.Stderr = io.MultiWriter(os.Stderr, buf)
|
||||
|
||||
if c.path != "" {
|
||||
cmd.Dir = c.path
|
||||
}
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package command
|
||||
|
||||
type Option func(*Command)
|
||||
|
||||
func WithUsername(val string) Option {
|
||||
return func(t *Command) {
|
||||
t.username = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithPassword(val string) Option {
|
||||
return func(t *Command) {
|
||||
t.password = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithSpec(val string) Option {
|
||||
return func(t *Command) {
|
||||
t.spec = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithPlatforms(val []string) Option {
|
||||
return func(t *Command) {
|
||||
t.platforms = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithTarget(val string) Option {
|
||||
return func(t *Command) {
|
||||
t.target = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithTemplate(val string) Option {
|
||||
return func(t *Command) {
|
||||
t.template = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithPath(val string) Option {
|
||||
return func(t *Command) {
|
||||
t.path = val
|
||||
}
|
||||
}
|
||||
|
||||
func IgnoreMissing() Option {
|
||||
return func(t *Command) {
|
||||
t.ignoreMissing = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/drone-plugins/drone-manifest/tagging"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "0.0.0"
|
||||
build = "0"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "manifest plugin"
|
||||
app.Usage = "manifest plugin"
|
||||
app.Version = fmt.Sprintf("%s+%s", version, build)
|
||||
app.Action = run
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "username",
|
||||
Usage: "username for registry",
|
||||
EnvVar: "PLUGIN_USERNAME,MANIFEST_USERNAME,DOCKER_USERNAME",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "password for registry",
|
||||
EnvVar: "PLUGIN_PASSWORD,MANIFEST_PASSWORD,DOCKER_PASSWORD",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "platforms",
|
||||
Usage: "platforms for manifests",
|
||||
EnvVar: "PLUGIN_PLATFORMS",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "target",
|
||||
Usage: "target for manifests",
|
||||
EnvVar: "PLUGIN_TARGET",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "template",
|
||||
Usage: "template for manifests",
|
||||
EnvVar: "PLUGIN_TEMPLATE",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "spec",
|
||||
Usage: "path to manifest spec",
|
||||
EnvVar: "PLUGIN_SPEC",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "ignore-missing",
|
||||
Usage: "ignore missing images",
|
||||
EnvVar: "PLUGIN_IGNORE_MISSING",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "tags",
|
||||
Usage: "list of additional tags",
|
||||
Value: &cli.StringSlice{},
|
||||
EnvVar: "PLUGIN_TAG,PLUGIN_TAGS",
|
||||
FilePath: ".tags",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "tags.auto",
|
||||
Usage: "automatically build tags",
|
||||
EnvVar: "PLUGIN_DEFAULT_TAGS,PLUGIN_AUTO_TAG",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "path",
|
||||
Usage: "git clone path",
|
||||
EnvVar: "DRONE_WORKSPACE",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "repo.owner",
|
||||
Usage: "repository owner",
|
||||
EnvVar: "DRONE_REPO_OWNER",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "repo.name",
|
||||
Usage: "repository name",
|
||||
EnvVar: "DRONE_REPO_NAME",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "repo.branch",
|
||||
Usage: "repository default branch",
|
||||
EnvVar: "DRONE_REPO_BRANCH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "commit.sha",
|
||||
Usage: "git commit sha",
|
||||
EnvVar: "DRONE_COMMIT_SHA",
|
||||
Value: "00000000",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "commit.ref",
|
||||
Usage: "git commit ref",
|
||||
EnvVar: "DRONE_COMMIT_REF",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "commit.branch",
|
||||
Value: "master",
|
||||
Usage: "git commit branch",
|
||||
EnvVar: "DRONE_COMMIT_BRANCH",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "commit.pull",
|
||||
Usage: "git pull request",
|
||||
EnvVar: "DRONE_PULL_REQUEST",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "build.event",
|
||||
Value: "push",
|
||||
Usage: "build event",
|
||||
EnvVar: "DRONE_BUILD_EVENT",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "build.number",
|
||||
Usage: "build number",
|
||||
EnvVar: "DRONE_BUILD_NUMBER",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "build.status",
|
||||
Usage: "build status",
|
||||
Value: "success",
|
||||
EnvVar: "DRONE_BUILD_STATUS",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "build.link",
|
||||
Usage: "build link",
|
||||
EnvVar: "DRONE_BUILD_LINK",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "build.started",
|
||||
Usage: "build started",
|
||||
EnvVar: "DRONE_BUILD_STARTED",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "build.created",
|
||||
Usage: "build created",
|
||||
EnvVar: "DRONE_BUILD_CREATED",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "build.tag",
|
||||
Usage: "build tag",
|
||||
EnvVar: "DRONE_TAG",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "job.started",
|
||||
Usage: "job started",
|
||||
EnvVar: "DRONE_JOB_STARTED",
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(c *cli.Context) error {
|
||||
plugin := Plugin{
|
||||
Repo: Repo{
|
||||
Owner: c.String("repo.owner"),
|
||||
Name: c.String("repo.name"),
|
||||
Branch: c.String("repo.branch"),
|
||||
},
|
||||
Build: Build{
|
||||
Path: c.String("path"),
|
||||
Tag: c.String("build.tag"),
|
||||
Number: c.Int("build.number"),
|
||||
Event: c.String("build.event"),
|
||||
Status: c.String("build.status"),
|
||||
Commit: c.String("commit.sha"),
|
||||
Ref: c.String("commit.ref"),
|
||||
Branch: c.String("commit.branch"),
|
||||
Pull: c.String("commit.pull"),
|
||||
Started: c.Int64("build.started"),
|
||||
Created: c.Int64("build.created"),
|
||||
Tags: c.StringSlice("tags"),
|
||||
},
|
||||
Job: Job{
|
||||
Started: c.Int64("job.started"),
|
||||
},
|
||||
Auto: Auto{
|
||||
Tags: []string{"1.0", "1"},
|
||||
},
|
||||
Config: Config{
|
||||
Username: c.String("username"),
|
||||
Password: c.String("password"),
|
||||
Platforms: c.StringSlice("platforms"),
|
||||
Target: c.String("target"),
|
||||
Template: c.String("template"),
|
||||
Spec: c.String("spec"),
|
||||
IgnoreMissing: c.Bool("ignore-missing"),
|
||||
},
|
||||
}
|
||||
|
||||
if c.Bool("tags.auto") {
|
||||
if tagging.UseDefaultTag(c.String("commit.ref"), c.String("repo.branch")) {
|
||||
plugin.Build.Tags = tagging.DefaultTags(c.String("commit.ref"))
|
||||
} else {
|
||||
log.Printf("skipping automated tags for %s", c.String("commit.ref"))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return plugin.Exec()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
-
|
||||
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}i386
|
||||
platform:
|
||||
architecture: 386
|
||||
os: linux
|
||||
-
|
||||
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
-
|
||||
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
-
|
||||
image: plugins/manifest:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}windows
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: windows
|
||||
@@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/drone-plugins/drone-manifest/command"
|
||||
)
|
||||
|
||||
type (
|
||||
Repo struct {
|
||||
Owner string
|
||||
Name string
|
||||
Branch string
|
||||
}
|
||||
|
||||
Build struct {
|
||||
Path string
|
||||
Tag string
|
||||
Event string
|
||||
Number int
|
||||
Commit string
|
||||
Ref string
|
||||
Branch string
|
||||
Author string
|
||||
Pull string
|
||||
Message string
|
||||
DeployTo string
|
||||
Status string
|
||||
Link string
|
||||
Started int64
|
||||
Created int64
|
||||
Tags []string
|
||||
}
|
||||
|
||||
Job struct {
|
||||
Started int64
|
||||
}
|
||||
|
||||
Auto struct {
|
||||
Tags []string
|
||||
}
|
||||
|
||||
Config struct {
|
||||
Username string
|
||||
Password string
|
||||
Platforms []string
|
||||
Target string
|
||||
Template string
|
||||
Spec string
|
||||
IgnoreMissing bool
|
||||
}
|
||||
|
||||
Plugin struct {
|
||||
Repo Repo
|
||||
Build Build
|
||||
Job Job
|
||||
Auto Auto
|
||||
Config Config
|
||||
}
|
||||
)
|
||||
|
||||
func (p *Plugin) Exec() error {
|
||||
opts := make([]command.Option, 0)
|
||||
|
||||
if p.Config.Username == "" {
|
||||
return errors.New("you must provide a username")
|
||||
} else {
|
||||
opts = append(opts, command.WithUsername(p.Config.Username))
|
||||
}
|
||||
|
||||
if p.Config.Password == "" {
|
||||
return errors.New("you must provide a password")
|
||||
} else {
|
||||
opts = append(opts, command.WithPassword(p.Config.Password))
|
||||
}
|
||||
|
||||
if p.Config.Spec != "" {
|
||||
spec, err := RenderTrim(p.Config.Spec, p)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts = append(opts, command.WithSpec(spec))
|
||||
|
||||
log.Printf(
|
||||
"pushing by spec",
|
||||
)
|
||||
} else {
|
||||
if len(p.Config.Platforms) == 0 {
|
||||
return errors.New("you must provide platforms")
|
||||
} else {
|
||||
opts = append(opts, command.WithPlatforms(p.Config.Platforms))
|
||||
}
|
||||
|
||||
if p.Config.Target == "" {
|
||||
return errors.New("you must provide a target")
|
||||
} else {
|
||||
opts = append(opts, command.WithTarget(p.Config.Target))
|
||||
}
|
||||
|
||||
if p.Config.Template == "" {
|
||||
return errors.New("you must provide a template")
|
||||
} else {
|
||||
opts = append(opts, command.WithTemplate(p.Config.Template))
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"pushing %s to %s for %s",
|
||||
p.Config.Template,
|
||||
p.Config.Target,
|
||||
strings.Join(p.Config.Platforms, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
if p.Config.IgnoreMissing {
|
||||
opts = append(opts, command.IgnoreMissing())
|
||||
}
|
||||
|
||||
if p.Build.Path != "" {
|
||||
opts = append(opts, command.WithPath(p.Build.Path))
|
||||
}
|
||||
|
||||
return command.New(opts...).Exec()
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package tagging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
// DefaultTags returns a set of default suggested tags.
|
||||
func DefaultTags(ref string) []string {
|
||||
if !strings.HasPrefix(ref, "refs/tags/") {
|
||||
return []string{"latest"}
|
||||
}
|
||||
|
||||
v := stripTagPrefix(ref)
|
||||
|
||||
version, err := semver.NewVersion(v)
|
||||
|
||||
if err != nil {
|
||||
return []string{"latest"}
|
||||
}
|
||||
|
||||
if version.PreRelease != "" || version.Metadata != "" {
|
||||
return []string{
|
||||
version.String(),
|
||||
}
|
||||
}
|
||||
|
||||
if version.Major == 0 {
|
||||
return []string{
|
||||
fmt.Sprintf("%d.%d", version.Major, version.Minor),
|
||||
fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Patch),
|
||||
}
|
||||
}
|
||||
|
||||
return []string{
|
||||
fmt.Sprint(version.Major),
|
||||
fmt.Sprintf("%d.%d", version.Major, version.Minor),
|
||||
fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Patch),
|
||||
}
|
||||
}
|
||||
|
||||
// UseDefaultTag to restrict latest tag for default branch.
|
||||
func UseDefaultTag(ref, defaultBranch string) bool {
|
||||
if strings.HasPrefix(ref, "refs/tags/") {
|
||||
return true
|
||||
}
|
||||
|
||||
if stripHeadPrefix(ref) == defaultBranch {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// stripHeadPrefix just strips the ref heads prefix.
|
||||
func stripHeadPrefix(ref string) string {
|
||||
return strings.TrimPrefix(ref, "refs/heads/")
|
||||
}
|
||||
|
||||
// stripTagPrefix just strips the ref tags prefix.
|
||||
func stripTagPrefix(ref string) string {
|
||||
ref = strings.TrimPrefix(ref, "refs/tags/")
|
||||
ref = strings.TrimPrefix(ref, "v")
|
||||
|
||||
return ref
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package tagging
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultTags(t *testing.T) {
|
||||
var tests = []struct {
|
||||
Before string
|
||||
After []string
|
||||
}{
|
||||
// valid combinations
|
||||
{"", []string{"latest"}},
|
||||
{"refs/heads/master", []string{"latest"}},
|
||||
{"refs/tags/0.9.0", []string{"0.9", "0.9.0"}},
|
||||
{"refs/tags/1.0.0", []string{"1", "1.0", "1.0.0"}},
|
||||
{"refs/tags/v1.0.0", []string{"1", "1.0", "1.0.0"}},
|
||||
{"refs/tags/v1.0.0-alpha.1", []string{"1.0.0-alpha.1"}},
|
||||
|
||||
// malformed or errors
|
||||
{"refs/tags/x1.0.0", []string{"latest"}},
|
||||
{"v1.0.0", []string{"latest"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got, want := DefaultTags(test.Before), test.After
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Got tag %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDefaultTag(t *testing.T) {
|
||||
type args struct {
|
||||
ref string
|
||||
defaultBranch string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "latest tag for default branch",
|
||||
args: args{
|
||||
ref: "refs/heads/master",
|
||||
defaultBranch: "master",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "build from tags",
|
||||
args: args{
|
||||
ref: "refs/tags/v1.0.0",
|
||||
defaultBranch: "master",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "skip build for not default branch",
|
||||
args: args{
|
||||
ref: "refs/heads/develop",
|
||||
defaultBranch: "master",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := UseDefaultTag(tt.args.ref, tt.args.defaultBranch); got != tt.want {
|
||||
t.Errorf("%q. UseDefaultTag() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_stripHeadPrefix(t *testing.T) {
|
||||
type args struct {
|
||||
ref string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
args: args{
|
||||
ref: "refs/heads/master",
|
||||
},
|
||||
want: "master",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := stripHeadPrefix(tt.args.ref); got != tt.want {
|
||||
t.Errorf("stripHeadPrefix() = %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_stripTagPrefix(t *testing.T) {
|
||||
var tests = []struct {
|
||||
Before string
|
||||
After string
|
||||
}{
|
||||
{"refs/tags/1.0.0", "1.0.0"},
|
||||
{"refs/tags/v1.0.0", "1.0.0"},
|
||||
{"v1.0.0", "1.0.0"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got, want := stripTagPrefix(test.Before), test.After
|
||||
|
||||
if got != want {
|
||||
t.Errorf("Got tag %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
func init() {
|
||||
raymond.RegisterHelpers(funcs)
|
||||
}
|
||||
|
||||
// Render parses and executes a template, returning the results in string format.
|
||||
func Render(template string, payload interface{}) (s string, err error) {
|
||||
u, err := url.Parse(template)
|
||||
if err == nil {
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
res, err := http.Get(template)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
out, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
template = string(out)
|
||||
|
||||
default:
|
||||
out, err := ioutil.ReadFile(u.Path)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
template = string(out)
|
||||
}
|
||||
}
|
||||
|
||||
return raymond.Render(template, payload)
|
||||
}
|
||||
|
||||
// RenderTrim parses and executes a template, returning the results in string
|
||||
// format. The result is trimmed to remove left and right padding and newlines
|
||||
// that may be added unintentially in the template markup.
|
||||
func RenderTrim(template string, playload interface{}) (string, error) {
|
||||
out, err := Render(template, playload)
|
||||
return strings.Trim(out, " \n"), err
|
||||
}
|
||||
|
||||
var funcs = map[string]interface{}{
|
||||
"uppercase": strings.ToUpper,
|
||||
"lowercase": strings.ToLower,
|
||||
"trimPrefix": strings.TrimPrefix,
|
||||
"trimSuffix": strings.TrimSuffix,
|
||||
"quote": strconv.Quote,
|
||||
"join": strings.Join,
|
||||
"uppercasefirst": uppercaseFirst,
|
||||
"duration": toDuration,
|
||||
"datetime": toDatetime,
|
||||
"success": isSuccess,
|
||||
"failure": isFailure,
|
||||
"truncate": truncate,
|
||||
"urlencode": urlencode,
|
||||
"since": since,
|
||||
}
|
||||
|
||||
func uppercaseFirst(s string) string {
|
||||
a := []rune(s)
|
||||
a[0] = unicode.ToUpper(a[0])
|
||||
s = string(a)
|
||||
return s
|
||||
}
|
||||
|
||||
func toDuration(started, finished float64) string {
|
||||
return fmt.Sprintln(time.Duration(finished-started) * time.Second)
|
||||
}
|
||||
|
||||
func toDatetime(timestamp float64, layout, zone string) string {
|
||||
if len(zone) == 0 {
|
||||
return time.Unix(int64(timestamp), 0).Format(layout)
|
||||
}
|
||||
loc, err := time.LoadLocation(zone)
|
||||
if err != nil {
|
||||
return time.Unix(int64(timestamp), 0).Local().Format(layout)
|
||||
}
|
||||
return time.Unix(int64(timestamp), 0).In(loc).Format(layout)
|
||||
}
|
||||
|
||||
func isSuccess(conditional bool, options *raymond.Options) string {
|
||||
if !conditional {
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
switch options.ParamStr(0) {
|
||||
case "success":
|
||||
return options.Fn()
|
||||
default:
|
||||
return options.Inverse()
|
||||
}
|
||||
}
|
||||
|
||||
func isFailure(conditional bool, options *raymond.Options) string {
|
||||
if !conditional {
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
switch options.ParamStr(0) {
|
||||
case "failure", "error", "killed":
|
||||
return options.Fn()
|
||||
default:
|
||||
return options.Inverse()
|
||||
}
|
||||
}
|
||||
|
||||
func truncate(s string, len int) string {
|
||||
if utf8.RuneCountInString(s) <= len {
|
||||
return s
|
||||
}
|
||||
runes := []rune(s)
|
||||
return string(runes[:len])
|
||||
}
|
||||
|
||||
func urlencode(options *raymond.Options) string {
|
||||
return url.QueryEscape(options.Fn())
|
||||
}
|
||||
|
||||
func since(start int64) string {
|
||||
// NOTE: not using `time.Since()` because the fractional second component
|
||||
// will give us something like "40m12.917523438s" vs "40m12s". We lose
|
||||
// some precision, but the format is much more readable.
|
||||
now := time.Unix(time.Now().Unix(), 0)
|
||||
return fmt.Sprintln(now.Sub(time.Unix(start, 0)))
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
[submodule "mustache"]
|
||||
path = mustache
|
||||
url = git://github.com/mustache/spec.git
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.3
|
||||
- tip
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Benchmarks
|
||||
|
||||
Hardware: MacBookPro11,1 - Intel Core i5 - 2,6 GHz - 8 Go RAM
|
||||
|
||||
With:
|
||||
|
||||
- handlebars.js #8cba84df119c317fcebc49fb285518542ca9c2d0
|
||||
- raymond #7bbaaf50ed03c96b56687d7fa6c6e04e02375a98
|
||||
|
||||
|
||||
## handlebars.js (ops/ms)
|
||||
|
||||
arguments 198 ±4 (5)
|
||||
array-each 568 ±23 (5)
|
||||
array-mustache 522 ±18 (4)
|
||||
complex 71 ±7 (3)
|
||||
data 67 ±2 (3)
|
||||
depth-1 47 ±2 (3)
|
||||
depth-2 14 ±1 (2)
|
||||
object-mustache 1099 ±47 (5)
|
||||
object 907 ±58 (4)
|
||||
partial-recursion 46 ±3 (4)
|
||||
partial 68 ±3 (3)
|
||||
paths 1650 ±50 (3)
|
||||
string 2552 ±157 (3)
|
||||
subexpression 141 ±2 (4)
|
||||
variables 2671 ±83 (4)
|
||||
|
||||
|
||||
## raymond
|
||||
|
||||
BenchmarkArguments 200000 6642 ns/op 151 ops/ms
|
||||
BenchmarkArrayEach 100000 19584 ns/op 51 ops/ms
|
||||
BenchmarkArrayMustache 100000 17305 ns/op 58 ops/ms
|
||||
BenchmarkComplex 30000 50270 ns/op 20 ops/ms
|
||||
BenchmarkData 50000 25551 ns/op 39 ops/ms
|
||||
BenchmarkDepth1 100000 20162 ns/op 50 ops/ms
|
||||
BenchmarkDepth2 30000 47782 ns/op 21 ops/ms
|
||||
BenchmarkObjectMustache 200000 7668 ns/op 130 ops/ms
|
||||
BenchmarkObject 200000 8843 ns/op 113 ops/ms
|
||||
BenchmarkPartialRecursion 50000 23139 ns/op 43 ops/ms
|
||||
BenchmarkPartial 50000 31015 ns/op 32 ops/ms
|
||||
BenchmarkPath 200000 8997 ns/op 111 ops/ms
|
||||
BenchmarkString 1000000 1879 ns/op 532 ops/ms
|
||||
BenchmarkSubExpression 300000 4935 ns/op 203 ops/ms
|
||||
BenchmarkVariables 200000 6478 ns/op 154 ops/ms
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Raymond Changelog
|
||||
|
||||
### Raymond 2.0.1 _(June 01, 2016)_
|
||||
|
||||
- [BUGFIX] Removes data races [#3](https://github.com/aymerick/raymond/issues/3) - Thanks [@markbates](https://github.com/markbates)
|
||||
|
||||
### Raymond 2.0.0 _(May 01, 2016)_
|
||||
|
||||
- [BUGFIX] Fixes passing of context in helper options [#2](https://github.com/aymerick/raymond/issues/2) - Thanks [@GhostRussia](https://github.com/GhostRussia)
|
||||
- [BREAKING] Renames and unexports constants:
|
||||
|
||||
- `handlebars.DUMP_TPL`
|
||||
- `lexer.ESCAPED_ESCAPED_OPEN_MUSTACHE`
|
||||
- `lexer.ESCAPED_OPEN_MUSTACHE`
|
||||
- `lexer.OPEN_MUSTACHE`
|
||||
- `lexer.CLOSE_MUSTACHE`
|
||||
- `lexer.CLOSE_STRIP_MUSTACHE`
|
||||
- `lexer.CLOSE_UNESCAPED_STRIP_MUSTACHE`
|
||||
- `lexer.DUMP_TOKEN_POS`
|
||||
- `lexer.DUMP_ALL_TOKENS_VAL`
|
||||
|
||||
|
||||
### Raymond 1.1.0 _(June 15, 2015)_
|
||||
|
||||
- Permits templates references with lowercase versions of struct fields.
|
||||
- Adds `ParseFile()` function.
|
||||
- Adds `RegisterPartialFile()`, `RegisterPartialFiles()` and `Clone()` methods on `Template`.
|
||||
- Helpers can now be struct methods.
|
||||
- Ensures safe concurrent access to helpers and partials.
|
||||
|
||||
### Raymond 1.0.0 _(June 09, 2015)_
|
||||
|
||||
- This is the first release. Raymond supports almost all handlebars features. See https://github.com/aymerick/raymond#limitations for a list of differences with the javascript implementation.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Aymerick JEHANNE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
+1382
File diff suppressed because it is too large
Load Diff
+1
@@ -0,0 +1 @@
|
||||
2.0.1
|
||||
+785
@@ -0,0 +1,785 @@
|
||||
// Package ast provides structures to represent a handlebars Abstract Syntax Tree, and a Visitor interface to visit that tree.
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// References:
|
||||
// - https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/ast.js
|
||||
// - https://github.com/wycats/handlebars.js/blob/master/docs/compiler-api.md
|
||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/node.go
|
||||
|
||||
// Node is an element in the AST.
|
||||
type Node interface {
|
||||
// node type
|
||||
Type() NodeType
|
||||
|
||||
// location of node in original input string
|
||||
Location() Loc
|
||||
|
||||
// string representation, used for debugging
|
||||
String() string
|
||||
|
||||
// accepts visitor
|
||||
Accept(Visitor) interface{}
|
||||
}
|
||||
|
||||
// Visitor is the interface to visit an AST.
|
||||
type Visitor interface {
|
||||
VisitProgram(*Program) interface{}
|
||||
|
||||
// statements
|
||||
VisitMustache(*MustacheStatement) interface{}
|
||||
VisitBlock(*BlockStatement) interface{}
|
||||
VisitPartial(*PartialStatement) interface{}
|
||||
VisitContent(*ContentStatement) interface{}
|
||||
VisitComment(*CommentStatement) interface{}
|
||||
|
||||
// expressions
|
||||
VisitExpression(*Expression) interface{}
|
||||
VisitSubExpression(*SubExpression) interface{}
|
||||
VisitPath(*PathExpression) interface{}
|
||||
|
||||
// literals
|
||||
VisitString(*StringLiteral) interface{}
|
||||
VisitBoolean(*BooleanLiteral) interface{}
|
||||
VisitNumber(*NumberLiteral) interface{}
|
||||
|
||||
// miscellaneous
|
||||
VisitHash(*Hash) interface{}
|
||||
VisitHashPair(*HashPair) interface{}
|
||||
}
|
||||
|
||||
// NodeType represents an AST Node type.
|
||||
type NodeType int
|
||||
|
||||
// Type returns itself, and permits struct includers to satisfy that part of Node interface.
|
||||
func (t NodeType) Type() NodeType {
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
// NodeProgram is the program node
|
||||
NodeProgram NodeType = iota
|
||||
|
||||
// NodeMustache is the mustache statement node
|
||||
NodeMustache
|
||||
|
||||
// NodeBlock is the block statement node
|
||||
NodeBlock
|
||||
|
||||
// NodePartial is the partial statement node
|
||||
NodePartial
|
||||
|
||||
// NodeContent is the content statement node
|
||||
NodeContent
|
||||
|
||||
// NodeComment is the comment statement node
|
||||
NodeComment
|
||||
|
||||
// NodeExpression is the expression node
|
||||
NodeExpression
|
||||
|
||||
// NodeSubExpression is the subexpression node
|
||||
NodeSubExpression
|
||||
|
||||
// NodePath is the expression path node
|
||||
NodePath
|
||||
|
||||
// NodeBoolean is the literal boolean node
|
||||
NodeBoolean
|
||||
|
||||
// NodeNumber is the literal number node
|
||||
NodeNumber
|
||||
|
||||
// NodeString is the literal string node
|
||||
NodeString
|
||||
|
||||
// NodeHash is the hash node
|
||||
NodeHash
|
||||
|
||||
// NodeHashPair is the hash pair node
|
||||
NodeHashPair
|
||||
)
|
||||
|
||||
// Loc represents the position of a parsed node in source file.
|
||||
type Loc struct {
|
||||
Pos int // Byte position
|
||||
Line int // Line number
|
||||
}
|
||||
|
||||
// Location returns itself, and permits struct includers to satisfy that part of Node interface.
|
||||
func (l Loc) Location() Loc {
|
||||
return l
|
||||
}
|
||||
|
||||
// Strip describes node whitespace management.
|
||||
type Strip struct {
|
||||
Open bool
|
||||
Close bool
|
||||
|
||||
OpenStandalone bool
|
||||
CloseStandalone bool
|
||||
InlineStandalone bool
|
||||
}
|
||||
|
||||
// NewStrip instanciates a Strip for given open and close mustaches.
|
||||
func NewStrip(openStr, closeStr string) *Strip {
|
||||
return &Strip{
|
||||
Open: (len(openStr) > 2) && openStr[2] == '~',
|
||||
Close: (len(closeStr) > 2) && closeStr[len(closeStr)-3] == '~',
|
||||
}
|
||||
}
|
||||
|
||||
// NewStripForStr instanciates a Strip for given tag.
|
||||
func NewStripForStr(str string) *Strip {
|
||||
return &Strip{
|
||||
Open: (len(str) > 2) && str[2] == '~',
|
||||
Close: (len(str) > 2) && str[len(str)-3] == '~',
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (s *Strip) String() string {
|
||||
return fmt.Sprintf("Open: %t, Close: %t, OpenStandalone: %t, CloseStandalone: %t, InlineStandalone: %t", s.Open, s.Close, s.OpenStandalone, s.CloseStandalone, s.InlineStandalone)
|
||||
}
|
||||
|
||||
//
|
||||
// Program
|
||||
//
|
||||
|
||||
// Program represents a program node.
|
||||
type Program struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Body []Node // [ Statement ... ]
|
||||
BlockParams []string
|
||||
Chained bool
|
||||
|
||||
// whitespace management
|
||||
Strip *Strip
|
||||
}
|
||||
|
||||
// NewProgram instanciates a new program node.
|
||||
func NewProgram(pos int, line int) *Program {
|
||||
return &Program{
|
||||
NodeType: NodeProgram,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *Program) String() string {
|
||||
return fmt.Sprintf("Program{Pos: %d}", node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *Program) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitProgram(node)
|
||||
}
|
||||
|
||||
// AddStatement adds given statement to program.
|
||||
func (node *Program) AddStatement(statement Node) {
|
||||
node.Body = append(node.Body, statement)
|
||||
}
|
||||
|
||||
//
|
||||
// Mustache Statement
|
||||
//
|
||||
|
||||
// MustacheStatement represents a mustache node.
|
||||
type MustacheStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Unescaped bool
|
||||
Expression *Expression
|
||||
|
||||
// whitespace management
|
||||
Strip *Strip
|
||||
}
|
||||
|
||||
// NewMustacheStatement instanciates a new mustache node.
|
||||
func NewMustacheStatement(pos int, line int, unescaped bool) *MustacheStatement {
|
||||
return &MustacheStatement{
|
||||
NodeType: NodeMustache,
|
||||
Loc: Loc{pos, line},
|
||||
Unescaped: unescaped,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *MustacheStatement) String() string {
|
||||
return fmt.Sprintf("Mustache{Pos: %d}", node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *MustacheStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitMustache(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Block Statement
|
||||
//
|
||||
|
||||
// BlockStatement represents a block node.
|
||||
type BlockStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Expression *Expression
|
||||
|
||||
Program *Program
|
||||
Inverse *Program
|
||||
|
||||
// whitespace management
|
||||
OpenStrip *Strip
|
||||
InverseStrip *Strip
|
||||
CloseStrip *Strip
|
||||
}
|
||||
|
||||
// NewBlockStatement instanciates a new block node.
|
||||
func NewBlockStatement(pos int, line int) *BlockStatement {
|
||||
return &BlockStatement{
|
||||
NodeType: NodeBlock,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *BlockStatement) String() string {
|
||||
return fmt.Sprintf("Block{Pos: %d}", node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *BlockStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitBlock(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Partial Statement
|
||||
//
|
||||
|
||||
// PartialStatement represents a partial node.
|
||||
type PartialStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Name Node // PathExpression | SubExpression
|
||||
Params []Node // [ Expression ... ]
|
||||
Hash *Hash
|
||||
|
||||
// whitespace management
|
||||
Strip *Strip
|
||||
Indent string
|
||||
}
|
||||
|
||||
// NewPartialStatement instanciates a new partial node.
|
||||
func NewPartialStatement(pos int, line int) *PartialStatement {
|
||||
return &PartialStatement{
|
||||
NodeType: NodePartial,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *PartialStatement) String() string {
|
||||
return fmt.Sprintf("Partial{Name:%s, Pos:%d}", node.Name, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *PartialStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitPartial(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Content Statement
|
||||
//
|
||||
|
||||
// ContentStatement represents a content node.
|
||||
type ContentStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value string
|
||||
Original string
|
||||
|
||||
// whitespace management
|
||||
RightStripped bool
|
||||
LeftStripped bool
|
||||
}
|
||||
|
||||
// NewContentStatement instanciates a new content node.
|
||||
func NewContentStatement(pos int, line int, val string) *ContentStatement {
|
||||
return &ContentStatement{
|
||||
NodeType: NodeContent,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
Original: val,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *ContentStatement) String() string {
|
||||
return fmt.Sprintf("Content{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *ContentStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitContent(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Comment Statement
|
||||
//
|
||||
|
||||
// CommentStatement represents a comment node.
|
||||
type CommentStatement struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value string
|
||||
|
||||
// whitespace management
|
||||
Strip *Strip
|
||||
}
|
||||
|
||||
// NewCommentStatement instanciates a new comment node.
|
||||
func NewCommentStatement(pos int, line int, val string) *CommentStatement {
|
||||
return &CommentStatement{
|
||||
NodeType: NodeComment,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *CommentStatement) String() string {
|
||||
return fmt.Sprintf("Comment{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *CommentStatement) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitComment(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Expression
|
||||
//
|
||||
|
||||
// Expression represents an expression node.
|
||||
type Expression struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Path Node // PathExpression | StringLiteral | BooleanLiteral | NumberLiteral
|
||||
Params []Node // [ Expression ... ]
|
||||
Hash *Hash
|
||||
}
|
||||
|
||||
// NewExpression instanciates a new expression node.
|
||||
func NewExpression(pos int, line int) *Expression {
|
||||
return &Expression{
|
||||
NodeType: NodeExpression,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *Expression) String() string {
|
||||
return fmt.Sprintf("Expr{Path:%s, Pos:%d}", node.Path, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *Expression) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitExpression(node)
|
||||
}
|
||||
|
||||
// HelperName returns helper name, or an empty string if this expression can't be a helper.
|
||||
func (node *Expression) HelperName() string {
|
||||
path, ok := node.Path.(*PathExpression)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
if path.Data || (len(path.Parts) != 1) || (path.Depth > 0) || path.Scoped {
|
||||
return ""
|
||||
}
|
||||
|
||||
return path.Parts[0]
|
||||
}
|
||||
|
||||
// FieldPath returns path expression representing a field path, or nil if this is not a field path.
|
||||
func (node *Expression) FieldPath() *PathExpression {
|
||||
path, ok := node.Path.(*PathExpression)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// LiteralStr returns the string representation of literal value, with a boolean set to false if this is not a literal.
|
||||
func (node *Expression) LiteralStr() (string, bool) {
|
||||
return LiteralStr(node.Path)
|
||||
}
|
||||
|
||||
// Canonical returns the canonical form of expression node as a string.
|
||||
func (node *Expression) Canonical() string {
|
||||
if str, ok := HelperNameStr(node.Path); ok {
|
||||
return str
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// HelperNameStr returns the string representation of a helper name, with a boolean set to false if this is not a valid helper name.
|
||||
//
|
||||
// helperName : path | dataName | STRING | NUMBER | BOOLEAN | UNDEFINED | NULL
|
||||
func HelperNameStr(node Node) (string, bool) {
|
||||
// PathExpression
|
||||
if str, ok := PathExpressionStr(node); ok {
|
||||
return str, ok
|
||||
}
|
||||
|
||||
// Literal
|
||||
if str, ok := LiteralStr(node); ok {
|
||||
return str, ok
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// PathExpressionStr returns the string representation of path expression value, with a boolean set to false if this is not a path expression.
|
||||
func PathExpressionStr(node Node) (string, bool) {
|
||||
if path, ok := node.(*PathExpression); ok {
|
||||
result := path.Original
|
||||
|
||||
// "[foo bar]"" => "foo bar"
|
||||
if (len(result) >= 2) && (result[0] == '[') && (result[len(result)-1] == ']') {
|
||||
result = result[1 : len(result)-1]
|
||||
}
|
||||
|
||||
return result, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// LiteralStr returns the string representation of literal value, with a boolean set to false if this is not a literal.
|
||||
func LiteralStr(node Node) (string, bool) {
|
||||
if lit, ok := node.(*StringLiteral); ok {
|
||||
return lit.Value, true
|
||||
}
|
||||
|
||||
if lit, ok := node.(*BooleanLiteral); ok {
|
||||
return lit.Canonical(), true
|
||||
}
|
||||
|
||||
if lit, ok := node.(*NumberLiteral); ok {
|
||||
return lit.Canonical(), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
//
|
||||
// SubExpression
|
||||
//
|
||||
|
||||
// SubExpression represents a subexpression node.
|
||||
type SubExpression struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Expression *Expression
|
||||
}
|
||||
|
||||
// NewSubExpression instanciates a new subexpression node.
|
||||
func NewSubExpression(pos int, line int) *SubExpression {
|
||||
return &SubExpression{
|
||||
NodeType: NodeSubExpression,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *SubExpression) String() string {
|
||||
return fmt.Sprintf("Sexp{Path:%s, Pos:%d}", node.Expression.Path, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *SubExpression) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitSubExpression(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Path Expression
|
||||
//
|
||||
|
||||
// PathExpression represents a path expression node.
|
||||
type PathExpression struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Original string
|
||||
Depth int
|
||||
Parts []string
|
||||
Data bool
|
||||
Scoped bool
|
||||
}
|
||||
|
||||
// NewPathExpression instanciates a new path expression node.
|
||||
func NewPathExpression(pos int, line int, data bool) *PathExpression {
|
||||
result := &PathExpression{
|
||||
NodeType: NodePath,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if data {
|
||||
result.Original = "@"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *PathExpression) String() string {
|
||||
return fmt.Sprintf("Path{Original:'%s', Pos:%d}", node.Original, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *PathExpression) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitPath(node)
|
||||
}
|
||||
|
||||
// Part adds path part.
|
||||
func (node *PathExpression) Part(part string) {
|
||||
node.Original += part
|
||||
|
||||
switch part {
|
||||
case "..":
|
||||
node.Depth++
|
||||
node.Scoped = true
|
||||
case ".", "this":
|
||||
node.Scoped = true
|
||||
default:
|
||||
node.Parts = append(node.Parts, part)
|
||||
}
|
||||
}
|
||||
|
||||
// Sep adds path separator.
|
||||
func (node *PathExpression) Sep(separator string) {
|
||||
node.Original += separator
|
||||
}
|
||||
|
||||
// IsDataRoot returns true if path expression is @root.
|
||||
func (node *PathExpression) IsDataRoot() bool {
|
||||
return node.Data && (node.Parts[0] == "root")
|
||||
}
|
||||
|
||||
//
|
||||
// String Literal
|
||||
//
|
||||
|
||||
// StringLiteral represents a string node.
|
||||
type StringLiteral struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value string
|
||||
}
|
||||
|
||||
// NewStringLiteral instanciates a new string node.
|
||||
func NewStringLiteral(pos int, line int, val string) *StringLiteral {
|
||||
return &StringLiteral{
|
||||
NodeType: NodeString,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *StringLiteral) String() string {
|
||||
return fmt.Sprintf("String{Value:'%s', Pos:%d}", node.Value, node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *StringLiteral) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitString(node)
|
||||
}
|
||||
|
||||
//
|
||||
// Boolean Literal
|
||||
//
|
||||
|
||||
// BooleanLiteral represents a boolean node.
|
||||
type BooleanLiteral struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value bool
|
||||
Original string
|
||||
}
|
||||
|
||||
// NewBooleanLiteral instanciates a new boolean node.
|
||||
func NewBooleanLiteral(pos int, line int, val bool, original string) *BooleanLiteral {
|
||||
return &BooleanLiteral{
|
||||
NodeType: NodeBoolean,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
Original: original,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *BooleanLiteral) String() string {
|
||||
return fmt.Sprintf("Boolean{Value:%s, Pos:%d}", node.Canonical(), node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *BooleanLiteral) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitBoolean(node)
|
||||
}
|
||||
|
||||
// Canonical returns the canonical form of boolean node as a string (ie. "true" | "false").
|
||||
func (node *BooleanLiteral) Canonical() string {
|
||||
if node.Value {
|
||||
return "true"
|
||||
}
|
||||
|
||||
return "false"
|
||||
}
|
||||
|
||||
//
|
||||
// Number Literal
|
||||
//
|
||||
|
||||
// NumberLiteral represents a number node.
|
||||
type NumberLiteral struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Value float64
|
||||
IsInt bool
|
||||
Original string
|
||||
}
|
||||
|
||||
// NewNumberLiteral instanciates a new number node.
|
||||
func NewNumberLiteral(pos int, line int, val float64, isInt bool, original string) *NumberLiteral {
|
||||
return &NumberLiteral{
|
||||
NodeType: NodeNumber,
|
||||
Loc: Loc{pos, line},
|
||||
|
||||
Value: val,
|
||||
IsInt: isInt,
|
||||
Original: original,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *NumberLiteral) String() string {
|
||||
return fmt.Sprintf("Number{Value:%s, Pos:%d}", node.Canonical(), node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *NumberLiteral) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitNumber(node)
|
||||
}
|
||||
|
||||
// Canonical returns the canonical form of number node as a string (eg: "12", "-1.51").
|
||||
func (node *NumberLiteral) Canonical() string {
|
||||
prec := -1
|
||||
if node.IsInt {
|
||||
prec = 0
|
||||
}
|
||||
return strconv.FormatFloat(node.Value, 'f', prec, 64)
|
||||
}
|
||||
|
||||
// Number returns an integer or a float.
|
||||
func (node *NumberLiteral) Number() interface{} {
|
||||
if node.IsInt {
|
||||
return int(node.Value)
|
||||
}
|
||||
|
||||
return node.Value
|
||||
}
|
||||
|
||||
//
|
||||
// Hash
|
||||
//
|
||||
|
||||
// Hash represents a hash node.
|
||||
type Hash struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Pairs []*HashPair
|
||||
}
|
||||
|
||||
// NewHash instanciates a new hash node.
|
||||
func NewHash(pos int, line int) *Hash {
|
||||
return &Hash{
|
||||
NodeType: NodeHash,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *Hash) String() string {
|
||||
result := fmt.Sprintf("Hash{[%d", node.Loc.Pos)
|
||||
|
||||
for i, p := range node.Pairs {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += p.String()
|
||||
}
|
||||
|
||||
return result + fmt.Sprintf("], Pos:%d}", node.Loc.Pos)
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *Hash) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitHash(node)
|
||||
}
|
||||
|
||||
//
|
||||
// HashPair
|
||||
//
|
||||
|
||||
// HashPair represents a hash pair node.
|
||||
type HashPair struct {
|
||||
NodeType
|
||||
Loc
|
||||
|
||||
Key string
|
||||
Val Node // Expression
|
||||
}
|
||||
|
||||
// NewHashPair instanciates a new hash pair node.
|
||||
func NewHashPair(pos int, line int) *HashPair {
|
||||
return &HashPair{
|
||||
NodeType: NodeHashPair,
|
||||
Loc: Loc{pos, line},
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of receiver that can be used for debugging.
|
||||
func (node *HashPair) String() string {
|
||||
return node.Key + "=" + node.Val.String()
|
||||
}
|
||||
|
||||
// Accept is the receiver entry point for visitors.
|
||||
func (node *HashPair) Accept(visitor Visitor) interface{} {
|
||||
return visitor.VisitHashPair(node)
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// printVisitor implements the Visitor interface to print a AST.
|
||||
type printVisitor struct {
|
||||
buf string
|
||||
depth int
|
||||
|
||||
original bool
|
||||
inBlock bool
|
||||
}
|
||||
|
||||
func newPrintVisitor() *printVisitor {
|
||||
return &printVisitor{}
|
||||
}
|
||||
|
||||
// Print returns a string representation of given AST, that can be used for debugging purpose.
|
||||
func Print(node Node) string {
|
||||
visitor := newPrintVisitor()
|
||||
node.Accept(visitor)
|
||||
return visitor.output()
|
||||
}
|
||||
|
||||
func (v *printVisitor) output() string {
|
||||
return v.buf
|
||||
}
|
||||
|
||||
func (v *printVisitor) indent() {
|
||||
for i := 0; i < v.depth; {
|
||||
v.buf += " "
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (v *printVisitor) str(val string) {
|
||||
v.buf += val
|
||||
}
|
||||
|
||||
func (v *printVisitor) nl() {
|
||||
v.str("\n")
|
||||
}
|
||||
|
||||
func (v *printVisitor) line(val string) {
|
||||
v.indent()
|
||||
v.str(val)
|
||||
v.nl()
|
||||
}
|
||||
|
||||
//
|
||||
// Visitor interface
|
||||
//
|
||||
|
||||
// Statements
|
||||
|
||||
// VisitProgram implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitProgram(node *Program) interface{} {
|
||||
if len(node.BlockParams) > 0 {
|
||||
v.line("BLOCK PARAMS: [ " + strings.Join(node.BlockParams, " ") + " ]")
|
||||
}
|
||||
|
||||
for _, n := range node.Body {
|
||||
n.Accept(v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitMustache implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitMustache(node *MustacheStatement) interface{} {
|
||||
v.indent()
|
||||
v.str("{{ ")
|
||||
|
||||
node.Expression.Accept(v)
|
||||
|
||||
v.str(" }}")
|
||||
v.nl()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitBlock implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitBlock(node *BlockStatement) interface{} {
|
||||
v.inBlock = true
|
||||
|
||||
v.line("BLOCK:")
|
||||
v.depth++
|
||||
|
||||
node.Expression.Accept(v)
|
||||
|
||||
if node.Program != nil {
|
||||
v.line("PROGRAM:")
|
||||
v.depth++
|
||||
node.Program.Accept(v)
|
||||
v.depth--
|
||||
}
|
||||
|
||||
if node.Inverse != nil {
|
||||
// if node.Program != nil {
|
||||
// v.depth++
|
||||
// }
|
||||
|
||||
v.line("{{^}}")
|
||||
v.depth++
|
||||
node.Inverse.Accept(v)
|
||||
v.depth--
|
||||
|
||||
// if node.Program != nil {
|
||||
// v.depth--
|
||||
// }
|
||||
}
|
||||
|
||||
v.inBlock = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitPartial implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitPartial(node *PartialStatement) interface{} {
|
||||
v.indent()
|
||||
v.str("{{> PARTIAL:")
|
||||
|
||||
v.original = true
|
||||
node.Name.Accept(v)
|
||||
v.original = false
|
||||
|
||||
if len(node.Params) > 0 {
|
||||
v.str(" ")
|
||||
node.Params[0].Accept(v)
|
||||
}
|
||||
|
||||
// hash
|
||||
if node.Hash != nil {
|
||||
v.str(" ")
|
||||
node.Hash.Accept(v)
|
||||
}
|
||||
|
||||
v.str(" }}")
|
||||
v.nl()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitContent implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitContent(node *ContentStatement) interface{} {
|
||||
v.line("CONTENT[ '" + node.Value + "' ]")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitComment implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitComment(node *CommentStatement) interface{} {
|
||||
v.line("{{! '" + node.Value + "' }}")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expressions
|
||||
|
||||
// VisitExpression implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitExpression(node *Expression) interface{} {
|
||||
if v.inBlock {
|
||||
v.indent()
|
||||
}
|
||||
|
||||
// path
|
||||
node.Path.Accept(v)
|
||||
|
||||
// params
|
||||
v.str(" [")
|
||||
for i, n := range node.Params {
|
||||
if i > 0 {
|
||||
v.str(", ")
|
||||
}
|
||||
n.Accept(v)
|
||||
}
|
||||
v.str("]")
|
||||
|
||||
// hash
|
||||
if node.Hash != nil {
|
||||
v.str(" ")
|
||||
node.Hash.Accept(v)
|
||||
}
|
||||
|
||||
if v.inBlock {
|
||||
v.nl()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitSubExpression implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitSubExpression(node *SubExpression) interface{} {
|
||||
node.Expression.Accept(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitPath implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitPath(node *PathExpression) interface{} {
|
||||
if v.original {
|
||||
v.str(node.Original)
|
||||
} else {
|
||||
path := strings.Join(node.Parts, "/")
|
||||
|
||||
result := ""
|
||||
if node.Data {
|
||||
result += "@"
|
||||
}
|
||||
|
||||
v.str(result + "PATH:" + path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Literals
|
||||
|
||||
// VisitString implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitString(node *StringLiteral) interface{} {
|
||||
if v.original {
|
||||
v.str(node.Value)
|
||||
} else {
|
||||
v.str("\"" + node.Value + "\"")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitBoolean implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitBoolean(node *BooleanLiteral) interface{} {
|
||||
if v.original {
|
||||
v.str(node.Original)
|
||||
} else {
|
||||
v.str(fmt.Sprintf("BOOLEAN{%s}", node.Canonical()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitNumber implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitNumber(node *NumberLiteral) interface{} {
|
||||
if v.original {
|
||||
v.str(node.Original)
|
||||
} else {
|
||||
v.str(fmt.Sprintf("NUMBER{%s}", node.Canonical()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Miscellaneous
|
||||
|
||||
// VisitHash implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitHash(node *Hash) interface{} {
|
||||
v.str("HASH{")
|
||||
|
||||
for i, p := range node.Pairs {
|
||||
if i > 0 {
|
||||
v.str(", ")
|
||||
}
|
||||
p.Accept(v)
|
||||
}
|
||||
|
||||
v.str("}")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitHashPair implements corresponding Visitor interface method
|
||||
func (v *printVisitor) VisitHashPair(node *HashPair) interface{} {
|
||||
v.str(node.Key + "=")
|
||||
node.Val.Accept(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Test struct {
|
||||
name string
|
||||
input string
|
||||
data interface{}
|
||||
privData map[string]interface{}
|
||||
helpers map[string]interface{}
|
||||
partials map[string]string
|
||||
output interface{}
|
||||
}
|
||||
|
||||
func launchTests(t *testing.T, tests []Test) {
|
||||
// NOTE: TestMustache() makes Parallel testing fail
|
||||
// t.Parallel()
|
||||
|
||||
for _, test := range tests {
|
||||
var err error
|
||||
var tpl *Template
|
||||
|
||||
// parse template
|
||||
tpl, err = Parse(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
||||
} else {
|
||||
if len(test.helpers) > 0 {
|
||||
// register helpers
|
||||
tpl.RegisterHelpers(test.helpers)
|
||||
}
|
||||
|
||||
if len(test.partials) > 0 {
|
||||
// register partials
|
||||
tpl.RegisterPartials(test.partials)
|
||||
}
|
||||
|
||||
// setup private data frame
|
||||
var privData *DataFrame
|
||||
if test.privData != nil {
|
||||
privData = NewDataFrame()
|
||||
for k, v := range test.privData {
|
||||
privData.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// render template
|
||||
output, err := tpl.ExecWith(test.data, privData)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, Str(test.data), err, tpl.PrintAST())
|
||||
} else {
|
||||
// check output
|
||||
var expectedArr []string
|
||||
expectedArr, ok := test.output.([]string)
|
||||
if ok {
|
||||
match := false
|
||||
for _, expectedStr := range expectedArr {
|
||||
if expectedStr == output {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedArr, output, tpl.PrintAST())
|
||||
}
|
||||
} else {
|
||||
expectedStr, ok := test.output.(string)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
||||
}
|
||||
|
||||
if expectedStr != output {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedStr, output, tpl.PrintAST())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func launchErrorTests(t *testing.T, tests []Test) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range tests {
|
||||
var err error
|
||||
var tpl *Template
|
||||
|
||||
// parse template
|
||||
tpl, err = Parse(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
||||
} else {
|
||||
if len(test.helpers) > 0 {
|
||||
// register helpers
|
||||
tpl.RegisterHelpers(test.helpers)
|
||||
}
|
||||
|
||||
if len(test.partials) > 0 {
|
||||
// register partials
|
||||
tpl.RegisterPartials(test.partials)
|
||||
}
|
||||
|
||||
// setup private data frame
|
||||
var privData *DataFrame
|
||||
if test.privData != nil {
|
||||
privData := NewDataFrame()
|
||||
for k, v := range test.privData {
|
||||
privData.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// render template
|
||||
output, err := tpl.ExecWith(test.data, privData)
|
||||
if err == nil {
|
||||
t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\nAST:\n%q", test.name, test.input, output, tpl.PrintAST())
|
||||
} else {
|
||||
var errMatch error
|
||||
match := false
|
||||
|
||||
// check output
|
||||
var expectedArr []string
|
||||
expectedArr, ok := test.output.([]string)
|
||||
if ok {
|
||||
if len(expectedArr) > 0 {
|
||||
for _, expectedStr := range expectedArr {
|
||||
match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err))
|
||||
if errMatch != nil {
|
||||
panic("Failed to match regexp")
|
||||
}
|
||||
|
||||
if match {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// nothing to test
|
||||
match = true
|
||||
}
|
||||
} else {
|
||||
expectedStr, ok := test.output.(string)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
||||
}
|
||||
|
||||
if expectedStr != "" {
|
||||
match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err))
|
||||
if errMatch != nil {
|
||||
panic("Failed to match regexp")
|
||||
}
|
||||
} else {
|
||||
// nothing to test
|
||||
match = true
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\ndata:\n\t%s\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, Str(test.data), test.output, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package raymond
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/bench/
|
||||
//
|
||||
// Note that handlebars.js does NOT benchmark template compilation, it only benchmarks evaluation.
|
||||
//
|
||||
|
||||
func BenchmarkArguments(b *testing.B) {
|
||||
source := `{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}`
|
||||
|
||||
ctx := map[string]bool{
|
||||
"bar": true,
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
tpl.RegisterHelper("foo", func(a, b, c, d interface{}) string { return "" })
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArrayEach(b *testing.B) {
|
||||
source := `{{#each names}}{{name}}{{/each}}`
|
||||
|
||||
ctx := map[string][]map[string]string{
|
||||
"names": {
|
||||
{"name": "Moe"},
|
||||
{"name": "Larry"},
|
||||
{"name": "Curly"},
|
||||
{"name": "Shemp"},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArrayMustache(b *testing.B) {
|
||||
source := `{{#names}}{{name}}{{/names}}`
|
||||
|
||||
ctx := map[string][]map[string]string{
|
||||
"names": {
|
||||
{"name": "Moe"},
|
||||
{"name": "Larry"},
|
||||
{"name": "Curly"},
|
||||
{"name": "Shemp"},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkComplex(b *testing.B) {
|
||||
source := `<h1>{{header}}</h1>
|
||||
{{#if items}}
|
||||
<ul>
|
||||
{{#each items}}
|
||||
{{#if current}}
|
||||
<li><strong>{{name}}</strong></li>
|
||||
{{^}}
|
||||
<li><a href="{{url}}">{{name}}</a></li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{^}}
|
||||
<p>The list is empty.</p>
|
||||
{{/if}}
|
||||
`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"header": func() string { return "Colors" },
|
||||
"hasItems": true,
|
||||
"items": []map[string]interface{}{
|
||||
{"name": "red", "current": true, "url": "#Red"},
|
||||
{"name": "green", "current": false, "url": "#Green"},
|
||||
{"name": "blue", "current": false, "url": "#Blue"},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkData(b *testing.B) {
|
||||
source := `{{#each names}}{{@index}}{{name}}{{/each}}`
|
||||
|
||||
ctx := map[string][]map[string]string{
|
||||
"names": {
|
||||
{"name": "Moe"},
|
||||
{"name": "Larry"},
|
||||
{"name": "Curly"},
|
||||
{"name": "Shemp"},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDepth1(b *testing.B) {
|
||||
source := `{{#each names}}{{../foo}}{{/each}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"names": []map[string]string{
|
||||
{"name": "Moe"},
|
||||
{"name": "Larry"},
|
||||
{"name": "Curly"},
|
||||
{"name": "Shemp"},
|
||||
},
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDepth2(b *testing.B) {
|
||||
source := `{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"names": []map[string]interface{}{
|
||||
{"bat": "foo", "name": []string{"Moe"}},
|
||||
{"bat": "foo", "name": []string{"Larry"}},
|
||||
{"bat": "foo", "name": []string{"Curly"}},
|
||||
{"bat": "foo", "name": []string{"Shemp"}},
|
||||
},
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkObjectMustache(b *testing.B) {
|
||||
source := `{{#person}}{{name}}{{age}}{{/person}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"person": map[string]interface{}{
|
||||
"name": "Larry",
|
||||
"age": 45,
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkObject(b *testing.B) {
|
||||
source := `{{#with person}}{{name}}{{age}}{{/with}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"person": map[string]interface{}{
|
||||
"name": "Larry",
|
||||
"age": 45,
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPartialRecursion(b *testing.B) {
|
||||
source := `{{name}}{{#each kids}}{{>recursion}}{{/each}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"name": 1,
|
||||
"kids": []map[string]interface{}{
|
||||
{
|
||||
"name": "1.1",
|
||||
"kids": []map[string]interface{}{
|
||||
{
|
||||
"name": "1.1.1",
|
||||
"kids": []map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
partial := MustParse(`{{name}}{{#each kids}}{{>recursion}}{{/each}}`)
|
||||
tpl.RegisterPartialTemplate("recursion", partial)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPartial(b *testing.B) {
|
||||
source := `{{#each peeps}}{{>variables}}{{/each}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"peeps": []map[string]interface{}{
|
||||
{"name": "Moe", "count": 15},
|
||||
{"name": "Moe", "count": 5},
|
||||
{"name": "Curly", "count": 1},
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
partial := MustParse(`Hello {{name}}! You have {{count}} new messages.`)
|
||||
tpl.RegisterPartialTemplate("variables", partial)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPath(b *testing.B) {
|
||||
source := `{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"person": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"bar": map[string]string{
|
||||
"baz": "Larry",
|
||||
},
|
||||
},
|
||||
"age": 45,
|
||||
},
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkString(b *testing.B) {
|
||||
source := `Hello world`
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSubExpression(b *testing.B) {
|
||||
source := `{{echo (header)}}`
|
||||
|
||||
ctx := map[string]interface{}{}
|
||||
|
||||
tpl := MustParse(source)
|
||||
tpl.RegisterHelpers(map[string]interface{}{
|
||||
"echo": func(v string) string { return "foo " + v },
|
||||
"header": func() string { return "Colors" },
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVariables(b *testing.B) {
|
||||
source := `Hello {{name}}! You have {{count}} new messages.`
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"name": "Mick",
|
||||
"count": 30,
|
||||
}
|
||||
|
||||
tpl := MustParse(source)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.MustExec(ctx)
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package raymond
|
||||
|
||||
import "reflect"
|
||||
|
||||
// DataFrame represents a private data frame.
|
||||
//
|
||||
// Cf. private variables documentation at: http://handlebarsjs.com/block_helpers.html
|
||||
type DataFrame struct {
|
||||
parent *DataFrame
|
||||
data map[string]interface{}
|
||||
}
|
||||
|
||||
// NewDataFrame instanciates a new private data frame.
|
||||
func NewDataFrame() *DataFrame {
|
||||
return &DataFrame{
|
||||
data: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Copy instanciates a new private data frame with receiver as parent.
|
||||
func (p *DataFrame) Copy() *DataFrame {
|
||||
result := NewDataFrame()
|
||||
|
||||
for k, v := range p.data {
|
||||
result.data[k] = v
|
||||
}
|
||||
|
||||
result.parent = p
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// newIterDataFrame instanciates a new private data frame with receiver as parent and with iteration data set (@index, @key, @first, @last)
|
||||
func (p *DataFrame) newIterDataFrame(length int, i int, key interface{}) *DataFrame {
|
||||
result := p.Copy()
|
||||
|
||||
result.Set("index", i)
|
||||
result.Set("key", key)
|
||||
result.Set("first", i == 0)
|
||||
result.Set("last", i == length-1)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Set sets a data value.
|
||||
func (p *DataFrame) Set(key string, val interface{}) {
|
||||
p.data[key] = val
|
||||
}
|
||||
|
||||
// Get gets a data value.
|
||||
func (p *DataFrame) Get(key string) interface{} {
|
||||
return p.find([]string{key})
|
||||
}
|
||||
|
||||
// find gets a deep data value
|
||||
//
|
||||
// @todo This is NOT consistent with the way we resolve data in template (cf. `evalDataPathExpression()`) ! FIX THAT !
|
||||
func (p *DataFrame) find(parts []string) interface{} {
|
||||
data := p.data
|
||||
|
||||
for i, part := range parts {
|
||||
val := data[part]
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if i == len(parts)-1 {
|
||||
// found
|
||||
return val
|
||||
}
|
||||
|
||||
valValue := reflect.ValueOf(val)
|
||||
if valValue.Kind() != reflect.Map {
|
||||
// not found
|
||||
return nil
|
||||
}
|
||||
|
||||
// continue
|
||||
data = mapStringInterface(valValue)
|
||||
}
|
||||
|
||||
// not found
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapStringInterface converts any `map` to `map[string]interface{}`
|
||||
func mapStringInterface(value reflect.Value) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for _, key := range value.MapKeys() {
|
||||
result[strValue(key)] = value.MapIndex(key).Interface()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//
|
||||
// That whole file is borrowed from https://github.com/golang/go/tree/master/src/html/escape.go
|
||||
//
|
||||
// With changes:
|
||||
// ' => '
|
||||
// " => "
|
||||
//
|
||||
// To stay in sync with JS implementation, and make mustache tests pass.
|
||||
//
|
||||
|
||||
type writer interface {
|
||||
WriteString(string) (int, error)
|
||||
}
|
||||
|
||||
const escapedChars = `&'<>"`
|
||||
|
||||
func escape(w writer, s string) error {
|
||||
i := strings.IndexAny(s, escapedChars)
|
||||
for i != -1 {
|
||||
if _, err := w.WriteString(s[:i]); err != nil {
|
||||
return err
|
||||
}
|
||||
var esc string
|
||||
switch s[i] {
|
||||
case '&':
|
||||
esc = "&"
|
||||
case '\'':
|
||||
esc = "'"
|
||||
case '<':
|
||||
esc = "<"
|
||||
case '>':
|
||||
esc = ">"
|
||||
case '"':
|
||||
esc = """
|
||||
default:
|
||||
panic("unrecognized escape character")
|
||||
}
|
||||
s = s[i+1:]
|
||||
if _, err := w.WriteString(esc); err != nil {
|
||||
return err
|
||||
}
|
||||
i = strings.IndexAny(s, escapedChars)
|
||||
}
|
||||
_, err := w.WriteString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// Escape escapes special HTML characters.
|
||||
//
|
||||
// It can be used by helpers that return a SafeString and that need to escape some content by themselves.
|
||||
func Escape(s string) string {
|
||||
if strings.IndexAny(s, escapedChars) == -1 {
|
||||
return s
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
escape(&buf, s)
|
||||
return buf.String()
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package raymond
|
||||
|
||||
import "fmt"
|
||||
|
||||
func ExampleEscape() {
|
||||
tpl := MustParse("{{link url text}}")
|
||||
|
||||
tpl.RegisterHelper("link", func(url string, text string) SafeString {
|
||||
return SafeString("<a href='" + Escape(url) + "'>" + Escape(text) + "</a>")
|
||||
})
|
||||
|
||||
ctx := map[string]string{
|
||||
"url": "http://www.aymerick.com/",
|
||||
"text": "This is a <em>cool</em> website",
|
||||
}
|
||||
|
||||
result := tpl.MustExec(ctx)
|
||||
fmt.Print(result)
|
||||
// Output: <a href='http://www.aymerick.com/'>This is a <em>cool</em> website</a>
|
||||
}
|
||||
+984
@@ -0,0 +1,984 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
)
|
||||
|
||||
var (
|
||||
// @note borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
|
||||
zero reflect.Value
|
||||
)
|
||||
|
||||
// evalVisitor evaluates a handlebars template with context
|
||||
type evalVisitor struct {
|
||||
tpl *Template
|
||||
|
||||
// contexts stack
|
||||
ctx []reflect.Value
|
||||
|
||||
// current data frame (chained with parent)
|
||||
dataFrame *DataFrame
|
||||
|
||||
// block parameters stack
|
||||
blockParams []map[string]interface{}
|
||||
|
||||
// block statements stack
|
||||
blocks []*ast.BlockStatement
|
||||
|
||||
// expressions stack
|
||||
exprs []*ast.Expression
|
||||
|
||||
// memoize expressions that were function calls
|
||||
exprFunc map[*ast.Expression]bool
|
||||
|
||||
// used for info on panic
|
||||
curNode ast.Node
|
||||
}
|
||||
|
||||
// NewEvalVisitor instanciate a new evaluation visitor with given context and initial private data frame
|
||||
//
|
||||
// If privData is nil, then a default data frame is created
|
||||
func newEvalVisitor(tpl *Template, ctx interface{}, privData *DataFrame) *evalVisitor {
|
||||
frame := privData
|
||||
if frame == nil {
|
||||
frame = NewDataFrame()
|
||||
}
|
||||
|
||||
return &evalVisitor{
|
||||
tpl: tpl,
|
||||
ctx: []reflect.Value{reflect.ValueOf(ctx)},
|
||||
dataFrame: frame,
|
||||
exprFunc: make(map[*ast.Expression]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// at sets current node
|
||||
func (v *evalVisitor) at(node ast.Node) {
|
||||
v.curNode = node
|
||||
}
|
||||
|
||||
//
|
||||
// Contexts stack
|
||||
//
|
||||
|
||||
// pushCtx pushes new context to the stack
|
||||
func (v *evalVisitor) pushCtx(ctx reflect.Value) {
|
||||
v.ctx = append(v.ctx, ctx)
|
||||
}
|
||||
|
||||
// popCtx pops last context from stack
|
||||
func (v *evalVisitor) popCtx() reflect.Value {
|
||||
if len(v.ctx) == 0 {
|
||||
return zero
|
||||
}
|
||||
|
||||
var result reflect.Value
|
||||
result, v.ctx = v.ctx[len(v.ctx)-1], v.ctx[:len(v.ctx)-1]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// rootCtx returns root context
|
||||
func (v *evalVisitor) rootCtx() reflect.Value {
|
||||
return v.ctx[0]
|
||||
}
|
||||
|
||||
// curCtx returns current context
|
||||
func (v *evalVisitor) curCtx() reflect.Value {
|
||||
return v.ancestorCtx(0)
|
||||
}
|
||||
|
||||
// ancestorCtx returns ancestor context
|
||||
func (v *evalVisitor) ancestorCtx(depth int) reflect.Value {
|
||||
index := len(v.ctx) - 1 - depth
|
||||
if index < 0 {
|
||||
return zero
|
||||
}
|
||||
|
||||
return v.ctx[index]
|
||||
}
|
||||
|
||||
//
|
||||
// Private data frame
|
||||
//
|
||||
|
||||
// setDataFrame sets new data frame
|
||||
func (v *evalVisitor) setDataFrame(frame *DataFrame) {
|
||||
v.dataFrame = frame
|
||||
}
|
||||
|
||||
// popDataFrame sets back parent data frame
|
||||
func (v *evalVisitor) popDataFrame() {
|
||||
v.dataFrame = v.dataFrame.parent
|
||||
}
|
||||
|
||||
//
|
||||
// Block Parameters stack
|
||||
//
|
||||
|
||||
// pushBlockParams pushes new block params to the stack
|
||||
func (v *evalVisitor) pushBlockParams(params map[string]interface{}) {
|
||||
v.blockParams = append(v.blockParams, params)
|
||||
}
|
||||
|
||||
// popBlockParams pops last block params from stack
|
||||
func (v *evalVisitor) popBlockParams() map[string]interface{} {
|
||||
var result map[string]interface{}
|
||||
|
||||
if len(v.blockParams) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
result, v.blockParams = v.blockParams[len(v.blockParams)-1], v.blockParams[:len(v.blockParams)-1]
|
||||
return result
|
||||
}
|
||||
|
||||
// blockParam iterates on stack to find given block parameter, and returns its value or nil if not founc
|
||||
func (v *evalVisitor) blockParam(name string) interface{} {
|
||||
for i := len(v.blockParams) - 1; i >= 0; i-- {
|
||||
for k, v := range v.blockParams[i] {
|
||||
if name == k {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Blocks stack
|
||||
//
|
||||
|
||||
// pushBlock pushes new block statement to stack
|
||||
func (v *evalVisitor) pushBlock(block *ast.BlockStatement) {
|
||||
v.blocks = append(v.blocks, block)
|
||||
}
|
||||
|
||||
// popBlock pops last block statement from stack
|
||||
func (v *evalVisitor) popBlock() *ast.BlockStatement {
|
||||
if len(v.blocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result *ast.BlockStatement
|
||||
result, v.blocks = v.blocks[len(v.blocks)-1], v.blocks[:len(v.blocks)-1]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// curBlock returns current block statement
|
||||
func (v *evalVisitor) curBlock() *ast.BlockStatement {
|
||||
if len(v.blocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.blocks[len(v.blocks)-1]
|
||||
}
|
||||
|
||||
//
|
||||
// Expressions stack
|
||||
//
|
||||
|
||||
// pushExpr pushes new expression to stack
|
||||
func (v *evalVisitor) pushExpr(expression *ast.Expression) {
|
||||
v.exprs = append(v.exprs, expression)
|
||||
}
|
||||
|
||||
// popExpr pops last expression from stack
|
||||
func (v *evalVisitor) popExpr() *ast.Expression {
|
||||
if len(v.exprs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result *ast.Expression
|
||||
result, v.exprs = v.exprs[len(v.exprs)-1], v.exprs[:len(v.exprs)-1]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// curExpr returns current expression
|
||||
func (v *evalVisitor) curExpr() *ast.Expression {
|
||||
if len(v.exprs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.exprs[len(v.exprs)-1]
|
||||
}
|
||||
|
||||
//
|
||||
// Error functions
|
||||
//
|
||||
|
||||
// errPanic panics
|
||||
func (v *evalVisitor) errPanic(err error) {
|
||||
panic(fmt.Errorf("Evaluation error: %s\nCurrent node:\n\t%s", err, v.curNode))
|
||||
}
|
||||
|
||||
// errorf panics with a custom message
|
||||
func (v *evalVisitor) errorf(format string, args ...interface{}) {
|
||||
v.errPanic(fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
//
|
||||
// Evaluation
|
||||
//
|
||||
|
||||
// evalProgram eEvaluates program with given context and returns string result
|
||||
func (v *evalVisitor) evalProgram(program *ast.Program, ctx interface{}, data *DataFrame, key interface{}) string {
|
||||
blockParams := make(map[string]interface{})
|
||||
|
||||
// compute block params
|
||||
if len(program.BlockParams) > 0 {
|
||||
blockParams[program.BlockParams[0]] = ctx
|
||||
}
|
||||
|
||||
if (len(program.BlockParams) > 1) && (key != nil) {
|
||||
blockParams[program.BlockParams[1]] = key
|
||||
}
|
||||
|
||||
// push contexts
|
||||
if len(blockParams) > 0 {
|
||||
v.pushBlockParams(blockParams)
|
||||
}
|
||||
|
||||
ctxVal := reflect.ValueOf(ctx)
|
||||
if ctxVal.IsValid() {
|
||||
v.pushCtx(ctxVal)
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
v.setDataFrame(data)
|
||||
}
|
||||
|
||||
// evaluate program
|
||||
result, _ := program.Accept(v).(string)
|
||||
|
||||
// pop contexts
|
||||
if data != nil {
|
||||
v.popDataFrame()
|
||||
}
|
||||
|
||||
if ctxVal.IsValid() {
|
||||
v.popCtx()
|
||||
}
|
||||
|
||||
if len(blockParams) > 0 {
|
||||
v.popBlockParams()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evalPath evaluates all path parts with given context
|
||||
func (v *evalVisitor) evalPath(ctx reflect.Value, parts []string, exprRoot bool) (reflect.Value, bool) {
|
||||
partResolved := false
|
||||
|
||||
for i := 0; i < len(parts); i++ {
|
||||
part := parts[i]
|
||||
|
||||
// "[foo bar]"" => "foo bar"
|
||||
if (len(part) >= 2) && (part[0] == '[') && (part[len(part)-1] == ']') {
|
||||
part = part[1 : len(part)-1]
|
||||
}
|
||||
|
||||
ctx = v.evalField(ctx, part, exprRoot)
|
||||
if !ctx.IsValid() {
|
||||
break
|
||||
}
|
||||
|
||||
// we resolved at least one part of path
|
||||
partResolved = true
|
||||
}
|
||||
|
||||
return ctx, partResolved
|
||||
}
|
||||
|
||||
// evalField evaluates field with given context
|
||||
func (v *evalVisitor) evalField(ctx reflect.Value, fieldName string, exprRoot bool) reflect.Value {
|
||||
result := zero
|
||||
|
||||
ctx, _ = indirect(ctx)
|
||||
if !ctx.IsValid() {
|
||||
return result
|
||||
}
|
||||
|
||||
// check if this is a method call
|
||||
result, isMeth := v.evalMethod(ctx, fieldName, exprRoot)
|
||||
if !isMeth {
|
||||
switch ctx.Kind() {
|
||||
case reflect.Struct:
|
||||
// example: firstName => FirstName
|
||||
expFieldName := strings.Title(fieldName)
|
||||
|
||||
// check if struct have this field and that it is exported
|
||||
if tField, ok := ctx.Type().FieldByName(expFieldName); ok && (tField.PkgPath == "") {
|
||||
// struct field
|
||||
result = ctx.FieldByIndex(tField.Index)
|
||||
}
|
||||
case reflect.Map:
|
||||
nameVal := reflect.ValueOf(fieldName)
|
||||
if nameVal.Type().AssignableTo(ctx.Type().Key()) {
|
||||
// map key
|
||||
result = ctx.MapIndex(nameVal)
|
||||
}
|
||||
case reflect.Array, reflect.Slice:
|
||||
if i, err := strconv.Atoi(fieldName); (err == nil) && (i < ctx.Len()) {
|
||||
result = ctx.Index(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if result is a function
|
||||
result, _ = indirect(result)
|
||||
if result.Kind() == reflect.Func {
|
||||
result = v.evalFieldFunc(fieldName, result, exprRoot)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evalFieldFunc tries to evaluate given method name, and a boolean to indicate if this was a method call
|
||||
func (v *evalVisitor) evalMethod(ctx reflect.Value, name string, exprRoot bool) (reflect.Value, bool) {
|
||||
if ctx.Kind() != reflect.Interface && ctx.CanAddr() {
|
||||
ctx = ctx.Addr()
|
||||
}
|
||||
|
||||
method := ctx.MethodByName(name)
|
||||
if !method.IsValid() {
|
||||
// example: subject() => Subject()
|
||||
method = ctx.MethodByName(strings.Title(name))
|
||||
}
|
||||
|
||||
if !method.IsValid() {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
return v.evalFieldFunc(name, method, exprRoot), true
|
||||
}
|
||||
|
||||
// evalFieldFunc evaluates given function
|
||||
func (v *evalVisitor) evalFieldFunc(name string, funcVal reflect.Value, exprRoot bool) reflect.Value {
|
||||
ensureValidHelper(name, funcVal)
|
||||
|
||||
var options *Options
|
||||
if exprRoot {
|
||||
// create function arg with all params/hash
|
||||
expr := v.curExpr()
|
||||
options = v.helperOptions(expr)
|
||||
|
||||
// ok, that expression was a function call
|
||||
v.exprFunc[expr] = true
|
||||
} else {
|
||||
// we are not at root of expression, so we are a parameter... and we don't like
|
||||
// infinite loops caused by trying to parse ourself forever
|
||||
options = newEmptyOptions(v)
|
||||
}
|
||||
|
||||
return v.callFunc(name, funcVal, options)
|
||||
}
|
||||
|
||||
// findBlockParam returns node's block parameter
|
||||
func (v *evalVisitor) findBlockParam(node *ast.PathExpression) (string, interface{}) {
|
||||
if len(node.Parts) > 0 {
|
||||
name := node.Parts[0]
|
||||
if value := v.blockParam(name); value != nil {
|
||||
return name, value
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// evalPathExpression evaluates a path expression
|
||||
func (v *evalVisitor) evalPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
||||
var result interface{}
|
||||
|
||||
if name, value := v.findBlockParam(node); value != nil {
|
||||
// block parameter value
|
||||
|
||||
// We push a new context so we can evaluate the path expression (note: this may be a bad idea).
|
||||
//
|
||||
// Example:
|
||||
// {{#foo as |bar|}}
|
||||
// {{bar.baz}}
|
||||
// {{/foo}}
|
||||
//
|
||||
// With data:
|
||||
// {"foo": {"baz": "bat"}}
|
||||
newCtx := map[string]interface{}{name: value}
|
||||
|
||||
v.pushCtx(reflect.ValueOf(newCtx))
|
||||
result = v.evalCtxPathExpression(node, exprRoot)
|
||||
v.popCtx()
|
||||
} else {
|
||||
ctxTried := false
|
||||
|
||||
if node.IsDataRoot() {
|
||||
// context path
|
||||
result = v.evalCtxPathExpression(node, exprRoot)
|
||||
|
||||
ctxTried = true
|
||||
}
|
||||
|
||||
if (result == nil) && node.Data {
|
||||
// if it is @root, then we tried to evaluate with root context but nothing was found
|
||||
// so let's try with private data
|
||||
|
||||
// private data
|
||||
result = v.evalDataPathExpression(node, exprRoot)
|
||||
}
|
||||
|
||||
if (result == nil) && !ctxTried {
|
||||
// context path
|
||||
result = v.evalCtxPathExpression(node, exprRoot)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evalDataPathExpression evaluates a private data path expression
|
||||
func (v *evalVisitor) evalDataPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
||||
// find data frame
|
||||
frame := v.dataFrame
|
||||
for i := node.Depth; i > 0; i-- {
|
||||
if frame.parent == nil {
|
||||
return nil
|
||||
}
|
||||
frame = frame.parent
|
||||
}
|
||||
|
||||
// resolve data
|
||||
// @note Can be changed to v.evalCtx() as context can't be an array
|
||||
result, _ := v.evalCtxPath(reflect.ValueOf(frame.data), node.Parts, exprRoot)
|
||||
return result
|
||||
}
|
||||
|
||||
// evalCtxPathExpression evaluates a context path expression
|
||||
func (v *evalVisitor) evalCtxPathExpression(node *ast.PathExpression, exprRoot bool) interface{} {
|
||||
v.at(node)
|
||||
|
||||
if node.IsDataRoot() {
|
||||
// `@root` - remove the first part
|
||||
parts := node.Parts[1:len(node.Parts)]
|
||||
|
||||
result, _ := v.evalCtxPath(v.rootCtx(), parts, exprRoot)
|
||||
return result
|
||||
}
|
||||
|
||||
return v.evalDepthPath(node.Depth, node.Parts, exprRoot)
|
||||
}
|
||||
|
||||
// evalDepthPath iterates on contexts, starting at given depth, until there is one that resolve given path parts
|
||||
func (v *evalVisitor) evalDepthPath(depth int, parts []string, exprRoot bool) interface{} {
|
||||
var result interface{}
|
||||
partResolved := false
|
||||
|
||||
ctx := v.ancestorCtx(depth)
|
||||
|
||||
for (result == nil) && ctx.IsValid() && (depth <= len(v.ctx) && !partResolved) {
|
||||
// try with context
|
||||
result, partResolved = v.evalCtxPath(ctx, parts, exprRoot)
|
||||
|
||||
// As soon as we find the first part of a path, we must not try to resolve with parent context if result is finally `nil`
|
||||
// Reference: "Dotted Names - Context Precedence" mustache test
|
||||
if !partResolved && (result == nil) {
|
||||
// try with previous context
|
||||
depth++
|
||||
ctx = v.ancestorCtx(depth)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evalCtxPath evaluates path with given context
|
||||
func (v *evalVisitor) evalCtxPath(ctx reflect.Value, parts []string, exprRoot bool) (interface{}, bool) {
|
||||
var result interface{}
|
||||
partResolved := false
|
||||
|
||||
switch ctx.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
// Array context
|
||||
var results []interface{}
|
||||
|
||||
for i := 0; i < ctx.Len(); i++ {
|
||||
value, _ := v.evalPath(ctx.Index(i), parts, exprRoot)
|
||||
if value.IsValid() {
|
||||
results = append(results, value.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
result = results
|
||||
default:
|
||||
// NOT array context
|
||||
var value reflect.Value
|
||||
|
||||
value, partResolved = v.evalPath(ctx, parts, exprRoot)
|
||||
if value.IsValid() {
|
||||
result = value.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result, partResolved
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
// isHelperCall returns true if given expression is a helper call
|
||||
func (v *evalVisitor) isHelperCall(node *ast.Expression) bool {
|
||||
if helperName := node.HelperName(); helperName != "" {
|
||||
return v.findHelper(helperName) != zero
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findHelper finds given helper
|
||||
func (v *evalVisitor) findHelper(name string) reflect.Value {
|
||||
// check template helpers
|
||||
if h := v.tpl.findHelper(name); h != zero {
|
||||
return h
|
||||
}
|
||||
|
||||
// check global helpers
|
||||
return findHelper(name)
|
||||
}
|
||||
|
||||
// callFunc calls function with given options
|
||||
func (v *evalVisitor) callFunc(name string, funcVal reflect.Value, options *Options) reflect.Value {
|
||||
params := options.Params()
|
||||
|
||||
funcType := funcVal.Type()
|
||||
|
||||
// @todo Is there a better way to do that ?
|
||||
strType := reflect.TypeOf("")
|
||||
boolType := reflect.TypeOf(true)
|
||||
|
||||
// check parameters number
|
||||
addOptions := false
|
||||
numIn := funcType.NumIn()
|
||||
|
||||
if numIn == len(params)+1 {
|
||||
lastArgType := funcType.In(numIn - 1)
|
||||
if reflect.TypeOf(options).AssignableTo(lastArgType) {
|
||||
addOptions = true
|
||||
}
|
||||
}
|
||||
|
||||
if !addOptions && (len(params) != numIn) {
|
||||
v.errorf("Helper '%s' called with wrong number of arguments, needed %d but got %d", name, numIn, len(params))
|
||||
}
|
||||
|
||||
// check and collect arguments
|
||||
args := make([]reflect.Value, numIn)
|
||||
for i, param := range params {
|
||||
arg := reflect.ValueOf(param)
|
||||
argType := funcType.In(i)
|
||||
|
||||
if !arg.IsValid() {
|
||||
if canBeNil(argType) {
|
||||
arg = reflect.Zero(argType)
|
||||
} else if argType.Kind() == reflect.String {
|
||||
arg = reflect.ValueOf("")
|
||||
} else {
|
||||
// @todo Maybe we can panic on that
|
||||
return reflect.Zero(strType)
|
||||
}
|
||||
}
|
||||
|
||||
if !arg.Type().AssignableTo(argType) {
|
||||
if strType.AssignableTo(argType) {
|
||||
// convert parameter to string
|
||||
arg = reflect.ValueOf(strValue(arg))
|
||||
} else if boolType.AssignableTo(argType) {
|
||||
// convert parameter to bool
|
||||
val, _ := isTrueValue(arg)
|
||||
arg = reflect.ValueOf(val)
|
||||
} else {
|
||||
v.errorf("Helper %s called with argument %d with type %s but it should be %s", name, i, arg.Type(), argType)
|
||||
}
|
||||
}
|
||||
|
||||
args[i] = arg
|
||||
}
|
||||
|
||||
if addOptions {
|
||||
args[numIn-1] = reflect.ValueOf(options)
|
||||
}
|
||||
|
||||
result := funcVal.Call(args)
|
||||
|
||||
return result[0]
|
||||
}
|
||||
|
||||
// callHelper invoqs helper function for given expression node
|
||||
func (v *evalVisitor) callHelper(name string, helper reflect.Value, node *ast.Expression) interface{} {
|
||||
result := v.callFunc(name, helper, v.helperOptions(node))
|
||||
if !result.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @todo We maybe want to ensure here that helper returned a string or a SafeString
|
||||
return result.Interface()
|
||||
}
|
||||
|
||||
// helperOptions computes helper options argument from an expression
|
||||
func (v *evalVisitor) helperOptions(node *ast.Expression) *Options {
|
||||
var params []interface{}
|
||||
var hash map[string]interface{}
|
||||
|
||||
for _, paramNode := range node.Params {
|
||||
param := paramNode.Accept(v)
|
||||
params = append(params, param)
|
||||
}
|
||||
|
||||
if node.Hash != nil {
|
||||
hash, _ = node.Hash.Accept(v).(map[string]interface{})
|
||||
}
|
||||
|
||||
return newOptions(v, params, hash)
|
||||
}
|
||||
|
||||
//
|
||||
// Partials
|
||||
//
|
||||
|
||||
// findPartial finds given partial
|
||||
func (v *evalVisitor) findPartial(name string) *partial {
|
||||
// check template partials
|
||||
if p := v.tpl.findPartial(name); p != nil {
|
||||
return p
|
||||
}
|
||||
|
||||
// check global partials
|
||||
return findPartial(name)
|
||||
}
|
||||
|
||||
// partialContext computes partial context
|
||||
func (v *evalVisitor) partialContext(node *ast.PartialStatement) reflect.Value {
|
||||
if nb := len(node.Params); nb > 1 {
|
||||
v.errorf("Unsupported number of partial arguments: %d", nb)
|
||||
}
|
||||
|
||||
if (len(node.Params) > 0) && (node.Hash != nil) {
|
||||
v.errorf("Passing both context and named parameters to a partial is not allowed")
|
||||
}
|
||||
|
||||
if len(node.Params) == 1 {
|
||||
return reflect.ValueOf(node.Params[0].Accept(v))
|
||||
}
|
||||
|
||||
if node.Hash != nil {
|
||||
hash, _ := node.Hash.Accept(v).(map[string]interface{})
|
||||
return reflect.ValueOf(hash)
|
||||
}
|
||||
|
||||
return zero
|
||||
}
|
||||
|
||||
// evalPartial evaluates a partial
|
||||
func (v *evalVisitor) evalPartial(p *partial, node *ast.PartialStatement) string {
|
||||
// get partial template
|
||||
partialTpl, err := p.template()
|
||||
if err != nil {
|
||||
v.errPanic(err)
|
||||
}
|
||||
|
||||
// push partial context
|
||||
ctx := v.partialContext(node)
|
||||
if ctx.IsValid() {
|
||||
v.pushCtx(ctx)
|
||||
}
|
||||
|
||||
// evaluate partial template
|
||||
result, _ := partialTpl.program.Accept(v).(string)
|
||||
|
||||
// ident partial
|
||||
result = indentLines(result, node.Indent)
|
||||
|
||||
if ctx.IsValid() {
|
||||
v.popCtx()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// indentLines indents all lines of given string
|
||||
func indentLines(str string, indent string) string {
|
||||
if indent == "" {
|
||||
return str
|
||||
}
|
||||
|
||||
var indented []string
|
||||
|
||||
lines := strings.Split(str, "\n")
|
||||
for i, line := range lines {
|
||||
if (i == (len(lines) - 1)) && (line == "") {
|
||||
// input string ends with a new line
|
||||
indented = append(indented, line)
|
||||
} else {
|
||||
indented = append(indented, indent+line)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(indented, "\n")
|
||||
}
|
||||
|
||||
//
|
||||
// Functions
|
||||
//
|
||||
|
||||
// wasFuncCall returns true if given expression was a function call
|
||||
func (v *evalVisitor) wasFuncCall(node *ast.Expression) bool {
|
||||
// check if expression was tagged as a function call
|
||||
return v.exprFunc[node]
|
||||
}
|
||||
|
||||
//
|
||||
// Visitor interface
|
||||
//
|
||||
|
||||
// Statements
|
||||
|
||||
// VisitProgram implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitProgram(node *ast.Program) interface{} {
|
||||
v.at(node)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for _, n := range node.Body {
|
||||
if str := Str(n.Accept(v)); str != "" {
|
||||
if _, err := buf.Write([]byte(str)); err != nil {
|
||||
v.errPanic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// VisitMustache implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitMustache(node *ast.MustacheStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
// evaluate expression
|
||||
expr := node.Expression.Accept(v)
|
||||
|
||||
// check if this is a safe string
|
||||
isSafe := isSafeString(expr)
|
||||
|
||||
// get string value
|
||||
str := Str(expr)
|
||||
if !isSafe && !node.Unescaped {
|
||||
// escape html
|
||||
str = Escape(str)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// VisitBlock implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitBlock(node *ast.BlockStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
v.pushBlock(node)
|
||||
|
||||
var result interface{}
|
||||
|
||||
// evaluate expression
|
||||
expr := node.Expression.Accept(v)
|
||||
|
||||
if v.isHelperCall(node.Expression) || v.wasFuncCall(node.Expression) {
|
||||
// it is the responsability of the helper/function to evaluate block
|
||||
result = expr
|
||||
} else {
|
||||
val := reflect.ValueOf(expr)
|
||||
|
||||
truth, _ := isTrueValue(val)
|
||||
if truth {
|
||||
if node.Program != nil {
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
concat := ""
|
||||
|
||||
// Array context
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
// Computes new private data frame
|
||||
frame := v.dataFrame.newIterDataFrame(val.Len(), i, nil)
|
||||
|
||||
// Evaluate program
|
||||
concat += v.evalProgram(node.Program, val.Index(i).Interface(), frame, i)
|
||||
}
|
||||
|
||||
result = concat
|
||||
default:
|
||||
// NOT array
|
||||
result = v.evalProgram(node.Program, expr, nil, nil)
|
||||
}
|
||||
}
|
||||
} else if node.Inverse != nil {
|
||||
result, _ = node.Inverse.Accept(v).(string)
|
||||
}
|
||||
}
|
||||
|
||||
v.popBlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// VisitPartial implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
// partialName: helperName | sexpr
|
||||
name, ok := ast.HelperNameStr(node.Name)
|
||||
if !ok {
|
||||
if subExpr, ok := node.Name.(*ast.SubExpression); ok {
|
||||
name, _ = subExpr.Accept(v).(string)
|
||||
}
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
v.errorf("Unexpected partial name: %q", node.Name)
|
||||
}
|
||||
|
||||
partial := v.findPartial(name)
|
||||
if partial == nil {
|
||||
v.errorf("Partial not found: %s", name)
|
||||
}
|
||||
|
||||
return v.evalPartial(partial, node)
|
||||
}
|
||||
|
||||
// VisitContent implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitContent(node *ast.ContentStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
// write content as is
|
||||
return node.Value
|
||||
}
|
||||
|
||||
// VisitComment implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitComment(node *ast.CommentStatement) interface{} {
|
||||
v.at(node)
|
||||
|
||||
// ignore comments
|
||||
return ""
|
||||
}
|
||||
|
||||
// Expressions
|
||||
|
||||
// VisitExpression implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitExpression(node *ast.Expression) interface{} {
|
||||
v.at(node)
|
||||
|
||||
var result interface{}
|
||||
done := false
|
||||
|
||||
v.pushExpr(node)
|
||||
|
||||
// helper call
|
||||
if helperName := node.HelperName(); helperName != "" {
|
||||
if helper := v.findHelper(helperName); helper != zero {
|
||||
result = v.callHelper(helperName, helper, node)
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
if !done {
|
||||
// literal
|
||||
if literal, ok := node.LiteralStr(); ok {
|
||||
if val := v.evalField(v.curCtx(), literal, true); val.IsValid() {
|
||||
result = val.Interface()
|
||||
done = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !done {
|
||||
// field path
|
||||
if path := node.FieldPath(); path != nil {
|
||||
// @todo Find a cleaner way ! Don't break the pattern !
|
||||
// this is an exception to visitor pattern, because we need to pass the info
|
||||
// that this path is at root of current expression
|
||||
if val := v.evalPathExpression(path, true); val != nil {
|
||||
result = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.popExpr()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// VisitSubExpression implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitSubExpression(node *ast.SubExpression) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Expression.Accept(v)
|
||||
}
|
||||
|
||||
// VisitPath implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitPath(node *ast.PathExpression) interface{} {
|
||||
return v.evalPathExpression(node, false)
|
||||
}
|
||||
|
||||
// Literals
|
||||
|
||||
// VisitString implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitString(node *ast.StringLiteral) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Value
|
||||
}
|
||||
|
||||
// VisitBoolean implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Value
|
||||
}
|
||||
|
||||
// VisitNumber implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitNumber(node *ast.NumberLiteral) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Number()
|
||||
}
|
||||
|
||||
// Miscellaneous
|
||||
|
||||
// VisitHash implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitHash(node *ast.Hash) interface{} {
|
||||
v.at(node)
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for _, pair := range node.Pairs {
|
||||
if value := pair.Accept(v); value != nil {
|
||||
result[pair.Key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// VisitHashPair implements corresponding Visitor interface method
|
||||
func (v *evalVisitor) VisitHashPair(node *ast.HashPair) interface{} {
|
||||
v.at(node)
|
||||
|
||||
return node.Val.Accept(v)
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
package raymond
|
||||
|
||||
import "testing"
|
||||
|
||||
var evalTests = []Test{
|
||||
{
|
||||
"only content",
|
||||
"this is content",
|
||||
nil, nil, nil, nil,
|
||||
"this is content",
|
||||
},
|
||||
{
|
||||
"checks path in parent contexts",
|
||||
"{{#a}}{{one}}{{#b}}{{one}}{{two}}{{one}}{{/b}}{{/a}}",
|
||||
map[string]interface{}{"a": map[string]int{"one": 1}, "b": map[string]int{"two": 2}},
|
||||
nil, nil, nil,
|
||||
"1121",
|
||||
},
|
||||
{
|
||||
"block params",
|
||||
"{{#foo as |bar|}}{{bar}}{{/foo}}{{bar}}",
|
||||
map[string]string{"foo": "baz", "bar": "bat"},
|
||||
nil, nil, nil,
|
||||
"bazbat",
|
||||
},
|
||||
{
|
||||
"block params on array",
|
||||
"{{#foo as |bar i|}}{{i}}.{{bar}} {{/foo}}",
|
||||
map[string][]string{"foo": {"baz", "bar", "bat"}},
|
||||
nil, nil, nil,
|
||||
"0.baz 1.bar 2.bat ",
|
||||
},
|
||||
{
|
||||
"nested block params",
|
||||
"{{#foos as |foo iFoo|}}{{#wats as |wat iWat|}}{{iFoo}}.{{iWat}}.{{foo}}-{{wat}} {{/wats}}{{/foos}}",
|
||||
map[string][]string{"foos": {"baz", "bar"}, "wats": {"the", "phoque"}},
|
||||
nil, nil, nil,
|
||||
"0.0.baz-the 0.1.baz-phoque 1.0.bar-the 1.1.bar-phoque ",
|
||||
},
|
||||
{
|
||||
"block params with path reference",
|
||||
"{{#foo as |bar|}}{{bar.baz}}{{/foo}}",
|
||||
map[string]map[string]string{"foo": {"baz": "bat"}},
|
||||
nil, nil, nil,
|
||||
"bat",
|
||||
},
|
||||
{
|
||||
"falsy block evaluation",
|
||||
"{{#foo}}bar{{/foo}} baz",
|
||||
map[string]interface{}{"foo": false},
|
||||
nil, nil, nil,
|
||||
" baz",
|
||||
},
|
||||
{
|
||||
"block helper returns a SafeString",
|
||||
"{{title}} - {{#bold}}{{body}}{{/bold}}",
|
||||
map[string]string{
|
||||
"title": "My new blog post",
|
||||
"body": "I have so many things to say!",
|
||||
},
|
||||
nil,
|
||||
map[string]interface{}{"bold": func(options *Options) SafeString {
|
||||
return SafeString(`<div class="mybold">` + options.Fn() + "</div>")
|
||||
}},
|
||||
nil,
|
||||
`My new blog post - <div class="mybold">I have so many things to say!</div>`,
|
||||
},
|
||||
{
|
||||
"chained blocks",
|
||||
"{{#if a}}A{{else if b}}B{{else}}C{{/if}}",
|
||||
map[string]interface{}{"b": false},
|
||||
nil, nil, nil,
|
||||
"C",
|
||||
},
|
||||
|
||||
// @todo Test with a "../../path" (depth 2 path) while context is only depth 1
|
||||
}
|
||||
|
||||
func TestEval(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
launchTests(t, evalTests)
|
||||
}
|
||||
|
||||
var evalErrors = []Test{
|
||||
{
|
||||
"functions with wrong number of arguments",
|
||||
`{{foo "bar"}}`,
|
||||
map[string]interface{}{"foo": func(a string, b string) string { return "foo" }},
|
||||
nil, nil, nil,
|
||||
"Helper 'foo' called with wrong number of arguments, needed 2 but got 1",
|
||||
},
|
||||
{
|
||||
"functions with wrong number of returned values (1)",
|
||||
"{{foo}}",
|
||||
map[string]interface{}{"foo": func() {}},
|
||||
nil, nil, nil,
|
||||
"Helper function must return a string or a SafeString",
|
||||
},
|
||||
{
|
||||
"functions with wrong number of returned values (2)",
|
||||
"{{foo}}",
|
||||
map[string]interface{}{"foo": func() (string, bool, string) { return "foo", true, "bar" }},
|
||||
nil, nil, nil,
|
||||
"Helper function must return a string or a SafeString",
|
||||
},
|
||||
}
|
||||
|
||||
func TestEvalErrors(t *testing.T) {
|
||||
launchErrorTests(t, evalErrors)
|
||||
}
|
||||
|
||||
func TestEvalStruct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
source := `<div class="post">
|
||||
<h1>By {{author.FirstName}} {{Author.lastName}}</h1>
|
||||
<div class="body">{{Body}}</div>
|
||||
|
||||
<h1>Comments</h1>
|
||||
|
||||
{{#each comments}}
|
||||
<h2>By {{Author.FirstName}} {{author.LastName}}</h2>
|
||||
<div class="body">{{body}}</div>
|
||||
{{/each}}
|
||||
</div>`
|
||||
|
||||
expected := `<div class="post">
|
||||
<h1>By Jean Valjean</h1>
|
||||
<div class="body">Life is difficult</div>
|
||||
|
||||
<h1>Comments</h1>
|
||||
|
||||
<h2>By Marcel Beliveau</h2>
|
||||
<div class="body">LOL!</div>
|
||||
</div>`
|
||||
|
||||
type Person struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
Author Person
|
||||
Body string
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
Author Person
|
||||
Body string
|
||||
Comments []Comment
|
||||
}
|
||||
|
||||
ctx := Post{
|
||||
Person{"Jean", "Valjean"},
|
||||
"Life is difficult",
|
||||
[]Comment{
|
||||
Comment{
|
||||
Person{"Marcel", "Beliveau"},
|
||||
"LOL!",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output := MustRender(source, ctx)
|
||||
if output != expected {
|
||||
t.Errorf("Failed to evaluate with struct context")
|
||||
}
|
||||
}
|
||||
|
||||
type TestFoo struct {
|
||||
}
|
||||
|
||||
func (t *TestFoo) Subject() string {
|
||||
return "foo"
|
||||
}
|
||||
|
||||
func TestEvalMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
source := `Subject is {{subject}}! YES I SAID {{Subject}}!`
|
||||
expected := `Subject is foo! YES I SAID foo!`
|
||||
|
||||
ctx := &TestFoo{}
|
||||
|
||||
output := MustRender(source, ctx)
|
||||
if output != expected {
|
||||
t.Errorf("Failed to evaluate struct method: %s", output)
|
||||
}
|
||||
}
|
||||
|
||||
type TestBar struct {
|
||||
}
|
||||
|
||||
func (t *TestBar) Subject() interface{} {
|
||||
return testBar
|
||||
}
|
||||
|
||||
func testBar() string {
|
||||
return "bar"
|
||||
}
|
||||
|
||||
func TestEvalMethodReturningFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
source := `Subject is {{subject}}! YES I SAID {{Subject}}!`
|
||||
expected := `Subject is bar! YES I SAID bar!`
|
||||
|
||||
ctx := &TestBar{}
|
||||
|
||||
output := MustRender(source, ctx)
|
||||
if output != expected {
|
||||
t.Errorf("Failed to evaluate struct method: %s", output)
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package handlebars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
// cf. https://github.com/aymerick/go-fuzz-tests/raymond
|
||||
const dumpTpl = false
|
||||
|
||||
var dumpTplNb = 0
|
||||
|
||||
type Test struct {
|
||||
name string
|
||||
input string
|
||||
data interface{}
|
||||
privData map[string]interface{}
|
||||
helpers map[string]interface{}
|
||||
partials map[string]string
|
||||
output interface{}
|
||||
}
|
||||
|
||||
func launchTests(t *testing.T, tests []Test) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range tests {
|
||||
var err error
|
||||
var tpl *raymond.Template
|
||||
|
||||
if dumpTpl {
|
||||
filename := strconv.Itoa(dumpTplNb)
|
||||
if err := ioutil.WriteFile(path.Join(".", "dump_tpl", filename), []byte(test.input), 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dumpTplNb++
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl, err = raymond.Parse(test.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err)
|
||||
} else {
|
||||
if len(test.helpers) > 0 {
|
||||
// register helpers
|
||||
tpl.RegisterHelpers(test.helpers)
|
||||
}
|
||||
|
||||
if len(test.partials) > 0 {
|
||||
// register partials
|
||||
tpl.RegisterPartials(test.partials)
|
||||
}
|
||||
|
||||
// setup private data frame
|
||||
var privData *raymond.DataFrame
|
||||
if test.privData != nil {
|
||||
privData = raymond.NewDataFrame()
|
||||
for k, v := range test.privData {
|
||||
privData.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// render template
|
||||
output, err := tpl.ExecWith(test.data, privData)
|
||||
if err != nil {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, raymond.Str(test.data), err, tpl.PrintAST())
|
||||
} else {
|
||||
// check output
|
||||
var expectedArr []string
|
||||
expectedArr, ok := test.output.([]string)
|
||||
if ok {
|
||||
match := false
|
||||
for _, expectedStr := range expectedArr {
|
||||
if expectedStr == output {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, raymond.Str(test.data), raymond.Str(test.partials), expectedArr, output, tpl.PrintAST())
|
||||
}
|
||||
} else {
|
||||
expectedStr, ok := test.output.(string)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("Erroneous test output description: %q", test.output))
|
||||
}
|
||||
|
||||
if expectedStr != output {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, raymond.Str(test.data), raymond.Str(test.partials), expectedStr, output, tpl.PrintAST())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+650
@@ -0,0 +1,650 @@
|
||||
package handlebars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/basic.js
|
||||
//
|
||||
var basicTests = []Test{
|
||||
{
|
||||
"most basic",
|
||||
"{{foo}}",
|
||||
map[string]string{"foo": "foo"},
|
||||
nil, nil, nil,
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"escaping (1)",
|
||||
"\\{{foo}}",
|
||||
map[string]string{"foo": "food"},
|
||||
nil, nil, nil,
|
||||
"{{foo}}",
|
||||
},
|
||||
{
|
||||
"escaping (2)",
|
||||
"content \\{{foo}}",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"content {{foo}}",
|
||||
},
|
||||
{
|
||||
"escaping (3)",
|
||||
"\\\\{{foo}}",
|
||||
map[string]string{"foo": "food"},
|
||||
nil, nil, nil,
|
||||
"\\food",
|
||||
},
|
||||
{
|
||||
"escaping (4)",
|
||||
"content \\\\{{foo}}",
|
||||
map[string]string{"foo": "food"},
|
||||
nil, nil, nil,
|
||||
"content \\food",
|
||||
},
|
||||
{
|
||||
"escaping (5)",
|
||||
"\\\\ {{foo}}",
|
||||
map[string]string{"foo": "food"},
|
||||
nil, nil, nil,
|
||||
"\\\\ food",
|
||||
},
|
||||
{
|
||||
"compiling with a basic context",
|
||||
"Goodbye\n{{cruel}}\n{{world}}!",
|
||||
map[string]string{"cruel": "cruel", "world": "world"},
|
||||
nil, nil, nil,
|
||||
"Goodbye\ncruel\nworld!",
|
||||
},
|
||||
{
|
||||
"compiling with an undefined context (1)",
|
||||
"Goodbye\n{{cruel}}\n{{world.bar}}!",
|
||||
nil, nil, nil, nil,
|
||||
"Goodbye\n\n!",
|
||||
},
|
||||
{
|
||||
"compiling with an undefined context (2)",
|
||||
"{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}",
|
||||
nil, nil, nil, nil,
|
||||
"Goodbye",
|
||||
},
|
||||
{
|
||||
"comments (1)",
|
||||
"{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
|
||||
map[string]string{"cruel": "cruel", "world": "world"},
|
||||
nil, nil, nil,
|
||||
"Goodbye\ncruel\nworld!",
|
||||
},
|
||||
{
|
||||
"comments (2)",
|
||||
" {{~! comment ~}} blah",
|
||||
nil, nil, nil, nil,
|
||||
"blah",
|
||||
},
|
||||
{
|
||||
"comments (3)",
|
||||
" {{~!-- long-comment --~}} blah",
|
||||
nil, nil, nil, nil,
|
||||
"blah",
|
||||
},
|
||||
{
|
||||
"comments (4)",
|
||||
" {{! comment ~}} blah",
|
||||
nil, nil, nil, nil,
|
||||
" blah",
|
||||
},
|
||||
{
|
||||
"comments (5)",
|
||||
" {{!-- long-comment --~}} blah",
|
||||
nil, nil, nil, nil,
|
||||
" blah",
|
||||
},
|
||||
{
|
||||
"comments (6)",
|
||||
" {{~! comment}} blah",
|
||||
nil, nil, nil, nil,
|
||||
" blah",
|
||||
},
|
||||
{
|
||||
"comments (7)",
|
||||
" {{~!-- long-comment --}} blah",
|
||||
nil, nil, nil, nil,
|
||||
" blah",
|
||||
},
|
||||
{
|
||||
"boolean (1)",
|
||||
"{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": true, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"boolean (2)",
|
||||
"{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": false, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"zeros (1)",
|
||||
"num1: {{num1}}, num2: {{num2}}",
|
||||
map[string]interface{}{"num1": 42, "num2": 0},
|
||||
nil, nil, nil,
|
||||
"num1: 42, num2: 0",
|
||||
},
|
||||
{
|
||||
"zeros (2)",
|
||||
"num: {{.}}",
|
||||
0,
|
||||
nil, nil, nil,
|
||||
"num: 0",
|
||||
},
|
||||
{
|
||||
"zeros (3)",
|
||||
"num: {{num1/num2}}",
|
||||
map[string]map[string]interface{}{"num1": {"num2": 0}},
|
||||
nil, nil, nil,
|
||||
"num: 0",
|
||||
},
|
||||
{
|
||||
"false (1)",
|
||||
"val1: {{val1}}, val2: {{val2}}",
|
||||
map[string]interface{}{"val1": false, "val2": false},
|
||||
nil, nil, nil,
|
||||
"val1: false, val2: false",
|
||||
},
|
||||
{
|
||||
"false (2)",
|
||||
"val: {{.}}",
|
||||
false,
|
||||
nil, nil, nil,
|
||||
"val: false",
|
||||
},
|
||||
{
|
||||
"false (3)",
|
||||
"val: {{val1/val2}}",
|
||||
map[string]map[string]interface{}{"val1": {"val2": false}},
|
||||
nil, nil, nil,
|
||||
"val: false",
|
||||
},
|
||||
{
|
||||
"false (4)",
|
||||
"val1: {{{val1}}}, val2: {{{val2}}}",
|
||||
map[string]interface{}{"val1": false, "val2": false},
|
||||
nil, nil, nil,
|
||||
"val1: false, val2: false",
|
||||
},
|
||||
{
|
||||
"false (5)",
|
||||
"val: {{{val1/val2}}}",
|
||||
map[string]map[string]interface{}{"val1": {"val2": false}},
|
||||
nil, nil, nil,
|
||||
"val: false",
|
||||
},
|
||||
{
|
||||
"newlines (1)",
|
||||
"Alan's\nTest",
|
||||
nil, nil, nil, nil,
|
||||
"Alan's\nTest",
|
||||
},
|
||||
{
|
||||
"newlines (2)",
|
||||
"Alan's\rTest",
|
||||
nil, nil, nil, nil,
|
||||
"Alan's\rTest",
|
||||
},
|
||||
{
|
||||
"escaping text (1)",
|
||||
"Awesome's",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"Awesome's",
|
||||
},
|
||||
{
|
||||
"escaping text (2)",
|
||||
"Awesome\\",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"Awesome\\",
|
||||
},
|
||||
{
|
||||
"escaping text (3)",
|
||||
"Awesome\\\\ foo",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"Awesome\\\\ foo",
|
||||
},
|
||||
{
|
||||
"escaping text (4)",
|
||||
"Awesome {{foo}}",
|
||||
map[string]string{"foo": "\\"},
|
||||
nil, nil, nil,
|
||||
"Awesome \\",
|
||||
},
|
||||
{
|
||||
"escaping text (5)",
|
||||
" ' ' ",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
" ' ' ",
|
||||
},
|
||||
{
|
||||
"escaping expressions (6)",
|
||||
"{{{awesome}}}",
|
||||
map[string]string{"awesome": "&'\\<>"},
|
||||
nil, nil, nil,
|
||||
"&'\\<>",
|
||||
},
|
||||
{
|
||||
"escaping expressions (7)",
|
||||
"{{&awesome}}",
|
||||
map[string]string{"awesome": "&'\\<>"},
|
||||
nil, nil, nil,
|
||||
"&'\\<>",
|
||||
},
|
||||
{
|
||||
"escaping expressions (8)",
|
||||
"{{awesome}}",
|
||||
map[string]string{"awesome": "&\"'`\\<>"},
|
||||
nil, nil, nil,
|
||||
"&"'`\\<>",
|
||||
},
|
||||
{
|
||||
"escaping expressions (9)",
|
||||
"{{awesome}}",
|
||||
map[string]string{"awesome": "Escaped, <b> looks like: <b>"},
|
||||
nil, nil, nil,
|
||||
"Escaped, <b> looks like: &lt;b&gt;",
|
||||
},
|
||||
{
|
||||
"functions returning safestrings shouldn't be escaped",
|
||||
"{{awesome}}",
|
||||
map[string]interface{}{"awesome": func() raymond.SafeString { return raymond.SafeString("&'\\<>") }},
|
||||
nil, nil, nil,
|
||||
"&'\\<>",
|
||||
},
|
||||
{
|
||||
"functions (1)",
|
||||
"{{awesome}}",
|
||||
map[string]interface{}{"awesome": func() string { return "Awesome" }},
|
||||
nil, nil, nil,
|
||||
"Awesome",
|
||||
},
|
||||
{
|
||||
"functions (2)",
|
||||
"{{awesome}}",
|
||||
map[string]interface{}{"awesome": func(options *raymond.Options) string {
|
||||
return options.ValueStr("more")
|
||||
}, "more": "More awesome"},
|
||||
nil, nil, nil,
|
||||
"More awesome",
|
||||
},
|
||||
{
|
||||
"functions with context argument",
|
||||
"{{awesome frank}}",
|
||||
map[string]interface{}{"awesome": func(context string) string {
|
||||
return context
|
||||
}, "frank": "Frank"},
|
||||
nil, nil, nil,
|
||||
"Frank",
|
||||
},
|
||||
{
|
||||
"pathed functions with context argument",
|
||||
"{{bar.awesome frank}}",
|
||||
map[string]interface{}{"bar": map[string]interface{}{"awesome": func(context string) string {
|
||||
return context
|
||||
}}, "frank": "Frank"},
|
||||
nil, nil, nil,
|
||||
"Frank",
|
||||
},
|
||||
{
|
||||
"depthed functions with context argument",
|
||||
"{{#with frank}}{{../awesome .}}{{/with}}",
|
||||
map[string]interface{}{"awesome": func(context string) string {
|
||||
return context
|
||||
}, "frank": "Frank"},
|
||||
nil, nil, nil,
|
||||
"Frank",
|
||||
},
|
||||
{
|
||||
"block functions with context argument",
|
||||
"{{#awesome 1}}inner {{.}}{{/awesome}}",
|
||||
map[string]interface{}{"awesome": func(context interface{}, options *raymond.Options) string {
|
||||
return options.FnWith(context)
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"inner 1",
|
||||
},
|
||||
{
|
||||
"depthed block functions with context argument",
|
||||
"{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}",
|
||||
map[string]interface{}{
|
||||
"awesome": func(context interface{}, options *raymond.Options) string {
|
||||
return options.FnWith(context)
|
||||
},
|
||||
"value": true,
|
||||
},
|
||||
nil, nil, nil,
|
||||
"inner 1",
|
||||
},
|
||||
{
|
||||
"block functions without context argument",
|
||||
"{{#awesome}}inner{{/awesome}}",
|
||||
map[string]interface{}{
|
||||
"awesome": func(options *raymond.Options) string {
|
||||
return options.Fn()
|
||||
},
|
||||
},
|
||||
nil, nil, nil,
|
||||
"inner",
|
||||
},
|
||||
// // @note I don't even understand why this test passes with the JS implementation... it should be
|
||||
// // the responsability of the function to evaluate the block
|
||||
// {
|
||||
// "pathed block functions without context argument",
|
||||
// "{{#foo.awesome}}inner{{/foo.awesome}}",
|
||||
// map[string]map[string]interface{}{
|
||||
// "foo": {
|
||||
// "awesome": func(options *raymond.Options) interface{} {
|
||||
// return options.Ctx()
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// nil, nil, nil,
|
||||
// "inner",
|
||||
// },
|
||||
// // @note I don't even understand why this test passes with the JS implementation... it should be
|
||||
// // the responsability of the function to evaluate the block
|
||||
// {
|
||||
// "depthed block functions without context argument",
|
||||
// "{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}",
|
||||
// map[string]interface{}{
|
||||
// "value": true,
|
||||
// "awesome": func(options *raymond.Options) interface{} {
|
||||
// return options.Ctx()
|
||||
// },
|
||||
// },
|
||||
// nil, nil, nil,
|
||||
// "inner",
|
||||
// },
|
||||
{
|
||||
"paths with hyphens (1)",
|
||||
"{{foo-bar}}",
|
||||
map[string]string{"foo-bar": "baz"},
|
||||
nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"paths with hyphens (2)",
|
||||
"{{foo.foo-bar}}",
|
||||
map[string]map[string]string{"foo": {"foo-bar": "baz"}},
|
||||
nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"paths with hyphens (3)",
|
||||
"{{foo/foo-bar}}",
|
||||
map[string]map[string]string{"foo": {"foo-bar": "baz"}},
|
||||
nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"nested paths",
|
||||
"Goodbye {{alan/expression}} world!",
|
||||
map[string]map[string]string{"alan": {"expression": "beautiful"}},
|
||||
nil, nil, nil,
|
||||
"Goodbye beautiful world!",
|
||||
},
|
||||
{
|
||||
"nested paths with empty string value",
|
||||
"Goodbye {{alan/expression}} world!",
|
||||
map[string]map[string]string{"alan": {"expression": ""}},
|
||||
nil, nil, nil,
|
||||
"Goodbye world!",
|
||||
},
|
||||
{
|
||||
"literal paths (1)",
|
||||
"Goodbye {{[@alan]/expression}} world!",
|
||||
map[string]map[string]string{"@alan": {"expression": "beautiful"}},
|
||||
nil, nil, nil,
|
||||
"Goodbye beautiful world!",
|
||||
},
|
||||
{
|
||||
"literal paths (2)",
|
||||
"Goodbye {{[foo bar]/expression}} world!",
|
||||
map[string]map[string]string{"foo bar": {"expression": "beautiful"}},
|
||||
nil, nil, nil,
|
||||
"Goodbye beautiful world!",
|
||||
},
|
||||
{
|
||||
"literal references",
|
||||
"Goodbye {{[foo bar]}} world!",
|
||||
map[string]string{"foo bar": "beautiful"},
|
||||
nil, nil, nil,
|
||||
"Goodbye beautiful world!",
|
||||
},
|
||||
// @note MMm ok, well... no... I don't see the purpose of that test
|
||||
{
|
||||
"that current context path ({{.}}) doesn't hit helpers",
|
||||
"test: {{.}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"helper": func() string {
|
||||
panic("fail")
|
||||
}},
|
||||
nil,
|
||||
"test: ",
|
||||
},
|
||||
{
|
||||
"complex but empty paths (1)",
|
||||
"{{person/name}}",
|
||||
map[string]map[string]interface{}{"person": {"name": nil}},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"complex but empty paths (2)",
|
||||
"{{person/name}}",
|
||||
map[string]map[string]string{"person": {}},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"this keyword in paths (1)",
|
||||
"{{#goodbyes}}{{this}}{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []string{"goodbye", "Goodbye", "GOODBYE"}},
|
||||
nil, nil, nil,
|
||||
"goodbyeGoodbyeGOODBYE",
|
||||
},
|
||||
{
|
||||
"this keyword in paths (2)",
|
||||
"{{#hellos}}{{this/text}}{{/hellos}}",
|
||||
map[string]interface{}{"hellos": []interface{}{
|
||||
map[string]string{"text": "hello"},
|
||||
map[string]string{"text": "Hello"},
|
||||
map[string]string{"text": "HELLO"},
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"helloHelloHELLO",
|
||||
},
|
||||
{
|
||||
"this keyword nested inside path' (1)",
|
||||
"{{[this]}}",
|
||||
map[string]string{"this": "bar"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"this keyword nested inside path' (2)",
|
||||
"{{text/[this]}}",
|
||||
map[string]map[string]string{"text": {"this": "bar"}},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"this keyword in helpers (1)",
|
||||
"{{#goodbyes}}{{foo this}}{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []string{"goodbye", "Goodbye", "GOODBYE"}},
|
||||
nil,
|
||||
map[string]interface{}{"foo": barSuffixHelper},
|
||||
nil,
|
||||
"bar goodbyebar Goodbyebar GOODBYE",
|
||||
},
|
||||
{
|
||||
"this keyword in helpers (2)",
|
||||
"{{#hellos}}{{foo this/text}}{{/hellos}}",
|
||||
map[string]interface{}{"hellos": []map[string]string{{"text": "hello"}, {"text": "Hello"}, {"text": "HELLO"}}},
|
||||
nil,
|
||||
map[string]interface{}{"foo": barSuffixHelper},
|
||||
nil,
|
||||
"bar hellobar Hellobar HELLO",
|
||||
},
|
||||
{
|
||||
"this keyword nested inside helpers param (1)",
|
||||
"{{foo [this]}}",
|
||||
map[string]interface{}{"this": "bar"},
|
||||
nil,
|
||||
map[string]interface{}{"foo": echoHelper},
|
||||
nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"this keyword nested inside helpers param (2)",
|
||||
"{{foo text/[this]}}",
|
||||
map[string]map[string]string{"text": {"this": "bar"}},
|
||||
nil,
|
||||
map[string]interface{}{"foo": echoHelper},
|
||||
nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"pass string literals (1)",
|
||||
`{{"foo"}}`,
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass string literals (2)",
|
||||
`{{"foo"}}`,
|
||||
map[string]string{"foo": "bar"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"pass string literals (3)",
|
||||
`{{#"foo"}}{{.}}{{/"foo"}}`,
|
||||
map[string]interface{}{"foo": []string{"bar", "baz"}},
|
||||
nil, nil, nil,
|
||||
"barbaz",
|
||||
},
|
||||
{
|
||||
"pass number literals (1)",
|
||||
"{{12}}",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass number literals (2)",
|
||||
"{{12}}",
|
||||
map[string]string{"12": "bar"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"pass number literals (3)",
|
||||
"{{12.34}}",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass number literals (4)",
|
||||
"{{12.34}}",
|
||||
map[string]string{"12.34": "bar"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"pass number literals (5)",
|
||||
"{{12.34 1}}",
|
||||
map[string]interface{}{"12.34": func(context string) string {
|
||||
return "bar" + context
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"bar1",
|
||||
},
|
||||
{
|
||||
"pass boolean literals (1)",
|
||||
"{{true}}",
|
||||
map[string]string{},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass boolean literals (2)",
|
||||
"{{true}}",
|
||||
map[string]string{"": "foo"},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"pass boolean literals (3)",
|
||||
"{{false}}",
|
||||
map[string]string{"false": "foo"},
|
||||
nil, nil, nil,
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
"should handle literals in subexpression",
|
||||
"{{foo (false)}}",
|
||||
map[string]interface{}{"false": func() string { return "bar" }},
|
||||
nil,
|
||||
map[string]interface{}{"foo": func(context string) string {
|
||||
return context
|
||||
}},
|
||||
nil,
|
||||
"bar",
|
||||
},
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
launchTests(t, basicTests)
|
||||
}
|
||||
|
||||
func TestBasicErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var err error
|
||||
|
||||
inputs := []string{
|
||||
// this keyword nested inside path
|
||||
"{{#hellos}}{{text/this/foo}}{{/hellos}}",
|
||||
// this keyword nested inside helpers param
|
||||
"{{#hellos}}{{foo text/this/foo}}{{/hellos}}",
|
||||
}
|
||||
|
||||
expectedError := regexp.QuoteMeta("Invalid path: text/this")
|
||||
|
||||
for _, input := range inputs {
|
||||
_, err = raymond.Parse(input)
|
||||
if err == nil {
|
||||
t.Errorf("Test failed - Error expected")
|
||||
}
|
||||
|
||||
match, errMatch := regexp.MatchString(expectedError, fmt.Sprint(err))
|
||||
if errMatch != nil {
|
||||
panic("Failed to match regexp")
|
||||
}
|
||||
|
||||
if !match {
|
||||
t.Errorf("Test failed - Expected error:\n\t%s\n\nGot:\n\t%s", expectedError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
package handlebars
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/blocks.js
|
||||
//
|
||||
var blocksTests = []Test{
|
||||
{
|
||||
"array (1) - Arrays iterate over the contents when not empty",
|
||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"array (2) - Arrays ignore the contents when empty",
|
||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"array without data",
|
||||
"{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE",
|
||||
},
|
||||
{
|
||||
"array with @index - The @index variable is used",
|
||||
"{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"empty block (1) - Arrays iterate over the contents when not empty",
|
||||
"{{#goodbyes}}{{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"empty block (1) - Arrays ignore the contents when empty",
|
||||
"{{#goodbyes}}{{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"block with complex lookup - Templates can access variables in contexts up the stack with relative path syntax",
|
||||
"{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"},
|
||||
nil, nil, nil,
|
||||
"goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
|
||||
},
|
||||
{
|
||||
"multiple blocks with complex lookup",
|
||||
"{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"},
|
||||
nil, nil, nil,
|
||||
"AlanAlanAlanAlanAlanAlan",
|
||||
},
|
||||
|
||||
// @todo "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}" should throw error
|
||||
|
||||
{
|
||||
"block with deep nested complex lookup",
|
||||
"{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}",
|
||||
map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"sibling": "sad", "inner": []map[string]string{{"text": "goodbye"}}}}},
|
||||
nil, nil, nil,
|
||||
"Goodbye cruel sad OMG!",
|
||||
},
|
||||
{
|
||||
"inverted sections with unset value - Inverted section rendered when value isn't set.",
|
||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
||||
map[string]interface{}{},
|
||||
nil, nil, nil,
|
||||
"Right On!",
|
||||
},
|
||||
{
|
||||
"inverted sections with false value - Inverted section rendered when value is false.",
|
||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": false},
|
||||
nil, nil, nil,
|
||||
"Right On!",
|
||||
},
|
||||
{
|
||||
"inverted section with empty set - Inverted section rendered when value is empty set.",
|
||||
"{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}",
|
||||
map[string]interface{}{"goodbyes": []interface{}{}},
|
||||
nil, nil, nil,
|
||||
"Right On!",
|
||||
},
|
||||
{
|
||||
"block inverted sections",
|
||||
"{{#people}}{{name}}{{^}}{{none}}{{/people}}",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
{
|
||||
"chained inverted sections (1)",
|
||||
"{{#people}}{{name}}{{else if none}}{{none}}{{/people}}",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
{
|
||||
"chained inverted sections (2)",
|
||||
"{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
{
|
||||
"chained inverted sections (3)",
|
||||
"{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
|
||||
// @todo "{{#people}}{{name}}{{else if none}}{{none}}{{/if}}" should throw error
|
||||
|
||||
{
|
||||
"block inverted sections with empty arrays",
|
||||
"{{#people}}{{name}}{{^}}{{none}}{{/people}}",
|
||||
map[string]interface{}{"none": "No people", "people": map[string]interface{}{}},
|
||||
nil, nil, nil,
|
||||
"No people",
|
||||
},
|
||||
{
|
||||
"block standalone else sections (1)",
|
||||
"{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"block standalone else sections (2)",
|
||||
"{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"block standalone else sections (3)",
|
||||
"{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"block standalone chained else sections (1)",
|
||||
"{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"block standalone chained else sections (2)",
|
||||
"{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n",
|
||||
map[string]interface{}{"none": "No people"},
|
||||
nil, nil, nil,
|
||||
"No people\n",
|
||||
},
|
||||
{
|
||||
"should handle nesting",
|
||||
"{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.",
|
||||
map[string]interface{}{"data": []int{1, 3, 5}},
|
||||
nil, nil, nil,
|
||||
"1\n3\n5\nOK.",
|
||||
},
|
||||
// // @todo compat mode
|
||||
// {
|
||||
// "block with deep recursive lookup lookup",
|
||||
// "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}",
|
||||
// map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"inner": []map[string]string{{"text": "goodbye"}}}}},
|
||||
// nil,
|
||||
// nil,
|
||||
// nil,
|
||||
// "Goodbye cruel OMG!",
|
||||
// },
|
||||
// // @todo compat mode
|
||||
// {
|
||||
// "block with deep recursive pathed lookup",
|
||||
// "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}",
|
||||
// map[string]interface{}{"omg": map[string]string{"yes": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}},
|
||||
// nil,
|
||||
// nil,
|
||||
// nil,
|
||||
// "Goodbye cruel OMG!",
|
||||
// },
|
||||
{
|
||||
"block with missed recursive lookup",
|
||||
"{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}",
|
||||
map[string]interface{}{"omg": map[string]string{"no": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}},
|
||||
nil, nil, nil,
|
||||
"Goodbye cruel ",
|
||||
},
|
||||
}
|
||||
|
||||
func TestBlocks(t *testing.T) {
|
||||
launchTests(t, blocksTests)
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
package handlebars
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/builtin.js
|
||||
//
|
||||
var builtinsTests = []Test{
|
||||
{
|
||||
"#if - if with boolean argument shows the contents when true",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": true, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with string argument shows the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": "dummy", "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with boolean argument does not show the contents when false",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": false, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with undefined does not show the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with non-empty array shows the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": []string{"foo"}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with empty array does not show the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": []string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with zero does not show the contents",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": 0, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with zero and includeZero option shows the contents",
|
||||
"{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbye": 0, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with function shows the contents when function returns true",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbye": func() bool { return true },
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with function shows the contents when function returns string",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbye": func() string { return "world" },
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
"GOODBYE cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with function does not show the contents when returns false",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbye": func() bool { return false },
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#if - if with function does not show the contents when returns undefined",
|
||||
"{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbye": func() interface{} { return nil },
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#with",
|
||||
"{{#with person}}{{first}} {{last}}{{/with}}",
|
||||
map[string]interface{}{"person": map[string]string{"first": "Alan", "last": "Johnson"}},
|
||||
nil, nil, nil,
|
||||
"Alan Johnson",
|
||||
},
|
||||
{
|
||||
"#with - with with function argument",
|
||||
"{{#with person}}{{first}} {{last}}{{/with}}",
|
||||
map[string]interface{}{
|
||||
"person": func() map[string]string { return map[string]string{"first": "Alan", "last": "Johnson"} },
|
||||
}, nil, nil, nil,
|
||||
"Alan Johnson",
|
||||
},
|
||||
{
|
||||
"#with - with with else",
|
||||
"{{#with person}}Person is present{{else}}Person is not present{{/with}}",
|
||||
map[string]interface{}{},
|
||||
nil, nil, nil,
|
||||
"Person is not present",
|
||||
},
|
||||
|
||||
{
|
||||
"#each - each with array argument iterates over the contents when not empty",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each with array argument ignores the contents when empty",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each without data (1)",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each without data (2)",
|
||||
"{{#each .}}{{.}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": "cruel", "world": "world"},
|
||||
nil, nil, nil,
|
||||
// note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation
|
||||
[]string{"cruelworld", "worldcruel"},
|
||||
},
|
||||
{
|
||||
"#each - each without context",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
nil, nil, nil, nil,
|
||||
"cruel !",
|
||||
},
|
||||
|
||||
// NOTE: we test with a map instead of an object
|
||||
{
|
||||
"#each - each with an object and @key (map)",
|
||||
"{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": map[interface{}]map[string]string{"<b>#1</b>": {"text": "goodbye"}, 2: {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
[]string{"<b>#1</b>. goodbye! 2. GOODBYE! cruel world!", "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"},
|
||||
},
|
||||
// NOTE: An additional test with a struct, but without an html stuff for the key, because it is impossible
|
||||
{
|
||||
"#each - each with an object and @key (struct)",
|
||||
"{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{
|
||||
"goodbyes": struct {
|
||||
Foo map[string]string
|
||||
Bar map[string]int
|
||||
}{map[string]string{"text": "baz"}, map[string]int{"text": 10}},
|
||||
"world": "world",
|
||||
},
|
||||
nil, nil, nil,
|
||||
[]string{"Foo. baz! Bar. 10! cruel world!", "Bar. 10! Foo. baz! cruel world!"},
|
||||
},
|
||||
{
|
||||
"#each - each with @index",
|
||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each with nested @index",
|
||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each with block params",
|
||||
"{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!",
|
||||
},
|
||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
||||
{
|
||||
"#each - each object with @index",
|
||||
"{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"a": {"text": "goodbye"}, "b": {"text": "Goodbye"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
[]string{"0. goodbye! 1. Goodbye! cruel world!", "0. Goodbye! 1. goodbye! cruel world!"},
|
||||
},
|
||||
{
|
||||
"#each - each with nested @first",
|
||||
"{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!",
|
||||
},
|
||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
||||
{
|
||||
"#each - each object with @first",
|
||||
"{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"foo": {"text": "goodbye"}, "bar": {"text": "Goodbye"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
[]string{"goodbye! cruel world!", "Goodbye! cruel world!"},
|
||||
},
|
||||
{
|
||||
"#each - each with @last",
|
||||
"{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"GOODBYE! cruel world!",
|
||||
},
|
||||
// @note: That test differs from JS impl because maps and structs are not ordered in go
|
||||
{
|
||||
"#each - each object with @last",
|
||||
"{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": map[string]map[string]string{"foo": {"text": "goodbye"}, "bar": {"text": "Goodbye"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
[]string{"goodbye! cruel world!", "Goodbye! cruel world!"},
|
||||
},
|
||||
{
|
||||
"#each - each with nested @last",
|
||||
"{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!",
|
||||
},
|
||||
|
||||
{
|
||||
"#each - each with function argument (1)",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": func() []map[string]string {
|
||||
return []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}
|
||||
}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - each with function argument (2)",
|
||||
"{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!",
|
||||
map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"},
|
||||
nil, nil, nil,
|
||||
"cruel world!",
|
||||
},
|
||||
{
|
||||
"#each - data passed to helpers",
|
||||
"{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}",
|
||||
map[string][]string{"letters": {"a", "b", "c"}},
|
||||
map[string]interface{}{"exclaim": "!"},
|
||||
map[string]interface{}{"detectDataInsideEach": detectDataHelper},
|
||||
nil,
|
||||
"a!b!c!",
|
||||
},
|
||||
|
||||
// @todo "each on implicit context" should throw error
|
||||
|
||||
// SKIP: #log - "should call logger at default level"
|
||||
// SKIP: #log - "should call logger at data level"
|
||||
// SKIP: #log - "should output to info"
|
||||
// SKIP: #log - "should log at data level"
|
||||
// SKIP: #log - "should handle missing logger"
|
||||
|
||||
// @note Test added
|
||||
// @todo Check log output
|
||||
{
|
||||
"#log",
|
||||
"{{log blah}}",
|
||||
map[string]string{"blah": "whee"},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
|
||||
// @note Test added
|
||||
{
|
||||
"#lookup - should lookup array element",
|
||||
"{{#each goodbyes}}{{lookup ../data @index}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
||||
nil, nil, nil,
|
||||
"foobar",
|
||||
},
|
||||
{
|
||||
"#lookup - should lookup map element",
|
||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []string{"foo", "bar"}, "data": map[string]string{"foo": "baz", "bar": "bat"}},
|
||||
nil, nil, nil,
|
||||
"bazbat",
|
||||
},
|
||||
{
|
||||
"#lookup - should lookup struct field",
|
||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []string{"Foo", "Bar"}, "data": struct {
|
||||
Foo string
|
||||
Bar string
|
||||
}{"baz", "bat"}},
|
||||
nil, nil, nil,
|
||||
"bazbat",
|
||||
},
|
||||
{
|
||||
"#lookup - should lookup arbitrary content",
|
||||
"{{#each goodbyes}}{{lookup ../data .}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
||||
nil, nil, nil,
|
||||
"foobar",
|
||||
},
|
||||
{
|
||||
"#lookup - should not fail on undefined value",
|
||||
"{{#each goodbyes}}{{lookup ../bar .}}{{/each}}",
|
||||
map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}},
|
||||
nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
func TestBuiltins(t *testing.T) {
|
||||
launchTests(t, builtinsTests)
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
package handlebars
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/data.js
|
||||
//
|
||||
var dataTests = []Test{
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with helpers",
|
||||
"{{hello}}",
|
||||
map[string]string{"noun": "cat"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + options.ValueStr("noun")
|
||||
}},
|
||||
nil,
|
||||
"happy cat",
|
||||
},
|
||||
{
|
||||
"data can be looked up via @foo",
|
||||
"{{@hello}}",
|
||||
nil,
|
||||
map[string]interface{}{"hello": "hello"},
|
||||
nil, nil,
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"deep @foo triggers automatic top-level data",
|
||||
`{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}`,
|
||||
map[string]bool{"foo": true},
|
||||
map[string]interface{}{"hello": "hello"},
|
||||
map[string]interface{}{"let": func(options *raymond.Options) string {
|
||||
frame := options.NewDataFrame()
|
||||
|
||||
for k, v := range options.Hash() {
|
||||
frame.Set(k, v)
|
||||
}
|
||||
|
||||
return options.FnData(frame)
|
||||
}},
|
||||
nil,
|
||||
"Hello world",
|
||||
},
|
||||
{
|
||||
"parameter data can be looked up via @foo",
|
||||
`{{hello @world}}`,
|
||||
nil,
|
||||
map[string]interface{}{"world": "world"},
|
||||
map[string]interface{}{"hello": func(context string) string {
|
||||
return "Hello " + context
|
||||
}},
|
||||
nil,
|
||||
"Hello world",
|
||||
},
|
||||
{
|
||||
"hash values can be looked up via @foo",
|
||||
`{{hello noun=@world}}`,
|
||||
nil,
|
||||
map[string]interface{}{"world": "world"},
|
||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
||||
return "Hello " + options.HashStr("noun")
|
||||
}},
|
||||
nil,
|
||||
"Hello world",
|
||||
},
|
||||
{
|
||||
"nested parameter data can be looked up via @foo.bar",
|
||||
`{{hello @world.bar}}`,
|
||||
nil,
|
||||
map[string]interface{}{"world": map[string]string{"bar": "world"}},
|
||||
map[string]interface{}{"hello": func(context string) string {
|
||||
return "Hello " + context
|
||||
}},
|
||||
nil,
|
||||
"Hello world",
|
||||
},
|
||||
{
|
||||
"nested parameter data does not fail with @world.bar",
|
||||
`{{hello @world.bar}}`,
|
||||
nil,
|
||||
map[string]interface{}{"foo": map[string]string{"bar": "world"}},
|
||||
map[string]interface{}{"hello": func(context string) string {
|
||||
return "Hello " + context
|
||||
}},
|
||||
nil,
|
||||
// @todo Test differs with JS implementation: we don't output `undefined`
|
||||
"Hello ",
|
||||
},
|
||||
|
||||
// @todo "parameter data throws when using complex scope references",
|
||||
|
||||
{
|
||||
"data can be functions",
|
||||
`{{@hello}}`,
|
||||
nil,
|
||||
map[string]interface{}{"hello": func() string { return "hello" }},
|
||||
nil, nil,
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"data can be functions with params",
|
||||
`{{@hello "hello"}}`,
|
||||
nil,
|
||||
map[string]interface{}{"hello": func(context string) string { return context }},
|
||||
nil, nil,
|
||||
"hello",
|
||||
},
|
||||
|
||||
{
|
||||
"data is inherited downstream",
|
||||
`{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}`,
|
||||
map[string]map[string]string{"bar": {"baz": "hello world"}},
|
||||
nil,
|
||||
map[string]interface{}{"let": func(options *raymond.Options) string {
|
||||
frame := options.NewDataFrame()
|
||||
|
||||
for k, v := range options.Hash() {
|
||||
frame.Set(k, v)
|
||||
}
|
||||
|
||||
return options.FnData(frame)
|
||||
}},
|
||||
nil,
|
||||
"2hello world1",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with helpers in partials",
|
||||
`{{>myPartial}}`,
|
||||
map[string]string{"noun": "cat"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{"hello": func(options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + options.ValueStr("noun")
|
||||
}},
|
||||
map[string]string{
|
||||
"myPartial": "{{hello}}",
|
||||
},
|
||||
"happy cat",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with helpers and parameters",
|
||||
`{{hello world}}`,
|
||||
map[string]interface{}{"exclaim": true, "world": "world"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{"hello": func(context string, options *raymond.Options) string {
|
||||
str := "error"
|
||||
if b, ok := options.Value("exclaim").(bool); ok {
|
||||
if b {
|
||||
str = "!"
|
||||
} else {
|
||||
str = ""
|
||||
}
|
||||
}
|
||||
|
||||
return options.DataStr("adjective") + " " + context + str
|
||||
}},
|
||||
nil,
|
||||
"happy world!",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with block helpers",
|
||||
`{{#hello}}{{world}}{{/hello}}`,
|
||||
map[string]bool{"exclaim": true},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
return options.Fn()
|
||||
},
|
||||
"world": func(options *raymond.Options) string {
|
||||
str := "error"
|
||||
if b, ok := options.Value("exclaim").(bool); ok {
|
||||
if b {
|
||||
str = "!"
|
||||
} else {
|
||||
str = ""
|
||||
}
|
||||
}
|
||||
|
||||
return options.DataStr("adjective") + " world" + str
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"happy world!",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - works with block helpers that use ..",
|
||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
return options.FnWith(map[string]string{"exclaim": "?"})
|
||||
},
|
||||
"world": func(context string, options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"happy world?",
|
||||
},
|
||||
{
|
||||
"passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..",
|
||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
||||
map[string]interface{}{"adjective": "happy", "accessData": "#win"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
return options.DataStr("accessData") + " " + options.FnWith(map[string]string{"exclaim": "?"})
|
||||
},
|
||||
"world": func(context string, options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"#win happy world?",
|
||||
},
|
||||
{
|
||||
"you can override inherited data when invoking a helper",
|
||||
`{{#hello}}{{world zomg}}{{/hello}}`,
|
||||
map[string]interface{}{"exclaim": true, "zomg": "planet"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
ctx := map[string]string{"exclaim": "?", "zomg": "world"}
|
||||
data := options.NewDataFrame()
|
||||
data.Set("adjective", "sad")
|
||||
|
||||
return options.FnCtxData(ctx, data)
|
||||
},
|
||||
"world": func(context string, options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"sad world?",
|
||||
},
|
||||
{
|
||||
"you can override inherited data when invoking a helper with depth",
|
||||
`{{#hello}}{{world ../zomg}}{{/hello}}`,
|
||||
map[string]interface{}{"exclaim": true, "zomg": "world"},
|
||||
map[string]interface{}{"adjective": "happy"},
|
||||
map[string]interface{}{
|
||||
"hello": func(options *raymond.Options) string {
|
||||
ctx := map[string]string{"exclaim": "?"}
|
||||
data := options.NewDataFrame()
|
||||
data.Set("adjective", "sad")
|
||||
|
||||
return options.FnCtxData(ctx, data)
|
||||
},
|
||||
"world": func(context string, options *raymond.Options) string {
|
||||
return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim")
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"sad world?",
|
||||
},
|
||||
{
|
||||
"@root - the root context can be looked up via @root",
|
||||
`{{@root.foo}}`,
|
||||
map[string]interface{}{"foo": "hello"},
|
||||
nil, nil, nil,
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"@root - passed root values take priority",
|
||||
`{{@root.foo}}`,
|
||||
nil,
|
||||
map[string]interface{}{"root": map[string]string{"foo": "hello"}},
|
||||
nil, nil,
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"nesting - the root context can be looked up via @root",
|
||||
`{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}`,
|
||||
map[string]interface{}{"foo": "hello"},
|
||||
map[string]interface{}{"depth": 0},
|
||||
map[string]interface{}{
|
||||
"helper": func(options *raymond.Options) string {
|
||||
data := options.NewDataFrame()
|
||||
|
||||
if depth, ok := options.Data("depth").(int); ok {
|
||||
data.Set("depth", depth+1)
|
||||
}
|
||||
|
||||
return options.FnData(data)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"2 1 0",
|
||||
},
|
||||
}
|
||||
|
||||
func TestData(t *testing.T) {
|
||||
launchTests(t, dataTests)
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package handlebars contains all the tests that come from handlebars.js project.
|
||||
package handlebars
|
||||
+665
@@ -0,0 +1,665 @@
|
||||
package handlebars
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
func barSuffixHelper(context string) string {
|
||||
return "bar " + context
|
||||
}
|
||||
|
||||
func echoHelper(str string) string {
|
||||
return str
|
||||
}
|
||||
|
||||
func echoNbHelper(str string, nb int) string {
|
||||
result := ""
|
||||
for i := 0; i < nb; i++ {
|
||||
result += str
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func linkHelper(prefix string, options *raymond.Options) string {
|
||||
return fmt.Sprintf(`<a href="%s/%s">%s</a>`, prefix, options.ValueStr("url"), options.ValueStr("text"))
|
||||
}
|
||||
|
||||
func rawHelper(options *raymond.Options) string {
|
||||
return options.Fn()
|
||||
}
|
||||
|
||||
func rawThreeHelper(a, b, c string, options *raymond.Options) string {
|
||||
return options.Fn() + a + b + c
|
||||
}
|
||||
|
||||
func formHelper(options *raymond.Options) string {
|
||||
return "<form>" + options.Fn() + "</form>"
|
||||
}
|
||||
|
||||
func formCtxHelper(context interface{}, options *raymond.Options) string {
|
||||
return "<form>" + options.FnWith(context) + "</form>"
|
||||
}
|
||||
|
||||
func listHelper(context interface{}, options *raymond.Options) string {
|
||||
val := reflect.ValueOf(context)
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if val.Len() > 0 {
|
||||
result := "<ul>"
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
result += "<li>"
|
||||
result += options.FnWith(val.Index(i).Interface())
|
||||
result += "</li>"
|
||||
}
|
||||
result += "</ul>"
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return "<p>" + options.Inverse() + "</p>"
|
||||
}
|
||||
|
||||
func blogHelper(val string) string {
|
||||
return "val is " + val
|
||||
}
|
||||
|
||||
func equalHelper(a, b string) string {
|
||||
return raymond.Str(a == b)
|
||||
}
|
||||
|
||||
func dashHelper(a, b string) string {
|
||||
return a + "-" + b
|
||||
}
|
||||
|
||||
func concatHelper(a, b string) string {
|
||||
return a + b
|
||||
}
|
||||
|
||||
func detectDataHelper(options *raymond.Options) string {
|
||||
if val, ok := options.DataFrame().Get("exclaim").(string); ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/helper.js
|
||||
//
|
||||
var helpersTests = []Test{
|
||||
{
|
||||
"helper with complex lookup",
|
||||
"{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}",
|
||||
map[string]interface{}{"prefix": "/root", "goodbyes": []map[string]string{{"text": "Goodbye", "url": "goodbye"}}},
|
||||
nil,
|
||||
map[string]interface{}{"link": linkHelper},
|
||||
nil,
|
||||
`<a href="/root/goodbye">Goodbye</a>`,
|
||||
},
|
||||
{
|
||||
"helper for raw block gets raw content",
|
||||
"{{{{raw}}}} {{test}} {{{{/raw}}}}",
|
||||
map[string]interface{}{"test": "hello"},
|
||||
nil,
|
||||
map[string]interface{}{"raw": rawHelper},
|
||||
nil,
|
||||
" {{test}} ",
|
||||
},
|
||||
{
|
||||
"helper for raw block gets parameters",
|
||||
"{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}",
|
||||
map[string]interface{}{"test": "hello"},
|
||||
nil,
|
||||
map[string]interface{}{"raw": rawThreeHelper},
|
||||
nil,
|
||||
" {{test}} 123",
|
||||
},
|
||||
{
|
||||
"helper block with complex lookup expression",
|
||||
"{{#goodbyes}}{{../name}}{{/goodbyes}}",
|
||||
map[string]interface{}{"name": "Alan"},
|
||||
nil,
|
||||
map[string]interface{}{"goodbyes": func(options *raymond.Options) string {
|
||||
out := ""
|
||||
for _, str := range []string{"Goodbye", "goodbye", "GOODBYE"} {
|
||||
out += str + " " + options.FnWith(str) + "! "
|
||||
}
|
||||
return out
|
||||
}},
|
||||
nil,
|
||||
"Goodbye Alan! goodbye Alan! GOODBYE Alan! ",
|
||||
},
|
||||
{
|
||||
"helper with complex lookup and nested template",
|
||||
"{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}",
|
||||
map[string]interface{}{"prefix": "/root", "goodbyes": []map[string]string{{"text": "Goodbye", "url": "goodbye"}}},
|
||||
nil,
|
||||
map[string]interface{}{"link": linkHelper},
|
||||
nil,
|
||||
`<a href="/root/goodbye">Goodbye</a>`,
|
||||
},
|
||||
{
|
||||
// note: The JS implementation returns undefined, we return empty string
|
||||
"helper returning undefined value (1)",
|
||||
" {{nothere}}",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{"nothere": func() string {
|
||||
return ""
|
||||
}},
|
||||
nil,
|
||||
" ",
|
||||
},
|
||||
{
|
||||
// note: The JS implementation returns undefined, we return empty string
|
||||
"helper returning undefined value (2)",
|
||||
" {{#nothere}}{{/nothere}}",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{"nothere": func() string {
|
||||
return ""
|
||||
}},
|
||||
nil,
|
||||
" ",
|
||||
},
|
||||
{
|
||||
"block helper",
|
||||
"{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!",
|
||||
map[string]interface{}{"world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{"goodbyes": func(options *raymond.Options) string {
|
||||
return options.FnWith(map[string]string{"text": "GOODBYE"})
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE! cruel world!",
|
||||
},
|
||||
{
|
||||
"block helper staying in the same context",
|
||||
"{{#form}}<p>{{name}}</p>{{/form}}",
|
||||
map[string]interface{}{"name": "Yehuda"},
|
||||
nil,
|
||||
map[string]interface{}{"form": formHelper},
|
||||
nil,
|
||||
"<form><p>Yehuda</p></form>",
|
||||
},
|
||||
{
|
||||
"block helper should have context in this",
|
||||
"<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>",
|
||||
map[string]interface{}{"people": []map[string]interface{}{{"name": "Alan", "id": 1}, {"name": "Yehuda", "id": 2}}},
|
||||
nil,
|
||||
map[string]interface{}{"link": func(options *raymond.Options) string {
|
||||
return fmt.Sprintf("<a href=\"/people/%s\">%s</a>", options.ValueStr("id"), options.Fn())
|
||||
}},
|
||||
nil,
|
||||
`<ul><li><a href="/people/1">Alan</a></li><li><a href="/people/2">Yehuda</a></li></ul>`,
|
||||
},
|
||||
{
|
||||
"block helper for undefined value",
|
||||
"{{#empty}}shouldn't render{{/empty}}",
|
||||
nil, nil, nil, nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"block helper passing a new context",
|
||||
"{{#form yehuda}}<p>{{name}}</p>{{/form}}",
|
||||
map[string]map[string]string{"yehuda": {"name": "Yehuda"}},
|
||||
nil,
|
||||
map[string]interface{}{"form": formCtxHelper},
|
||||
nil,
|
||||
"<form><p>Yehuda</p></form>",
|
||||
},
|
||||
{
|
||||
"block helper passing a complex path context",
|
||||
"{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}",
|
||||
map[string]map[string]interface{}{"yehuda": {"name": "Yehuda", "cat": map[string]string{"name": "Harold"}}},
|
||||
nil,
|
||||
map[string]interface{}{"form": formCtxHelper},
|
||||
nil,
|
||||
"<form><p>Harold</p></form>",
|
||||
},
|
||||
{
|
||||
"nested block helpers",
|
||||
"{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}",
|
||||
map[string]map[string]string{"yehuda": {"name": "Yehuda"}},
|
||||
nil,
|
||||
map[string]interface{}{"link": func(options *raymond.Options) string {
|
||||
return fmt.Sprintf("<a href=\"%s\">%s</a>", options.ValueStr("name"), options.Fn())
|
||||
}, "form": formCtxHelper},
|
||||
nil,
|
||||
`<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>`,
|
||||
},
|
||||
{
|
||||
"block helper inverted sections (1) - an inverse wrapper is passed in as a new context",
|
||||
"{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}",
|
||||
map[string][]map[string]string{"people": {{"name": "Alan"}, {"name": "Yehuda"}}},
|
||||
nil,
|
||||
map[string]interface{}{"list": listHelper},
|
||||
nil,
|
||||
`<ul><li>Alan</li><li>Yehuda</li></ul>`,
|
||||
},
|
||||
{
|
||||
"block helper inverted sections (2) - an inverse wrapper can be optionally called",
|
||||
"{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}",
|
||||
map[string][]map[string]string{"people": {}},
|
||||
nil,
|
||||
map[string]interface{}{"list": listHelper},
|
||||
nil,
|
||||
`<p><em>Nobody's here</em></p>`,
|
||||
},
|
||||
{
|
||||
"block helper inverted sections (3) - the context of an inverse is the parent of the block",
|
||||
"{{#list people}}Hello{{^}}{{message}}{{/list}}",
|
||||
map[string]interface{}{"people": []interface{}{}, "message": "Nobody's here"},
|
||||
nil,
|
||||
map[string]interface{}{"list": listHelper},
|
||||
nil,
|
||||
`<p>Nobody's here</p>`,
|
||||
},
|
||||
|
||||
{
|
||||
"pathed lambdas with parameters (1)",
|
||||
"{{./helper 1}}",
|
||||
map[string]interface{}{
|
||||
"helper": func(param int) string { return "winning" },
|
||||
"hash": map[string]interface{}{
|
||||
"helper": func(param int) string { return "winning" },
|
||||
}},
|
||||
nil,
|
||||
map[string]interface{}{"./helper": func(param int) string { return "fail" }},
|
||||
nil,
|
||||
"winning",
|
||||
},
|
||||
{
|
||||
"pathed lambdas with parameters (2)",
|
||||
"{{hash/helper 1}}",
|
||||
map[string]interface{}{
|
||||
"helper": func(param int) string { return "winning" },
|
||||
"hash": map[string]interface{}{
|
||||
"helper": func(param int) string { return "winning" },
|
||||
}},
|
||||
nil,
|
||||
map[string]interface{}{"./helper": func(param int) string { return "fail" }},
|
||||
nil,
|
||||
"winning",
|
||||
},
|
||||
|
||||
{
|
||||
"helpers hash - providing a helpers hash (1)",
|
||||
"Goodbye {{cruel}} {{world}}!",
|
||||
map[string]interface{}{"cruel": "cruel"},
|
||||
nil,
|
||||
map[string]interface{}{"world": func() string { return "world" }},
|
||||
nil,
|
||||
"Goodbye cruel world!",
|
||||
},
|
||||
{
|
||||
"helpers hash - providing a helpers hash (2)",
|
||||
"Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!",
|
||||
map[string]interface{}{"iter": []map[string]string{{"cruel": "cruel"}}},
|
||||
nil,
|
||||
map[string]interface{}{"world": func() string { return "world" }},
|
||||
nil,
|
||||
"Goodbye cruel world!",
|
||||
},
|
||||
{
|
||||
"helpers hash - in cases of conflict, helpers win (1)",
|
||||
"{{{lookup}}}",
|
||||
map[string]interface{}{"lookup": "Explicit"},
|
||||
nil,
|
||||
map[string]interface{}{"lookup": func() string { return "helpers" }},
|
||||
nil,
|
||||
"helpers",
|
||||
},
|
||||
{
|
||||
"helpers hash - in cases of conflict, helpers win (2)",
|
||||
"{{lookup}}",
|
||||
map[string]interface{}{"lookup": "Explicit"},
|
||||
nil,
|
||||
map[string]interface{}{"lookup": func() string { return "helpers" }},
|
||||
nil,
|
||||
"helpers",
|
||||
},
|
||||
{
|
||||
"helpers hash - the helpers hash is available is nested contexts",
|
||||
"{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
|
||||
map[string]interface{}{"outer": map[string]interface{}{"inner": map[string]interface{}{"unused": []string{}}}},
|
||||
nil,
|
||||
map[string]interface{}{"helper": func() string { return "helper" }},
|
||||
nil,
|
||||
"helper",
|
||||
},
|
||||
|
||||
// @todo "helpers hash - the helper hash should augment the global hash"
|
||||
|
||||
// @todo "registration"
|
||||
|
||||
{
|
||||
"decimal number literals work",
|
||||
"Message: {{hello -1.2 1.2}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(times, times2 interface{}) string {
|
||||
ts, t2s := "NaN", "NaN"
|
||||
|
||||
if v, ok := times.(float64); ok {
|
||||
ts = raymond.Str(v)
|
||||
}
|
||||
|
||||
if v, ok := times2.(float64); ok {
|
||||
t2s = raymond.Str(v)
|
||||
}
|
||||
|
||||
return "Hello " + ts + " " + t2s + " times"
|
||||
}},
|
||||
nil,
|
||||
"Message: Hello -1.2 1.2 times",
|
||||
},
|
||||
{
|
||||
"negative number literals work",
|
||||
"Message: {{hello -12}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(times interface{}) string {
|
||||
ts := "NaN"
|
||||
|
||||
if v, ok := times.(int); ok {
|
||||
ts = raymond.Str(v)
|
||||
}
|
||||
|
||||
return "Hello " + ts + " times"
|
||||
}},
|
||||
nil,
|
||||
"Message: Hello -12 times",
|
||||
},
|
||||
|
||||
{
|
||||
"String literal parameters - simple literals work",
|
||||
`Message: {{hello "world" 12 true false}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(p, t, b, b2 interface{}) string {
|
||||
times, bool1, bool2 := "NaN", "NaB", "NaB"
|
||||
|
||||
param, ok := p.(string)
|
||||
if !ok {
|
||||
param = "NaN"
|
||||
}
|
||||
|
||||
if v, ok := t.(int); ok {
|
||||
times = raymond.Str(v)
|
||||
}
|
||||
|
||||
if v, ok := b.(bool); ok {
|
||||
bool1 = raymond.Str(v)
|
||||
}
|
||||
|
||||
if v, ok := b2.(bool); ok {
|
||||
bool2 = raymond.Str(v)
|
||||
}
|
||||
|
||||
return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2
|
||||
}},
|
||||
nil,
|
||||
"Message: Hello world 12 times: true false",
|
||||
},
|
||||
|
||||
// @todo "using a quote in the middle of a parameter raises an error"
|
||||
|
||||
{
|
||||
"String literal parameters - escaping a String is possible",
|
||||
"Message: {{{hello \"\\\"world\\\"\"}}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(param string) string {
|
||||
return "Hello " + param
|
||||
}},
|
||||
nil,
|
||||
`Message: Hello "world"`,
|
||||
},
|
||||
{
|
||||
"String literal parameters - it works with ' marks",
|
||||
"Message: {{{hello \"Alan's world\"}}}",
|
||||
nil, nil,
|
||||
map[string]interface{}{"hello": func(param string) string {
|
||||
return "Hello " + param
|
||||
}},
|
||||
nil,
|
||||
`Message: Hello Alan's world`,
|
||||
},
|
||||
|
||||
{
|
||||
"multiple parameters - simple multi-params work",
|
||||
"Message: {{goodbye cruel world}}",
|
||||
map[string]string{"cruel": "cruel", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{"goodbye": func(cruel, world string) string {
|
||||
return "Goodbye " + cruel + " " + world
|
||||
}},
|
||||
nil,
|
||||
"Message: Goodbye cruel world",
|
||||
},
|
||||
{
|
||||
"multiple parameters - block multi-params work",
|
||||
"Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}",
|
||||
map[string]string{"cruel": "cruel", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{"goodbye": func(cruel, world string, options *raymond.Options) string {
|
||||
return options.FnWith(map[string]interface{}{"greeting": "Goodbye", "adj": cruel, "noun": world})
|
||||
}},
|
||||
nil,
|
||||
"Message: Goodbye cruel world",
|
||||
},
|
||||
|
||||
{
|
||||
"hash - helpers can take an optional hash",
|
||||
`{{goodbye cruel="CRUEL" world="WORLD" times=12}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world") + " " + options.HashStr("times") + " TIMES"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL WORLD 12 TIMES",
|
||||
},
|
||||
{
|
||||
"hash - helpers can take an optional hash with booleans (1)",
|
||||
`{{goodbye cruel="CRUEL" world="WORLD" print=true}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world")
|
||||
}
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL WORLD",
|
||||
},
|
||||
{
|
||||
"hash - helpers can take an optional hash with booleans (2)",
|
||||
`{{goodbye cruel="CRUEL" world="WORLD" print=false}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.HashStr("world")
|
||||
}
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"NOT PRINTING",
|
||||
},
|
||||
{
|
||||
"block helpers can take an optional hash",
|
||||
`{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn() + " " + options.HashStr("times") + " TIMES"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL world 12 TIMES",
|
||||
},
|
||||
{
|
||||
"block helpers can take an optional hash with single quoted stings",
|
||||
`{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn() + " " + options.HashStr("times") + " TIMES"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL world 12 TIMES",
|
||||
},
|
||||
{
|
||||
"block helpers can take an optional hash with booleans (1)",
|
||||
`{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
||||
}
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"GOODBYE CRUEL world",
|
||||
},
|
||||
{
|
||||
"block helpers can take an optional hash with booleans (1)",
|
||||
`{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
||||
}
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"NOT PRINTING",
|
||||
},
|
||||
|
||||
// @todo "helperMissing - if a context is not found, helperMissing is used" throw error
|
||||
|
||||
// @todo "helperMissing - if a context is not found, custom helperMissing is used"
|
||||
|
||||
// @todo "helperMissing - if a value is not found, custom helperMissing is used"
|
||||
|
||||
{
|
||||
"block helpers can take an optional hash with booleans (1)",
|
||||
`{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"goodbye": func(options *raymond.Options) string {
|
||||
p, ok := options.HashProp("print").(bool)
|
||||
if ok {
|
||||
if p {
|
||||
return "GOODBYE " + options.HashStr("cruel") + " " + options.Fn()
|
||||
}
|
||||
return "NOT PRINTING"
|
||||
}
|
||||
|
||||
return "THIS SHOULD NOT HAPPEN"
|
||||
}},
|
||||
nil,
|
||||
"NOT PRINTING",
|
||||
},
|
||||
|
||||
// @todo "knownHelpers/knownHelpersOnly" tests
|
||||
|
||||
// @todo "blockHelperMissing" tests
|
||||
|
||||
// @todo "name field" tests
|
||||
|
||||
{
|
||||
"name conflicts - helpers take precedence over same-named context properties",
|
||||
`{{goodbye}} {{cruel world}}`,
|
||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"goodbye": func(options *raymond.Options) string {
|
||||
return strings.ToUpper(options.ValueStr("goodbye"))
|
||||
},
|
||||
"cruel": func(world string) string {
|
||||
return "cruel " + strings.ToUpper(world)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"GOODBYE cruel WORLD",
|
||||
},
|
||||
{
|
||||
"name conflicts - helpers take precedence over same-named context properties",
|
||||
`{{#goodbye}} {{cruel world}}{{/goodbye}}`,
|
||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"goodbye": func(options *raymond.Options) string {
|
||||
return strings.ToUpper(options.ValueStr("goodbye")) + options.Fn()
|
||||
},
|
||||
"cruel": func(world string) string {
|
||||
return "cruel " + strings.ToUpper(world)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"GOODBYE cruel WORLD",
|
||||
},
|
||||
{
|
||||
"name conflicts - Scoped names take precedence over helpers",
|
||||
`{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}`,
|
||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"goodbye": func(options *raymond.Options) string {
|
||||
return strings.ToUpper(options.ValueStr("goodbye"))
|
||||
},
|
||||
"cruel": func(world string) string {
|
||||
return "cruel " + strings.ToUpper(world)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"goodbye cruel WORLD cruel GOODBYE",
|
||||
},
|
||||
{
|
||||
"name conflicts - Scoped names take precedence over block helpers",
|
||||
`{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}`,
|
||||
map[string]string{"goodbye": "goodbye", "world": "world"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"goodbye": func(options *raymond.Options) string {
|
||||
return strings.ToUpper(options.ValueStr("goodbye")) + options.Fn()
|
||||
},
|
||||
"cruel": func(world string) string {
|
||||
return "cruel " + strings.ToUpper(world)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"GOODBYE cruel WORLD goodbye",
|
||||
},
|
||||
|
||||
// @todo "block params" tests
|
||||
}
|
||||
|
||||
func TestHelpers(t *testing.T) {
|
||||
launchTests(t, helpersTests)
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package handlebars
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/partials.js
|
||||
//
|
||||
var partialsTests = []Test{
|
||||
{
|
||||
"basic partials",
|
||||
"Dudes: {{#dudes}}{{> dude}}{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}} ({{url}}) "},
|
||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
},
|
||||
{
|
||||
"dynamic partials",
|
||||
"Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil,
|
||||
map[string]interface{}{"partial": func() string {
|
||||
return "dude"
|
||||
}},
|
||||
map[string]string{"dude": "{{name}} ({{url}}) "},
|
||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
},
|
||||
|
||||
// @todo "failing dynamic partials"
|
||||
|
||||
{
|
||||
"partials with context",
|
||||
"Dudes: {{>dude dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{#this}}{{name}} ({{url}}) {{/this}}"},
|
||||
"Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
},
|
||||
{
|
||||
"partials with undefined context",
|
||||
"Dudes: {{>dude dudes}}",
|
||||
map[string]interface{}{},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{foo}} Empty"},
|
||||
"Dudes: Empty",
|
||||
},
|
||||
|
||||
// @todo "partials with duplicate parameters"
|
||||
|
||||
{
|
||||
"partials with parameters",
|
||||
"Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}",
|
||||
map[string]interface{}{"foo": "bar", "dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{others.foo}}{{name}} ({{url}}) "},
|
||||
"Dudes: barYehuda (http://yehuda) barAlan (http://alan) ",
|
||||
},
|
||||
{
|
||||
"partial in a partial",
|
||||
"Dudes: {{#dudes}}{{>dude}}{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}} {{> url}} ", "url": `<a href="{{url}}">{{url}}</a>`},
|
||||
`Dudes: Yehuda <a href="http://yehuda">http://yehuda</a> Alan <a href="http://alan">http://alan</a> `,
|
||||
},
|
||||
|
||||
// @todo "rendering undefined partial throws an exception"
|
||||
|
||||
// @todo "registering undefined partial throws an exception"
|
||||
|
||||
// SKIP: "rendering template partial in vm mode throws an exception"
|
||||
// SKIP: "rendering function partial in vm mode"
|
||||
|
||||
{
|
||||
"GH-14: a partial preceding a selector",
|
||||
"Dudes: {{>dude}} {{anotherDude}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}}"},
|
||||
"Dudes: Jeepers Creepers",
|
||||
},
|
||||
{
|
||||
"Partials with slash paths",
|
||||
"Dudes: {{> shared/dude}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"shared/dude": "{{name}}"},
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
{
|
||||
"Partials with slash and point paths",
|
||||
"Dudes: {{> shared/dude.thing}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"shared/dude.thing": "{{name}}"},
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
|
||||
// @todo "Global Partials"
|
||||
|
||||
// @todo "Multiple partial registration"
|
||||
|
||||
{
|
||||
"Partials with integer path",
|
||||
"Dudes: {{> 404}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"404": "{{name}}"}, // @note Difference with JS test: partial name is a string
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
// @note This is not supported by our implementation. But really... who cares ?
|
||||
// {
|
||||
// "Partials with complex path",
|
||||
// "Dudes: {{> 404/asdf?.bar}}",
|
||||
// map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
// nil, nil,
|
||||
// map[string]string{"404/asdf?.bar": "{{name}}"},
|
||||
// "Dudes: Jeepers",
|
||||
// },
|
||||
{
|
||||
"Partials with escaped",
|
||||
"Dudes: {{> [+404/asdf?.bar]}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"+404/asdf?.bar": "{{name}}"},
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
{
|
||||
"Partials with string",
|
||||
"Dudes: {{> '+404/asdf?.bar'}}",
|
||||
map[string]string{"name": "Jeepers", "anotherDude": "Creepers"},
|
||||
nil, nil,
|
||||
map[string]string{"+404/asdf?.bar": "{{name}}"},
|
||||
"Dudes: Jeepers",
|
||||
},
|
||||
{
|
||||
"should handle empty partial",
|
||||
"Dudes: {{#dudes}}{{> dude}}{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": ""},
|
||||
"Dudes: ",
|
||||
},
|
||||
|
||||
// @todo "throw on missing partial"
|
||||
|
||||
// SKIP: "should pass compiler flags"
|
||||
|
||||
{
|
||||
"standalone partials (1) - indented partials",
|
||||
"Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}}\n"},
|
||||
"Dudes:\n Yehuda\n Alan\n",
|
||||
},
|
||||
{
|
||||
"standalone partials (2) - nested indented partials",
|
||||
"Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
||||
map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
nil, nil,
|
||||
map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"},
|
||||
"Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n",
|
||||
},
|
||||
|
||||
// // @todo preventIndent option
|
||||
// {
|
||||
// "standalone partials (3) - prevent nested indented partials",
|
||||
// "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}",
|
||||
// map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}},
|
||||
// nil, nil,
|
||||
// map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"},
|
||||
// "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n",
|
||||
// },
|
||||
|
||||
// @todo "compat mode"
|
||||
}
|
||||
|
||||
func TestPartials(t *testing.T) {
|
||||
launchTests(t, partialsTests)
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package handlebars
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond"
|
||||
)
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/subexpression.js
|
||||
//
|
||||
var subexpressionsTests = []Test{
|
||||
{
|
||||
"arg-less helper",
|
||||
"{{foo (bar)}}!",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"foo": func(val string) string {
|
||||
return val + val
|
||||
},
|
||||
"bar": func() string {
|
||||
return "LOL"
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"LOLLOL!",
|
||||
},
|
||||
{
|
||||
"helper w args",
|
||||
"{{blog (equal a b)}}",
|
||||
map[string]interface{}{"bar": "LOL"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": blogHelper,
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is true",
|
||||
},
|
||||
{
|
||||
"mixed paths and helpers",
|
||||
"{{blog baz.bat (equal a b) baz.bar}}",
|
||||
map[string]interface{}{"bar": "LOL", "baz": map[string]string{"bat": "foo!", "bar": "bar!"}},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": func(p, p2, p3 string) string {
|
||||
return "val is " + p + ", " + p2 + " and " + p3
|
||||
},
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is foo!, true and bar!",
|
||||
},
|
||||
{
|
||||
"supports much nesting",
|
||||
"{{blog (equal (equal true true) true)}}",
|
||||
map[string]interface{}{"bar": "LOL"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": blogHelper,
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is true",
|
||||
},
|
||||
|
||||
{
|
||||
"GH-800 : Complex subexpressions (1)",
|
||||
"{{dash 'abc' (concat a b)}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"abc-ab",
|
||||
},
|
||||
{
|
||||
"GH-800 : Complex subexpressions (2)",
|
||||
"{{dash d (concat a b)}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"d-ab",
|
||||
},
|
||||
{
|
||||
"GH-800 : Complex subexpressions (3)",
|
||||
"{{dash c.c (concat a b)}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"c-ab",
|
||||
},
|
||||
{
|
||||
"GH-800 : Complex subexpressions (4)",
|
||||
"{{dash (concat a b) c.c}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"ab-c",
|
||||
},
|
||||
{
|
||||
"GH-800 : Complex subexpressions (5)",
|
||||
"{{dash (concat a e.e) c.c}}",
|
||||
map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}},
|
||||
nil,
|
||||
map[string]interface{}{"dash": dashHelper, "concat": concatHelper},
|
||||
nil,
|
||||
"ae-c",
|
||||
},
|
||||
|
||||
{
|
||||
// note: test not relevant
|
||||
"provides each nested helper invocation its own options hash",
|
||||
"{{equal (equal true true) true}}",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"true",
|
||||
},
|
||||
{
|
||||
"with hashes",
|
||||
"{{blog (equal (equal true true) true fun='yes')}}",
|
||||
map[string]interface{}{"bar": "LOL"},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": blogHelper,
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is true",
|
||||
},
|
||||
{
|
||||
"as hashes",
|
||||
"{{blog fun=(equal (blog fun=1) 'val is 1')}}",
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"blog": func(options *raymond.Options) string {
|
||||
return "val is " + options.HashStr("fun")
|
||||
},
|
||||
"equal": equalHelper,
|
||||
},
|
||||
nil,
|
||||
"val is true",
|
||||
},
|
||||
{
|
||||
"multiple subexpressions in a hash",
|
||||
`{{input aria-label=(t "Name") placeholder=(t "Example User")}}`,
|
||||
map[string]interface{}{},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"input": func(options *raymond.Options) raymond.SafeString {
|
||||
return raymond.SafeString(`<input aria-label="` + options.HashStr("aria-label") + `" placeholder="` + options.HashStr("placeholder") + `" />`)
|
||||
},
|
||||
"t": func(param string) raymond.SafeString {
|
||||
return raymond.SafeString(param)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
`<input aria-label="Name" placeholder="Example User" />`,
|
||||
},
|
||||
{
|
||||
"multiple subexpressions in a hash with context",
|
||||
`{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}`,
|
||||
map[string]map[string]string{"item": {"field": "Name", "placeholder": "Example User"}},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"input": func(options *raymond.Options) raymond.SafeString {
|
||||
return raymond.SafeString(`<input aria-label="` + options.HashStr("aria-label") + `" placeholder="` + options.HashStr("placeholder") + `" />`)
|
||||
},
|
||||
"t": func(param string) raymond.SafeString {
|
||||
return raymond.SafeString(param)
|
||||
},
|
||||
},
|
||||
nil,
|
||||
`<input aria-label="Name" placeholder="Example User" />`,
|
||||
},
|
||||
|
||||
// @todo "in string params mode"
|
||||
|
||||
// @todo "as hashes in string params mode"
|
||||
|
||||
{
|
||||
"subexpression functions on the context",
|
||||
"{{foo (bar)}}!",
|
||||
map[string]interface{}{"bar": func() string { return "LOL" }},
|
||||
nil,
|
||||
map[string]interface{}{
|
||||
"foo": func(val string) string {
|
||||
return val + val
|
||||
},
|
||||
},
|
||||
nil,
|
||||
"LOLLOL!",
|
||||
},
|
||||
|
||||
// @todo "subexpressions can't just be property lookups" should raise error
|
||||
}
|
||||
|
||||
func TestSubexpressions(t *testing.T) {
|
||||
launchTests(t, subexpressionsTests)
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package handlebars
|
||||
|
||||
import "testing"
|
||||
|
||||
//
|
||||
// Those tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/whitespace-control.js
|
||||
//
|
||||
var whitespaceControlTests = []Test{
|
||||
{
|
||||
"should strip whitespace around mustache calls (1)",
|
||||
" {{~foo~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (2)",
|
||||
" {{~foo}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar< ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (3)",
|
||||
" {{foo~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (4)",
|
||||
" {{~&foo~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (5)",
|
||||
" {{~{foo}~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around mustache calls (6)",
|
||||
"1\n{{foo~}} \n\n 23\n{{bar}}4",
|
||||
nil, nil, nil, nil,
|
||||
"1\n23\n4",
|
||||
},
|
||||
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (1)",
|
||||
" {{~#if foo~}} bar {{~/if~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (2)",
|
||||
" {{#if foo~}} bar {{/if~}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (3)",
|
||||
" {{~#if foo}} bar {{~/if}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (4)",
|
||||
" {{#if foo}} bar {{/if}} ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (5)",
|
||||
" \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"blocks - should strip whitespace around simple block calls (6)",
|
||||
" a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" abara ",
|
||||
},
|
||||
|
||||
{
|
||||
"should strip whitespace around inverse block calls (1)",
|
||||
" {{~^if foo~}} bar {{~/if~}} ",
|
||||
nil, nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around inverse block calls (2)",
|
||||
" {{^if foo~}} bar {{/if~}} ",
|
||||
nil, nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around inverse block calls (3)",
|
||||
" {{~^if foo}} bar {{~/if}} ",
|
||||
nil, nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around inverse block calls (4)",
|
||||
" {{^if foo}} bar {{/if}} ",
|
||||
nil, nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around inverse block calls (5)",
|
||||
" \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ",
|
||||
nil, nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
|
||||
{
|
||||
"should strip whitespace around complex block calls (1)",
|
||||
"{{#if foo~}} bar {{~^~}} baz {{~/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (2)",
|
||||
"{{#if foo~}} bar {{^~}} baz {{/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (3)",
|
||||
"{{#if foo}} bar {{~^~}} baz {{~/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (4)",
|
||||
"{{#if foo}} bar {{^~}} baz {{/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
" bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (5)",
|
||||
"{{#if foo~}} bar {{~else~}} baz {{~/if}}",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (6)",
|
||||
"\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (7)",
|
||||
"\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
||||
map[string]string{"foo": "bar<"},
|
||||
nil, nil, nil,
|
||||
"bar<",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (8)",
|
||||
"{{#if foo~}} bar {{~^~}} baz {{~/if}}",
|
||||
nil, nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (9)",
|
||||
"{{#if foo}} bar {{~^~}} baz {{/if}}",
|
||||
nil, nil, nil, nil,
|
||||
"baz ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (10)",
|
||||
"{{#if foo~}} bar {{~^}} baz {{~/if}}",
|
||||
nil, nil, nil, nil,
|
||||
" baz",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (11)",
|
||||
"{{#if foo~}} bar {{~^}} baz {{/if}}",
|
||||
nil, nil, nil, nil,
|
||||
" baz ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (12)",
|
||||
"{{#if foo~}} bar {{~else~}} baz {{~/if}}",
|
||||
nil, nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around complex block calls (13)",
|
||||
"\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n",
|
||||
nil, nil, nil, nil,
|
||||
"baz",
|
||||
},
|
||||
|
||||
{
|
||||
"should strip whitespace around partials (1)",
|
||||
"foo {{~> dude~}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foobar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around partials (2)",
|
||||
"foo {{> dude~}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foo bar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around partials (3)",
|
||||
"foo {{> dude}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foo bar ",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around partials (4)",
|
||||
"foo\n {{~> dude}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foobar",
|
||||
},
|
||||
{
|
||||
"should strip whitespace around partials (5)",
|
||||
"foo\n {{> dude}} ",
|
||||
nil, nil, nil,
|
||||
map[string]string{"dude": "bar"},
|
||||
"foo\n bar",
|
||||
},
|
||||
|
||||
{
|
||||
"should only strip whitespace once",
|
||||
" {{~foo~}} {{foo}} {{foo}} ",
|
||||
map[string]string{"foo": "bar"},
|
||||
nil, nil, nil,
|
||||
"barbar bar ",
|
||||
},
|
||||
}
|
||||
|
||||
func TestWhitespaceControl(t *testing.T) {
|
||||
launchTests(t, whitespaceControlTests)
|
||||
}
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Options represents the options argument provided to helpers and context functions.
|
||||
type Options struct {
|
||||
// evaluation visitor
|
||||
eval *evalVisitor
|
||||
|
||||
// params
|
||||
params []interface{}
|
||||
hash map[string]interface{}
|
||||
}
|
||||
|
||||
// helpers stores all globally registered helpers
|
||||
var helpers = make(map[string]reflect.Value)
|
||||
|
||||
// protects global helpers
|
||||
var helpersMutex sync.RWMutex
|
||||
|
||||
func init() {
|
||||
// register builtin helpers
|
||||
RegisterHelper("if", ifHelper)
|
||||
RegisterHelper("unless", unlessHelper)
|
||||
RegisterHelper("with", withHelper)
|
||||
RegisterHelper("each", eachHelper)
|
||||
RegisterHelper("log", logHelper)
|
||||
RegisterHelper("lookup", lookupHelper)
|
||||
}
|
||||
|
||||
// RegisterHelper registers a global helper. That helper will be available to all templates.
|
||||
func RegisterHelper(name string, helper interface{}) {
|
||||
helpersMutex.Lock()
|
||||
defer helpersMutex.Unlock()
|
||||
|
||||
if helpers[name] != zero {
|
||||
panic(fmt.Errorf("Helper already registered: %s", name))
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(helper)
|
||||
ensureValidHelper(name, val)
|
||||
|
||||
helpers[name] = val
|
||||
}
|
||||
|
||||
// RegisterHelpers registers several global helpers. Those helpers will be available to all templates.
|
||||
func RegisterHelpers(helpers map[string]interface{}) {
|
||||
for name, helper := range helpers {
|
||||
RegisterHelper(name, helper)
|
||||
}
|
||||
}
|
||||
|
||||
// ensureValidHelper panics if given helper is not valid
|
||||
func ensureValidHelper(name string, funcValue reflect.Value) {
|
||||
if funcValue.Kind() != reflect.Func {
|
||||
panic(fmt.Errorf("Helper must be a function: %s", name))
|
||||
}
|
||||
|
||||
funcType := funcValue.Type()
|
||||
|
||||
if funcType.NumOut() != 1 {
|
||||
panic(fmt.Errorf("Helper function must return a string or a SafeString: %s", name))
|
||||
}
|
||||
|
||||
// @todo Check if first returned value is a string, SafeString or interface{} ?
|
||||
}
|
||||
|
||||
// findHelper finds a globally registered helper
|
||||
func findHelper(name string) reflect.Value {
|
||||
helpersMutex.RLock()
|
||||
defer helpersMutex.RUnlock()
|
||||
|
||||
return helpers[name]
|
||||
}
|
||||
|
||||
// newOptions instanciates a new Options
|
||||
func newOptions(eval *evalVisitor, params []interface{}, hash map[string]interface{}) *Options {
|
||||
return &Options{
|
||||
eval: eval,
|
||||
params: params,
|
||||
hash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
// newEmptyOptions instanciates a new empty Options
|
||||
func newEmptyOptions(eval *evalVisitor) *Options {
|
||||
return &Options{
|
||||
eval: eval,
|
||||
hash: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Context Values
|
||||
//
|
||||
|
||||
// Value returns field value from current context.
|
||||
func (options *Options) Value(name string) interface{} {
|
||||
value := options.eval.evalField(options.eval.curCtx(), name, false)
|
||||
if !value.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return value.Interface()
|
||||
}
|
||||
|
||||
// ValueStr returns string representation of field value from current context.
|
||||
func (options *Options) ValueStr(name string) string {
|
||||
return Str(options.Value(name))
|
||||
}
|
||||
|
||||
// Ctx returns current evaluation context.
|
||||
func (options *Options) Ctx() interface{} {
|
||||
return options.eval.curCtx().Interface()
|
||||
}
|
||||
|
||||
//
|
||||
// Hash Arguments
|
||||
//
|
||||
|
||||
// HashProp returns hash property.
|
||||
func (options *Options) HashProp(name string) interface{} {
|
||||
return options.hash[name]
|
||||
}
|
||||
|
||||
// HashStr returns string representation of hash property.
|
||||
func (options *Options) HashStr(name string) string {
|
||||
return Str(options.hash[name])
|
||||
}
|
||||
|
||||
// Hash returns entire hash.
|
||||
func (options *Options) Hash() map[string]interface{} {
|
||||
return options.hash
|
||||
}
|
||||
|
||||
//
|
||||
// Parameters
|
||||
//
|
||||
|
||||
// Param returns parameter at given position.
|
||||
func (options *Options) Param(pos int) interface{} {
|
||||
if len(options.params) > pos {
|
||||
return options.params[pos]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParamStr returns string representation of parameter at given position.
|
||||
func (options *Options) ParamStr(pos int) string {
|
||||
return Str(options.Param(pos))
|
||||
}
|
||||
|
||||
// Params returns all parameters.
|
||||
func (options *Options) Params() []interface{} {
|
||||
return options.params
|
||||
}
|
||||
|
||||
//
|
||||
// Private data
|
||||
//
|
||||
|
||||
// Data returns private data value.
|
||||
func (options *Options) Data(name string) interface{} {
|
||||
return options.eval.dataFrame.Get(name)
|
||||
}
|
||||
|
||||
// DataStr returns string representation of private data value.
|
||||
func (options *Options) DataStr(name string) string {
|
||||
return Str(options.eval.dataFrame.Get(name))
|
||||
}
|
||||
|
||||
// DataFrame returns current private data frame.
|
||||
func (options *Options) DataFrame() *DataFrame {
|
||||
return options.eval.dataFrame
|
||||
}
|
||||
|
||||
// NewDataFrame instanciates a new data frame that is a copy of current evaluation data frame.
|
||||
//
|
||||
// Parent of returned data frame is set to current evaluation data frame.
|
||||
func (options *Options) NewDataFrame() *DataFrame {
|
||||
return options.eval.dataFrame.Copy()
|
||||
}
|
||||
|
||||
// newIterDataFrame instanciates a new data frame and set iteration specific vars
|
||||
func (options *Options) newIterDataFrame(length int, i int, key interface{}) *DataFrame {
|
||||
return options.eval.dataFrame.newIterDataFrame(length, i, key)
|
||||
}
|
||||
|
||||
//
|
||||
// Evaluation
|
||||
//
|
||||
|
||||
// evalBlock evaluates block with given context, private data and iteration key
|
||||
func (options *Options) evalBlock(ctx interface{}, data *DataFrame, key interface{}) string {
|
||||
result := ""
|
||||
|
||||
if block := options.eval.curBlock(); (block != nil) && (block.Program != nil) {
|
||||
result = options.eval.evalProgram(block.Program, ctx, data, key)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Fn evaluates block with current evaluation context.
|
||||
func (options *Options) Fn() string {
|
||||
return options.evalBlock(nil, nil, nil)
|
||||
}
|
||||
|
||||
// FnCtxData evaluates block with given context and private data frame.
|
||||
func (options *Options) FnCtxData(ctx interface{}, data *DataFrame) string {
|
||||
return options.evalBlock(ctx, data, nil)
|
||||
}
|
||||
|
||||
// FnWith evaluates block with given context.
|
||||
func (options *Options) FnWith(ctx interface{}) string {
|
||||
return options.evalBlock(ctx, nil, nil)
|
||||
}
|
||||
|
||||
// FnData evaluates block with given private data frame.
|
||||
func (options *Options) FnData(data *DataFrame) string {
|
||||
return options.evalBlock(nil, data, nil)
|
||||
}
|
||||
|
||||
// Inverse evaluates "else block".
|
||||
func (options *Options) Inverse() string {
|
||||
result := ""
|
||||
if block := options.eval.curBlock(); (block != nil) && (block.Inverse != nil) {
|
||||
result, _ = block.Inverse.Accept(options.eval).(string)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Eval evaluates field for given context.
|
||||
func (options *Options) Eval(ctx interface{}, field string) interface{} {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if field == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
val := options.eval.evalField(reflect.ValueOf(ctx), field, false)
|
||||
if !val.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return val.Interface()
|
||||
}
|
||||
|
||||
//
|
||||
// Misc
|
||||
//
|
||||
|
||||
// isIncludableZero returns true if 'includeZero' option is set and first param is the number 0
|
||||
func (options *Options) isIncludableZero() bool {
|
||||
b, ok := options.HashProp("includeZero").(bool)
|
||||
if ok && b {
|
||||
nb, ok := options.Param(0).(int)
|
||||
if ok && nb == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//
|
||||
// Builtin helpers
|
||||
//
|
||||
|
||||
// #if block helper
|
||||
func ifHelper(conditional interface{}, options *Options) interface{} {
|
||||
if options.isIncludableZero() || IsTrue(conditional) {
|
||||
return options.Fn()
|
||||
}
|
||||
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
// #unless block helper
|
||||
func unlessHelper(conditional interface{}, options *Options) interface{} {
|
||||
if options.isIncludableZero() || IsTrue(conditional) {
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
return options.Fn()
|
||||
}
|
||||
|
||||
// #with block helper
|
||||
func withHelper(context interface{}, options *Options) interface{} {
|
||||
if IsTrue(context) {
|
||||
return options.FnWith(context)
|
||||
}
|
||||
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
// #each block helper
|
||||
func eachHelper(context interface{}, options *Options) interface{} {
|
||||
if !IsTrue(context) {
|
||||
return options.Inverse()
|
||||
}
|
||||
|
||||
result := ""
|
||||
|
||||
val := reflect.ValueOf(context)
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
// computes private data
|
||||
data := options.newIterDataFrame(val.Len(), i, nil)
|
||||
|
||||
// evaluates block
|
||||
result += options.evalBlock(val.Index(i).Interface(), data, i)
|
||||
}
|
||||
case reflect.Map:
|
||||
// note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation
|
||||
keys := val.MapKeys()
|
||||
for i := 0; i < len(keys); i++ {
|
||||
key := keys[i].Interface()
|
||||
ctx := val.MapIndex(keys[i]).Interface()
|
||||
|
||||
// computes private data
|
||||
data := options.newIterDataFrame(len(keys), i, key)
|
||||
|
||||
// evaluates block
|
||||
result += options.evalBlock(ctx, data, key)
|
||||
}
|
||||
case reflect.Struct:
|
||||
var exportedFields []int
|
||||
|
||||
// collect exported fields only
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
if tField := val.Type().Field(i); tField.PkgPath == "" {
|
||||
exportedFields = append(exportedFields, i)
|
||||
}
|
||||
}
|
||||
|
||||
for i, fieldIndex := range exportedFields {
|
||||
key := val.Type().Field(fieldIndex).Name
|
||||
ctx := val.Field(fieldIndex).Interface()
|
||||
|
||||
// computes private data
|
||||
data := options.newIterDataFrame(len(exportedFields), i, key)
|
||||
|
||||
// evaluates block
|
||||
result += options.evalBlock(ctx, data, key)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// #log helper
|
||||
func logHelper(message string) interface{} {
|
||||
log.Print(message)
|
||||
return ""
|
||||
}
|
||||
|
||||
// #lookup helper
|
||||
func lookupHelper(obj interface{}, field string, options *Options) interface{} {
|
||||
return Str(options.Eval(obj, field))
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package raymond
|
||||
|
||||
import "testing"
|
||||
|
||||
const (
|
||||
VERBOSE = false
|
||||
)
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
func barHelper(options *Options) string { return "bar" }
|
||||
|
||||
func echoHelper(str string, nb int) string {
|
||||
result := ""
|
||||
for i := 0; i < nb; i++ {
|
||||
result += str
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func boolHelper(b bool) string {
|
||||
if b {
|
||||
return "yes it is"
|
||||
}
|
||||
|
||||
return "absolutely not"
|
||||
}
|
||||
|
||||
func gnakHelper(nb int) string {
|
||||
result := ""
|
||||
for i := 0; i < nb; i++ {
|
||||
result += "GnAK!"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
//
|
||||
// Tests
|
||||
//
|
||||
|
||||
var helperTests = []Test{
|
||||
{
|
||||
"simple helper",
|
||||
`{{foo}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"foo": barHelper},
|
||||
nil,
|
||||
`bar`,
|
||||
},
|
||||
{
|
||||
"helper with literal string param",
|
||||
`{{echo "foo" 1}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"echo": echoHelper},
|
||||
nil,
|
||||
`foo`,
|
||||
},
|
||||
{
|
||||
"helper with identifier param",
|
||||
`{{echo foo 1}}`,
|
||||
map[string]interface{}{"foo": "bar"},
|
||||
nil,
|
||||
map[string]interface{}{"echo": echoHelper},
|
||||
nil,
|
||||
`bar`,
|
||||
},
|
||||
{
|
||||
"helper with literal boolean param",
|
||||
`{{bool true}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"bool": boolHelper},
|
||||
nil,
|
||||
`yes it is`,
|
||||
},
|
||||
{
|
||||
"helper with literal boolean param",
|
||||
`{{bool false}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"bool": boolHelper},
|
||||
nil,
|
||||
`absolutely not`,
|
||||
},
|
||||
{
|
||||
"helper with literal boolean param",
|
||||
`{{gnak 5}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"gnak": gnakHelper},
|
||||
nil,
|
||||
`GnAK!GnAK!GnAK!GnAK!GnAK!`,
|
||||
},
|
||||
{
|
||||
"helper with several parameters",
|
||||
`{{echo "GnAK!" 3}}`,
|
||||
nil, nil,
|
||||
map[string]interface{}{"echo": echoHelper},
|
||||
nil,
|
||||
`GnAK!GnAK!GnAK!`,
|
||||
},
|
||||
{
|
||||
"#if helper with true literal",
|
||||
`{{#if true}}YES MAN{{/if}}`,
|
||||
nil, nil, nil, nil,
|
||||
`YES MAN`,
|
||||
},
|
||||
{
|
||||
"#if helper with false literal",
|
||||
`{{#if false}}YES MAN{{/if}}`,
|
||||
nil, nil, nil, nil,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"#if helper with truthy identifier",
|
||||
`{{#if ok}}YES MAN{{/if}}`,
|
||||
map[string]interface{}{"ok": true},
|
||||
nil, nil, nil,
|
||||
`YES MAN`,
|
||||
},
|
||||
{
|
||||
"#if helper with falsy identifier",
|
||||
`{{#if ok}}YES MAN{{/if}}`,
|
||||
map[string]interface{}{"ok": false},
|
||||
nil, nil, nil,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"#unless helper with true literal",
|
||||
`{{#unless true}}YES MAN{{/unless}}`,
|
||||
nil, nil, nil, nil,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"#unless helper with false literal",
|
||||
`{{#unless false}}YES MAN{{/unless}}`,
|
||||
nil, nil, nil, nil,
|
||||
`YES MAN`,
|
||||
},
|
||||
{
|
||||
"#unless helper with truthy identifier",
|
||||
`{{#unless ok}}YES MAN{{/unless}}`,
|
||||
map[string]interface{}{"ok": true},
|
||||
nil, nil, nil,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"#unless helper with falsy identifier",
|
||||
`{{#unless ok}}YES MAN{{/unless}}`,
|
||||
map[string]interface{}{"ok": false},
|
||||
nil, nil, nil,
|
||||
`YES MAN`,
|
||||
},
|
||||
}
|
||||
|
||||
//
|
||||
// Let's go
|
||||
//
|
||||
|
||||
func TestHelper(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
launchTests(t, helperTests)
|
||||
}
|
||||
|
||||
//
|
||||
// Fixes: https://github.com/aymerick/raymond/issues/2
|
||||
//
|
||||
|
||||
type Author struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
func TestHelperCtx(t *testing.T) {
|
||||
RegisterHelper("template", func(name string, options *Options) SafeString {
|
||||
context := options.Ctx()
|
||||
|
||||
template := name + " - {{ firstName }} {{ lastName }}"
|
||||
result, _ := Render(template, context)
|
||||
|
||||
return SafeString(result)
|
||||
})
|
||||
|
||||
template := `By {{ template "namefile" }}`
|
||||
context := Author{"Alan", "Johnson"}
|
||||
|
||||
result, _ := Render(template, context)
|
||||
if result != "By namefile - Alan Johnson" {
|
||||
t.Errorf("Failed to render template in helper: %q", result)
|
||||
}
|
||||
}
|
||||
+639
@@ -0,0 +1,639 @@
|
||||
// Package lexer provides a handlebars tokenizer.
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// References:
|
||||
// - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.l
|
||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/lex.go
|
||||
|
||||
const (
|
||||
// Mustaches detection
|
||||
escapedEscapedOpenMustache = "\\\\{{"
|
||||
escapedOpenMustache = "\\{{"
|
||||
openMustache = "{{"
|
||||
closeMustache = "}}"
|
||||
closeStripMustache = "~}}"
|
||||
closeUnescapedStripMustache = "}~}}"
|
||||
)
|
||||
|
||||
const eof = -1
|
||||
|
||||
// lexFunc represents a function that returns the next lexer function.
|
||||
type lexFunc func(*Lexer) lexFunc
|
||||
|
||||
// Lexer is a lexical analyzer.
|
||||
type Lexer struct {
|
||||
input string // input to scan
|
||||
name string // lexer name, used for testing purpose
|
||||
tokens chan Token // channel of scanned tokens
|
||||
nextFunc lexFunc // the next function to execute
|
||||
|
||||
pos int // current byte position in input string
|
||||
line int // current line position in input string
|
||||
width int // size of last rune scanned from input string
|
||||
start int // start position of the token we are scanning
|
||||
|
||||
// the shameful contextual properties needed because `nextFunc` is not enough
|
||||
closeComment *regexp.Regexp // regexp to scan close of current comment
|
||||
rawBlock bool // are we parsing a raw block content ?
|
||||
}
|
||||
|
||||
var (
|
||||
lookheadChars = `[\s` + regexp.QuoteMeta("=~}/)|") + `]`
|
||||
literalLookheadChars = `[\s` + regexp.QuoteMeta("~})") + `]`
|
||||
|
||||
// characters not allowed in an identifier
|
||||
unallowedIDChars = " \n\t!\"#%&'()*+,./;<=>@[\\]^`{|}~"
|
||||
|
||||
// regular expressions
|
||||
rID = regexp.MustCompile(`^[^` + regexp.QuoteMeta(unallowedIDChars) + `]+`)
|
||||
rDotID = regexp.MustCompile(`^\.` + lookheadChars)
|
||||
rTrue = regexp.MustCompile(`^true` + literalLookheadChars)
|
||||
rFalse = regexp.MustCompile(`^false` + literalLookheadChars)
|
||||
rOpenRaw = regexp.MustCompile(`^\{\{\{\{`)
|
||||
rCloseRaw = regexp.MustCompile(`^\}\}\}\}`)
|
||||
rOpenEndRaw = regexp.MustCompile(`^\{\{\{\{/`)
|
||||
rOpenEndRawLookAhead = regexp.MustCompile(`\{\{\{\{/`)
|
||||
rOpenUnescaped = regexp.MustCompile(`^\{\{~?\{`)
|
||||
rCloseUnescaped = regexp.MustCompile(`^\}~?\}\}`)
|
||||
rOpenBlock = regexp.MustCompile(`^\{\{~?#`)
|
||||
rOpenEndBlock = regexp.MustCompile(`^\{\{~?/`)
|
||||
rOpenPartial = regexp.MustCompile(`^\{\{~?>`)
|
||||
// {{^}} or {{else}}
|
||||
rInverse = regexp.MustCompile(`^(\{\{~?\^\s*~?\}\}|\{\{~?\s*else\s*~?\}\})`)
|
||||
rOpenInverse = regexp.MustCompile(`^\{\{~?\^`)
|
||||
rOpenInverseChain = regexp.MustCompile(`^\{\{~?\s*else`)
|
||||
// {{ or {{&
|
||||
rOpen = regexp.MustCompile(`^\{\{~?&?`)
|
||||
rClose = regexp.MustCompile(`^~?\}\}`)
|
||||
rOpenBlockParams = regexp.MustCompile(`^as\s+\|`)
|
||||
// {{!-- ... --}}
|
||||
rOpenCommentDash = regexp.MustCompile(`^\{\{~?!--\s*`)
|
||||
rCloseCommentDash = regexp.MustCompile(`^\s*--~?\}\}`)
|
||||
// {{! ... }}
|
||||
rOpenComment = regexp.MustCompile(`^\{\{~?!\s*`)
|
||||
rCloseComment = regexp.MustCompile(`^\s*~?\}\}`)
|
||||
)
|
||||
|
||||
// Scan scans given input.
|
||||
//
|
||||
// Tokens can then be fetched sequentially thanks to NextToken() function on returned lexer.
|
||||
func Scan(input string) *Lexer {
|
||||
return scanWithName(input, "")
|
||||
}
|
||||
|
||||
// scanWithName scans given input, with a name used for testing
|
||||
//
|
||||
// Tokens can then be fetched sequentially thanks to NextToken() function on returned lexer.
|
||||
func scanWithName(input string, name string) *Lexer {
|
||||
result := &Lexer{
|
||||
input: input,
|
||||
name: name,
|
||||
tokens: make(chan Token),
|
||||
line: 1,
|
||||
}
|
||||
|
||||
go result.run()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Collect scans and collect all tokens.
|
||||
//
|
||||
// This should be used for debugging purpose only. You should use Scan() and lexer.NextToken() functions instead.
|
||||
func Collect(input string) []Token {
|
||||
var result []Token
|
||||
|
||||
l := Scan(input)
|
||||
for {
|
||||
token := l.NextToken()
|
||||
result = append(result, token)
|
||||
|
||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// NextToken returns the next scanned token.
|
||||
func (l *Lexer) NextToken() Token {
|
||||
result := <-l.tokens
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// run starts lexical analysis
|
||||
func (l *Lexer) run() {
|
||||
for l.nextFunc = lexContent; l.nextFunc != nil; {
|
||||
l.nextFunc = l.nextFunc(l)
|
||||
}
|
||||
}
|
||||
|
||||
// next returns next character from input, or eof of there is nothing left to scan
|
||||
func (l *Lexer) next() rune {
|
||||
if l.pos >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
|
||||
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.width = w
|
||||
l.pos += l.width
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *Lexer) produce(kind TokenKind, val string) {
|
||||
l.tokens <- Token{kind, val, l.start, l.line}
|
||||
|
||||
// scanning a new token
|
||||
l.start = l.pos
|
||||
|
||||
// update line number
|
||||
l.line += strings.Count(val, "\n")
|
||||
}
|
||||
|
||||
// emit emits a new scanned token
|
||||
func (l *Lexer) emit(kind TokenKind) {
|
||||
l.produce(kind, l.input[l.start:l.pos])
|
||||
}
|
||||
|
||||
// emitContent emits scanned content
|
||||
func (l *Lexer) emitContent() {
|
||||
if l.pos > l.start {
|
||||
l.emit(TokenContent)
|
||||
}
|
||||
}
|
||||
|
||||
// emitString emits a scanned string
|
||||
func (l *Lexer) emitString(delimiter rune) {
|
||||
str := l.input[l.start:l.pos]
|
||||
|
||||
// replace escaped delimiters
|
||||
str = strings.Replace(str, "\\"+string(delimiter), string(delimiter), -1)
|
||||
|
||||
l.produce(TokenString, str)
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next character in the input
|
||||
func (l *Lexer) peek() rune {
|
||||
r := l.next()
|
||||
l.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one character
|
||||
//
|
||||
// WARNING: Can only be called once per call of next
|
||||
func (l *Lexer) backup() {
|
||||
l.pos -= l.width
|
||||
}
|
||||
|
||||
// ignoreskips all characters that have been scanned up to current position
|
||||
func (l *Lexer) ignore() {
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
// accept scans the next character if it is included in given string
|
||||
func (l *Lexer) accept(valid string) bool {
|
||||
if strings.IndexRune(valid, l.next()) >= 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
l.backup()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// acceptRun scans all following characters that are part of given string
|
||||
func (l *Lexer) acceptRun(valid string) {
|
||||
for strings.IndexRune(valid, l.next()) >= 0 {
|
||||
}
|
||||
|
||||
l.backup()
|
||||
}
|
||||
|
||||
// errorf emits an error token
|
||||
func (l *Lexer) errorf(format string, args ...interface{}) lexFunc {
|
||||
l.tokens <- Token{TokenError, fmt.Sprintf(format, args...), l.start, l.line}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isString returns true if content at current scanning position starts with given string
|
||||
func (l *Lexer) isString(str string) bool {
|
||||
return strings.HasPrefix(l.input[l.pos:], str)
|
||||
}
|
||||
|
||||
// findRegexp returns the first string from current scanning position that matches given regular expression
|
||||
func (l *Lexer) findRegexp(r *regexp.Regexp) string {
|
||||
return r.FindString(l.input[l.pos:])
|
||||
}
|
||||
|
||||
// indexRegexp returns the index of the first string from current scanning position that matches given regular expression
|
||||
//
|
||||
// It returns -1 if not found
|
||||
func (l *Lexer) indexRegexp(r *regexp.Regexp) int {
|
||||
loc := r.FindStringIndex(l.input[l.pos:])
|
||||
if loc == nil {
|
||||
return -1
|
||||
}
|
||||
return loc[0]
|
||||
}
|
||||
|
||||
// lexContent scans content (ie: not between mustaches)
|
||||
func lexContent(l *Lexer) lexFunc {
|
||||
var next lexFunc
|
||||
|
||||
if l.rawBlock {
|
||||
if i := l.indexRegexp(rOpenEndRawLookAhead); i != -1 {
|
||||
// {{{{/
|
||||
l.rawBlock = false
|
||||
l.pos += i
|
||||
|
||||
next = lexOpenMustache
|
||||
} else {
|
||||
return l.errorf("Unclosed raw block")
|
||||
}
|
||||
} else if l.isString(escapedEscapedOpenMustache) {
|
||||
// \\{{
|
||||
|
||||
// emit content with only one escaped escape
|
||||
l.next()
|
||||
l.emitContent()
|
||||
|
||||
// ignore second escaped escape
|
||||
l.next()
|
||||
l.ignore()
|
||||
|
||||
next = lexContent
|
||||
} else if l.isString(escapedOpenMustache) {
|
||||
// \{{
|
||||
next = lexEscapedOpenMustache
|
||||
} else if str := l.findRegexp(rOpenCommentDash); str != "" {
|
||||
// {{!--
|
||||
l.closeComment = rCloseCommentDash
|
||||
|
||||
next = lexComment
|
||||
} else if str := l.findRegexp(rOpenComment); str != "" {
|
||||
// {{!
|
||||
l.closeComment = rCloseComment
|
||||
|
||||
next = lexComment
|
||||
} else if l.isString(openMustache) {
|
||||
// {{
|
||||
next = lexOpenMustache
|
||||
}
|
||||
|
||||
if next != nil {
|
||||
// emit scanned content
|
||||
l.emitContent()
|
||||
|
||||
// scan next token
|
||||
return next
|
||||
}
|
||||
|
||||
// scan next rune
|
||||
if l.next() == eof {
|
||||
// emit scanned content
|
||||
l.emitContent()
|
||||
|
||||
// this is over
|
||||
l.emit(TokenEOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
// continue content scanning
|
||||
return lexContent
|
||||
}
|
||||
|
||||
// lexEscapedOpenMustache scans \{{
|
||||
func lexEscapedOpenMustache(l *Lexer) lexFunc {
|
||||
// ignore escape character
|
||||
l.next()
|
||||
l.ignore()
|
||||
|
||||
// scan mustaches
|
||||
for l.peek() == '{' {
|
||||
l.next()
|
||||
}
|
||||
|
||||
return lexContent
|
||||
}
|
||||
|
||||
// lexOpenMustache scans {{
|
||||
func lexOpenMustache(l *Lexer) lexFunc {
|
||||
var str string
|
||||
var tok TokenKind
|
||||
|
||||
nextFunc := lexExpression
|
||||
|
||||
if str = l.findRegexp(rOpenEndRaw); str != "" {
|
||||
tok = TokenOpenEndRawBlock
|
||||
} else if str = l.findRegexp(rOpenRaw); str != "" {
|
||||
tok = TokenOpenRawBlock
|
||||
l.rawBlock = true
|
||||
} else if str = l.findRegexp(rOpenUnescaped); str != "" {
|
||||
tok = TokenOpenUnescaped
|
||||
} else if str = l.findRegexp(rOpenBlock); str != "" {
|
||||
tok = TokenOpenBlock
|
||||
} else if str = l.findRegexp(rOpenEndBlock); str != "" {
|
||||
tok = TokenOpenEndBlock
|
||||
} else if str = l.findRegexp(rOpenPartial); str != "" {
|
||||
tok = TokenOpenPartial
|
||||
} else if str = l.findRegexp(rInverse); str != "" {
|
||||
tok = TokenInverse
|
||||
nextFunc = lexContent
|
||||
} else if str = l.findRegexp(rOpenInverse); str != "" {
|
||||
tok = TokenOpenInverse
|
||||
} else if str = l.findRegexp(rOpenInverseChain); str != "" {
|
||||
tok = TokenOpenInverseChain
|
||||
} else if str = l.findRegexp(rOpen); str != "" {
|
||||
tok = TokenOpen
|
||||
} else {
|
||||
// this is rotten
|
||||
panic("Current pos MUST be an opening mustache")
|
||||
}
|
||||
|
||||
l.pos += len(str)
|
||||
l.emit(tok)
|
||||
|
||||
return nextFunc
|
||||
}
|
||||
|
||||
// lexCloseMustache scans }} or ~}}
|
||||
func lexCloseMustache(l *Lexer) lexFunc {
|
||||
var str string
|
||||
var tok TokenKind
|
||||
|
||||
if str = l.findRegexp(rCloseRaw); str != "" {
|
||||
// }}}}
|
||||
tok = TokenCloseRawBlock
|
||||
} else if str = l.findRegexp(rCloseUnescaped); str != "" {
|
||||
// }}}
|
||||
tok = TokenCloseUnescaped
|
||||
} else if str = l.findRegexp(rClose); str != "" {
|
||||
// }}
|
||||
tok = TokenClose
|
||||
} else {
|
||||
// this is rotten
|
||||
panic("Current pos MUST be a closing mustache")
|
||||
}
|
||||
|
||||
l.pos += len(str)
|
||||
l.emit(tok)
|
||||
|
||||
return lexContent
|
||||
}
|
||||
|
||||
// lexExpression scans inside mustaches
|
||||
func lexExpression(l *Lexer) lexFunc {
|
||||
// search close mustache delimiter
|
||||
if l.isString(closeMustache) || l.isString(closeStripMustache) || l.isString(closeUnescapedStripMustache) {
|
||||
return lexCloseMustache
|
||||
}
|
||||
|
||||
// search some patterns before advancing scanning position
|
||||
|
||||
// "as |"
|
||||
if str := l.findRegexp(rOpenBlockParams); str != "" {
|
||||
l.pos += len(str)
|
||||
l.emit(TokenOpenBlockParams)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// ..
|
||||
if l.isString("..") {
|
||||
l.pos += len("..")
|
||||
l.emit(TokenID)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// .
|
||||
if str := l.findRegexp(rDotID); str != "" {
|
||||
l.pos += len(".")
|
||||
l.emit(TokenID)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// true
|
||||
if str := l.findRegexp(rTrue); str != "" {
|
||||
l.pos += len("true")
|
||||
l.emit(TokenBoolean)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// false
|
||||
if str := l.findRegexp(rFalse); str != "" {
|
||||
l.pos += len("false")
|
||||
l.emit(TokenBoolean)
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// let's scan next character
|
||||
switch r := l.next(); {
|
||||
case r == eof:
|
||||
return l.errorf("Unclosed expression")
|
||||
case isIgnorable(r):
|
||||
return lexIgnorable
|
||||
case r == '(':
|
||||
l.emit(TokenOpenSexpr)
|
||||
case r == ')':
|
||||
l.emit(TokenCloseSexpr)
|
||||
case r == '=':
|
||||
l.emit(TokenEquals)
|
||||
case r == '@':
|
||||
l.emit(TokenData)
|
||||
case r == '"' || r == '\'':
|
||||
l.backup()
|
||||
return lexString
|
||||
case r == '/' || r == '.':
|
||||
l.emit(TokenSep)
|
||||
case r == '|':
|
||||
l.emit(TokenCloseBlockParams)
|
||||
case r == '+' || r == '-' || (r >= '0' && r <= '9'):
|
||||
l.backup()
|
||||
return lexNumber
|
||||
case r == '[':
|
||||
return lexPathLiteral
|
||||
case strings.IndexRune(unallowedIDChars, r) < 0:
|
||||
l.backup()
|
||||
return lexIdentifier
|
||||
default:
|
||||
return l.errorf("Unexpected character in expression: '%c'", r)
|
||||
}
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// lexComment scans {{!-- or {{!
|
||||
func lexComment(l *Lexer) lexFunc {
|
||||
if str := l.findRegexp(l.closeComment); str != "" {
|
||||
l.pos += len(str)
|
||||
l.emit(TokenComment)
|
||||
|
||||
return lexContent
|
||||
}
|
||||
|
||||
if r := l.next(); r == eof {
|
||||
return l.errorf("Unclosed comment")
|
||||
}
|
||||
|
||||
return lexComment
|
||||
}
|
||||
|
||||
// lexIgnorable scans all following ignorable characters
|
||||
func lexIgnorable(l *Lexer) lexFunc {
|
||||
for isIgnorable(l.peek()) {
|
||||
l.next()
|
||||
}
|
||||
l.ignore()
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// lexString scans a string
|
||||
func lexString(l *Lexer) lexFunc {
|
||||
// get string delimiter
|
||||
delim := l.next()
|
||||
var prev rune
|
||||
|
||||
// ignore delimiter
|
||||
l.ignore()
|
||||
|
||||
for {
|
||||
r := l.next()
|
||||
if r == eof || r == '\n' {
|
||||
return l.errorf("Unterminated string")
|
||||
}
|
||||
|
||||
if (r == delim) && (prev != '\\') {
|
||||
break
|
||||
}
|
||||
|
||||
prev = r
|
||||
}
|
||||
|
||||
// remove end delimiter
|
||||
l.backup()
|
||||
|
||||
// emit string
|
||||
l.emitString(delim)
|
||||
|
||||
// skip end delimiter
|
||||
l.next()
|
||||
l.ignore()
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// lexNumber scans a number: decimal, octal, hex, float, or imaginary. This
|
||||
// isn't a perfect number scanner - for instance it accepts "." and "0x0.2"
|
||||
// and "089" - but when it's wrong the input is invalid and the parser (via
|
||||
// strconv) will notice.
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
||||
func lexNumber(l *Lexer) lexFunc {
|
||||
if !l.scanNumber() {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
if sign := l.peek(); sign == '+' || sign == '-' {
|
||||
// Complex: 1+2i. No spaces, must end in 'i'.
|
||||
if !l.scanNumber() || l.input[l.pos-1] != 'i' {
|
||||
return l.errorf("bad number syntax: %q", l.input[l.start:l.pos])
|
||||
}
|
||||
l.emit(TokenNumber)
|
||||
} else {
|
||||
l.emit(TokenNumber)
|
||||
}
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// scanNumber scans a number
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
||||
func (l *Lexer) scanNumber() bool {
|
||||
// Optional leading sign.
|
||||
l.accept("+-")
|
||||
|
||||
// Is it hex?
|
||||
digits := "0123456789"
|
||||
|
||||
if l.accept("0") && l.accept("xX") {
|
||||
digits = "0123456789abcdefABCDEF"
|
||||
}
|
||||
|
||||
l.acceptRun(digits)
|
||||
|
||||
if l.accept(".") {
|
||||
l.acceptRun(digits)
|
||||
}
|
||||
|
||||
if l.accept("eE") {
|
||||
l.accept("+-")
|
||||
l.acceptRun("0123456789")
|
||||
}
|
||||
|
||||
// Is it imaginary?
|
||||
l.accept("i")
|
||||
|
||||
// Next thing mustn't be alphanumeric.
|
||||
if isAlphaNumeric(l.peek()) {
|
||||
l.next()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// lexIdentifier scans an ID
|
||||
func lexIdentifier(l *Lexer) lexFunc {
|
||||
str := l.findRegexp(rID)
|
||||
if len(str) == 0 {
|
||||
// this is rotten
|
||||
panic("Identifier expected")
|
||||
}
|
||||
|
||||
l.pos += len(str)
|
||||
l.emit(TokenID)
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// lexPathLiteral scans an [ID]
|
||||
func lexPathLiteral(l *Lexer) lexFunc {
|
||||
for {
|
||||
r := l.next()
|
||||
if r == eof || r == '\n' {
|
||||
return l.errorf("Unterminated path literal")
|
||||
}
|
||||
|
||||
if r == ']' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
l.emit(TokenID)
|
||||
|
||||
return lexExpression
|
||||
}
|
||||
|
||||
// isIgnorable returns true if given character is ignorable (ie. whitespace of line feed)
|
||||
func isIgnorable(r rune) bool {
|
||||
return r == ' ' || r == '\t' || r == '\n'
|
||||
}
|
||||
|
||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
//
|
||||
// NOTE borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
||||
+541
@@ -0,0 +1,541 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexTest struct {
|
||||
name string
|
||||
input string
|
||||
tokens []Token
|
||||
}
|
||||
|
||||
// helpers
|
||||
func tokContent(val string) Token { return Token{TokenContent, val, 0, 1} }
|
||||
func tokID(val string) Token { return Token{TokenID, val, 0, 1} }
|
||||
func tokSep(val string) Token { return Token{TokenSep, val, 0, 1} }
|
||||
func tokString(val string) Token { return Token{TokenString, val, 0, 1} }
|
||||
func tokNumber(val string) Token { return Token{TokenNumber, val, 0, 1} }
|
||||
func tokInverse(val string) Token { return Token{TokenInverse, val, 0, 1} }
|
||||
func tokBool(val string) Token { return Token{TokenBoolean, val, 0, 1} }
|
||||
func tokError(val string) Token { return Token{TokenError, val, 0, 1} }
|
||||
func tokComment(val string) Token { return Token{TokenComment, val, 0, 1} }
|
||||
|
||||
var tokEOF = Token{TokenEOF, "", 0, 1}
|
||||
var tokEquals = Token{TokenEquals, "=", 0, 1}
|
||||
var tokData = Token{TokenData, "@", 0, 1}
|
||||
var tokOpen = Token{TokenOpen, "{{", 0, 1}
|
||||
var tokOpenAmp = Token{TokenOpen, "{{&", 0, 1}
|
||||
var tokOpenPartial = Token{TokenOpenPartial, "{{>", 0, 1}
|
||||
var tokClose = Token{TokenClose, "}}", 0, 1}
|
||||
var tokOpenStrip = Token{TokenOpen, "{{~", 0, 1}
|
||||
var tokCloseStrip = Token{TokenClose, "~}}", 0, 1}
|
||||
var tokOpenUnescaped = Token{TokenOpenUnescaped, "{{{", 0, 1}
|
||||
var tokCloseUnescaped = Token{TokenCloseUnescaped, "}}}", 0, 1}
|
||||
var tokOpenUnescapedStrip = Token{TokenOpenUnescaped, "{{~{", 0, 1}
|
||||
var tokCloseUnescapedStrip = Token{TokenCloseUnescaped, "}~}}", 0, 1}
|
||||
var tokOpenBlock = Token{TokenOpenBlock, "{{#", 0, 1}
|
||||
var tokOpenEndBlock = Token{TokenOpenEndBlock, "{{/", 0, 1}
|
||||
var tokOpenInverse = Token{TokenOpenInverse, "{{^", 0, 1}
|
||||
var tokOpenInverseChain = Token{TokenOpenInverseChain, "{{else", 0, 1}
|
||||
var tokOpenSexpr = Token{TokenOpenSexpr, "(", 0, 1}
|
||||
var tokCloseSexpr = Token{TokenCloseSexpr, ")", 0, 1}
|
||||
var tokOpenBlockParams = Token{TokenOpenBlockParams, "as |", 0, 1}
|
||||
var tokCloseBlockParams = Token{TokenCloseBlockParams, "|", 0, 1}
|
||||
var tokOpenRawBlock = Token{TokenOpenRawBlock, "{{{{", 0, 1}
|
||||
var tokCloseRawBlock = Token{TokenCloseRawBlock, "}}}}", 0, 1}
|
||||
var tokOpenEndRawBlock = Token{TokenOpenEndRawBlock, "{{{{/", 0, 1}
|
||||
|
||||
var lexTests = []lexTest{
|
||||
{"empty", "", []Token{tokEOF}},
|
||||
{"spaces", " \t\n", []Token{tokContent(" \t\n"), tokEOF}},
|
||||
{"content", `now is the time`, []Token{tokContent(`now is the time`), tokEOF}},
|
||||
|
||||
{
|
||||
`does not tokenizes identifier starting with true as boolean`,
|
||||
`{{ foo truebar }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("truebar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`does not tokenizes identifier starting with false as boolean`,
|
||||
`{{ foo falsebar }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("falsebar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes raw block`,
|
||||
`{{{{foo}}}} {{{{/foo}}}}`,
|
||||
[]Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent(" "), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes raw block with mustaches in content`,
|
||||
`{{{{foo}}}}{{bar}}{{{{/foo}}}}`,
|
||||
[]Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent("{{bar}}"), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes @../foo`,
|
||||
`{{@../foo}}`,
|
||||
[]Token{tokOpen, tokData, tokID(".."), tokSep("/"), tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes escaped mustaches`,
|
||||
"\\{{bar}}",
|
||||
[]Token{tokContent("{{bar}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes strip mustaches`,
|
||||
`{{~ foo ~}}`,
|
||||
[]Token{tokOpenStrip, tokID("foo"), tokCloseStrip, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes unescaped strip mustaches`,
|
||||
`{{~{ foo }~}}`,
|
||||
[]Token{tokOpenUnescapedStrip, tokID("foo"), tokCloseUnescapedStrip, tokEOF},
|
||||
},
|
||||
|
||||
//
|
||||
// Next tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/tokenizer.js
|
||||
//
|
||||
{
|
||||
`tokenizes a simple mustache as "OPEN ID CLOSE"`,
|
||||
`{{foo}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports unescaping with &`,
|
||||
`{{&bar}}`,
|
||||
[]Token{tokOpenAmp, tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports unescaping with {{{`,
|
||||
`{{{bar}}}`,
|
||||
[]Token{tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping delimiters`,
|
||||
"{{foo}} \\{{bar}} {{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping multiple delimiters`,
|
||||
"{{foo}} \\{{bar}} \\{{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("{{baz}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping a triple stash`,
|
||||
"{{foo}} \\{{{bar}}} {{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{{bar}}} "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping escape character`,
|
||||
"{{foo}} \\\\{{bar}} {{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaping multiple escape characters`,
|
||||
"{{foo}} \\\\{{bar}} \\\\{{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" \\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaped mustaches after escaped escape characters`,
|
||||
"{{foo}} \\\\{{bar}} \\{{baz}}",
|
||||
// NOTE: JS implementation returns:
|
||||
// ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT'],
|
||||
// WTF is the last CONTENT ?
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokContent("{{baz}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaped escape characters after escaped mustaches`,
|
||||
"{{foo}} \\{{bar}} \\\\{{baz}}",
|
||||
// NOTE: JS implementation returns:
|
||||
// []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("\\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} \\"), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`supports escaped escape character on a triple stash`,
|
||||
"{{foo}} \\\\{{{bar}}} {{baz}}",
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a simple path`,
|
||||
`{{foo/bar}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`allows dot notation (1)`,
|
||||
`{{foo.bar}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`allows dot notation (2)`,
|
||||
`{{foo.bar.baz}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`allows path literals with []`,
|
||||
`{{foo.[bar]}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`allows multiple path literals on a line with []`,
|
||||
`{{foo.[bar]}}{{foo.[baz]}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokOpen, tokID("foo"), tokSep("."), tokID("[baz]"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes {{.}} as OPEN ID CLOSE`,
|
||||
`{{.}}`,
|
||||
[]Token{tokOpen, tokID("."), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a path as "OPEN (ID SEP)* ID CLOSE"`,
|
||||
`{{../foo/bar}}`,
|
||||
[]Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a path with .. as a parent path`,
|
||||
`{{../foo.bar}}`,
|
||||
[]Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a path with this/foo as OPEN ID SEP ID CLOSE`,
|
||||
`{{this/foo}}`,
|
||||
[]Token{tokOpen, tokID("this"), tokSep("/"), tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a simple mustache with spaces as "OPEN ID CLOSE"`,
|
||||
`{{ foo }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"`,
|
||||
"{{ foo \n bar }}",
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes raw content as "CONTENT"`,
|
||||
`foo {{ bar }} baz`,
|
||||
[]Token{tokContent("foo "), tokOpen, tokID("bar"), tokClose, tokContent(" baz"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial as "OPEN_PARTIAL ID CLOSE"`,
|
||||
`{{> foo}}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"`,
|
||||
`{{> foo bar }}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"`,
|
||||
`{{>foo}}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`,
|
||||
`{{>foo }}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`,
|
||||
`{{>foo/bar.baz }}`,
|
||||
[]Token{tokOpenPartial, tokID("foo"), tokSep("/"), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a comment as "COMMENT"`,
|
||||
`foo {{! this is a comment }} bar {{ baz }}`,
|
||||
[]Token{tokContent("foo "), tokComment("{{! this is a comment }}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a block comment as "COMMENT"`,
|
||||
`foo {{!-- this is a {{comment}} --}} bar {{ baz }}`,
|
||||
[]Token{tokContent("foo "), tokComment("{{!-- this is a {{comment}} --}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes a block comment with whitespace as "COMMENT"`,
|
||||
"foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}",
|
||||
[]Token{tokContent("foo "), tokComment("{{!-- this is a\n{{comment}}\n--}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE`,
|
||||
`{{#foo}}content{{/foo}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokClose, tokContent("content"), tokOpenEndBlock, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections as "INVERSE"`,
|
||||
`{{^}}`,
|
||||
[]Token{tokInverse("{{^}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections as "INVERSE" with alternate format`,
|
||||
`{{else}}`,
|
||||
[]Token{tokInverse("{{else}}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections as "INVERSE" with spaces`,
|
||||
`{{ else }}`,
|
||||
[]Token{tokInverse("{{ else }}"), tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"`,
|
||||
`{{^foo}}`,
|
||||
[]Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"`,
|
||||
`{{^ foo }}`,
|
||||
[]Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes mustaches with params as "OPEN ID ID ID CLOSE"`,
|
||||
`{{ foo bar baz }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"`,
|
||||
`{{ foo bar "baz" }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"`,
|
||||
`{{ foo bar 'baz' }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes String params with spaces inside as "STRING"`,
|
||||
`{{ foo bar "baz bat" }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz bat"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes String params with escapes quotes as STRING`,
|
||||
`{{ foo "bar\"baz" }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokString(`bar"baz`), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes String params using single quotes with escapes quotes as STRING`,
|
||||
`{{ foo 'bar\'baz' }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokString(`bar'baz`), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes numbers`,
|
||||
`{{ foo 1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokNumber("1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes floats`,
|
||||
`{{ foo 1.1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokNumber("1.1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes negative numbers`,
|
||||
`{{ foo -1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokNumber("-1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes negative floats`,
|
||||
`{{ foo -1.1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokNumber("-1.1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes boolean true`,
|
||||
`{{ foo true }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokBool("true"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes boolean false`,
|
||||
`{{ foo false }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokBool("false"), tokClose, tokEOF},
|
||||
},
|
||||
// SKIP: 'tokenizes undefined and null'
|
||||
{
|
||||
`tokenizes hash arguments (1)`,
|
||||
`{{ foo bar=baz }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (2)`,
|
||||
`{{ foo bar baz=bat }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (3)`,
|
||||
`{{ foo bar baz=1 }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokNumber("1"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (4)`,
|
||||
`{{ foo bar baz=true }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("true"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (5)`,
|
||||
`{{ foo bar baz=false }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("false"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (6)`,
|
||||
"{{ foo bar\n baz=bat }}",
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (7)`,
|
||||
`{{ foo bar baz="bat" }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (8)`,
|
||||
`{{ foo bar baz="bat" bam=wot }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokID("bam"), tokEquals, tokID("wot"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes hash arguments (9)`,
|
||||
`{{foo omg bar=baz bat="bam"}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("omg"), tokID("bar"), tokEquals, tokID("baz"), tokID("bat"), tokEquals, tokString("bam"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes special @ identifiers (1)`,
|
||||
`{{ @foo }}`,
|
||||
[]Token{tokOpen, tokData, tokID("foo"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes special @ identifiers (2)`,
|
||||
`{{ foo @bar }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokData, tokID("bar"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes special @ identifiers (3)`,
|
||||
`{{ foo bar=@baz }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokData, tokID("baz"), tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`does not time out in a mustache with a single } followed by EOF`,
|
||||
`{{foo}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '}'")},
|
||||
},
|
||||
{
|
||||
`does not time out in a mustache when invalid ID characters are used`,
|
||||
`{{foo & }}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '&'")},
|
||||
},
|
||||
{
|
||||
`tokenizes subexpressions (1)`,
|
||||
`{{foo (bar)}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokCloseSexpr, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes subexpressions (2)`,
|
||||
`{{foo (a-x b-y)}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("a-x"), tokID("b-y"), tokCloseSexpr, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes nested subexpressions`,
|
||||
`{{foo (bar (lol rofl)) (baz)}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokID("rofl"), tokCloseSexpr, tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokCloseSexpr, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes nested subexpressions: literals`,
|
||||
`{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg "c")}}`,
|
||||
[]Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokBool("true"), tokCloseSexpr, tokBool("false"), tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokNumber("1"), tokCloseSexpr, tokOpenSexpr, tokID("blah"), tokString("b"), tokCloseSexpr, tokOpenSexpr, tokID("blorg"), tokString("c"), tokCloseSexpr, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (1)`,
|
||||
`{{#foo as |bar|}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (2)`,
|
||||
`{{#foo as |bar baz|}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (3)`,
|
||||
`{{#foo as | bar baz |}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (4)`,
|
||||
`{{#foo as as | bar baz |}}`,
|
||||
[]Token{tokOpenBlock, tokID("foo"), tokID("as"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
{
|
||||
`tokenizes block params (5)`,
|
||||
`{{else foo as |bar baz|}}`,
|
||||
[]Token{tokOpenInverseChain, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF},
|
||||
},
|
||||
}
|
||||
|
||||
func collect(t *lexTest) []Token {
|
||||
var result []Token
|
||||
|
||||
l := scanWithName(t.input, t.name)
|
||||
for {
|
||||
token := l.NextToken()
|
||||
result = append(result, token)
|
||||
|
||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func equal(i1, i2 []Token, checkPos bool) bool {
|
||||
if len(i1) != len(i2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k := range i1 {
|
||||
if i1[k].Kind != i2[k].Kind {
|
||||
return false
|
||||
}
|
||||
|
||||
if checkPos && i1[k].Pos != i2[k].Pos {
|
||||
return false
|
||||
}
|
||||
|
||||
if i1[k].Val != i2[k].Val {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range lexTests {
|
||||
tokens := collect(&test)
|
||||
if !equal(tokens, test.tokens, false) {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%v\ngot\n\t%+v\n", test.name, test.input, test.tokens, tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Test errors:
|
||||
// `{{{{raw foo`
|
||||
|
||||
// package example
|
||||
func Example() {
|
||||
source := "You know {{nothing}} John Snow"
|
||||
|
||||
output := ""
|
||||
|
||||
lex := Scan(source)
|
||||
for {
|
||||
// consume next token
|
||||
token := lex.NextToken()
|
||||
|
||||
output += fmt.Sprintf(" %s", token)
|
||||
|
||||
// stops when all tokens have been consumed, or on error
|
||||
if token.Kind == TokenEOF || token.Kind == TokenError {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: Content{"You know "} Open{"{{"} ID{"nothing"} Close{"}}"} Content{" John Snow"} EOF
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package lexer
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
// TokenError represents an error
|
||||
TokenError TokenKind = iota
|
||||
|
||||
// TokenEOF represents an End Of File
|
||||
TokenEOF
|
||||
|
||||
//
|
||||
// Mustache delimiters
|
||||
//
|
||||
|
||||
// TokenOpen is the OPEN token
|
||||
TokenOpen
|
||||
|
||||
// TokenClose is the CLOSE token
|
||||
TokenClose
|
||||
|
||||
// TokenOpenRawBlock is the OPEN_RAW_BLOCK token
|
||||
TokenOpenRawBlock
|
||||
|
||||
// TokenCloseRawBlock is the CLOSE_RAW_BLOCK token
|
||||
TokenCloseRawBlock
|
||||
|
||||
// TokenOpenEndRawBlock is the END_RAW_BLOCK token
|
||||
TokenOpenEndRawBlock
|
||||
|
||||
// TokenOpenUnescaped is the OPEN_UNESCAPED token
|
||||
TokenOpenUnescaped
|
||||
|
||||
// TokenCloseUnescaped is the CLOSE_UNESCAPED token
|
||||
TokenCloseUnescaped
|
||||
|
||||
// TokenOpenBlock is the OPEN_BLOCK token
|
||||
TokenOpenBlock
|
||||
|
||||
// TokenOpenEndBlock is the OPEN_ENDBLOCK token
|
||||
TokenOpenEndBlock
|
||||
|
||||
// TokenInverse is the INVERSE token
|
||||
TokenInverse
|
||||
|
||||
// TokenOpenInverse is the OPEN_INVERSE token
|
||||
TokenOpenInverse
|
||||
|
||||
// TokenOpenInverseChain is the OPEN_INVERSE_CHAIN token
|
||||
TokenOpenInverseChain
|
||||
|
||||
// TokenOpenPartial is the OPEN_PARTIAL token
|
||||
TokenOpenPartial
|
||||
|
||||
// TokenComment is the COMMENT token
|
||||
TokenComment
|
||||
|
||||
//
|
||||
// Inside mustaches
|
||||
//
|
||||
|
||||
// TokenOpenSexpr is the OPEN_SEXPR token
|
||||
TokenOpenSexpr
|
||||
|
||||
// TokenCloseSexpr is the CLOSE_SEXPR token
|
||||
TokenCloseSexpr
|
||||
|
||||
// TokenEquals is the EQUALS token
|
||||
TokenEquals
|
||||
|
||||
// TokenData is the DATA token
|
||||
TokenData
|
||||
|
||||
// TokenSep is the SEP token
|
||||
TokenSep
|
||||
|
||||
// TokenOpenBlockParams is the OPEN_BLOCK_PARAMS token
|
||||
TokenOpenBlockParams
|
||||
|
||||
// TokenCloseBlockParams is the CLOSE_BLOCK_PARAMS token
|
||||
TokenCloseBlockParams
|
||||
|
||||
//
|
||||
// Tokens with content
|
||||
//
|
||||
|
||||
// TokenContent is the CONTENT token
|
||||
TokenContent
|
||||
|
||||
// TokenID is the ID token
|
||||
TokenID
|
||||
|
||||
// TokenString is the STRING token
|
||||
TokenString
|
||||
|
||||
// TokenNumber is the NUMBER token
|
||||
TokenNumber
|
||||
|
||||
// TokenBoolean is the BOOLEAN token
|
||||
TokenBoolean
|
||||
)
|
||||
|
||||
const (
|
||||
// Option to generate token position in its string representation
|
||||
dumpTokenPos = false
|
||||
|
||||
// Option to generate values for all token kinds for their string representations
|
||||
dumpAllTokensVal = true
|
||||
)
|
||||
|
||||
// TokenKind represents a Token type.
|
||||
type TokenKind int
|
||||
|
||||
// Token represents a scanned token.
|
||||
type Token struct {
|
||||
Kind TokenKind // Token kind
|
||||
Val string // Token value
|
||||
|
||||
Pos int // Byte position in input string
|
||||
Line int // Line number in input string
|
||||
}
|
||||
|
||||
// tokenName permits to display token name given token type
|
||||
var tokenName = map[TokenKind]string{
|
||||
TokenError: "Error",
|
||||
TokenEOF: "EOF",
|
||||
TokenContent: "Content",
|
||||
TokenComment: "Comment",
|
||||
TokenOpen: "Open",
|
||||
TokenClose: "Close",
|
||||
TokenOpenUnescaped: "OpenUnescaped",
|
||||
TokenCloseUnescaped: "CloseUnescaped",
|
||||
TokenOpenBlock: "OpenBlock",
|
||||
TokenOpenEndBlock: "OpenEndBlock",
|
||||
TokenOpenRawBlock: "OpenRawBlock",
|
||||
TokenCloseRawBlock: "CloseRawBlock",
|
||||
TokenOpenEndRawBlock: "OpenEndRawBlock",
|
||||
TokenOpenBlockParams: "OpenBlockParams",
|
||||
TokenCloseBlockParams: "CloseBlockParams",
|
||||
TokenInverse: "Inverse",
|
||||
TokenOpenInverse: "OpenInverse",
|
||||
TokenOpenInverseChain: "OpenInverseChain",
|
||||
TokenOpenPartial: "OpenPartial",
|
||||
TokenOpenSexpr: "OpenSexpr",
|
||||
TokenCloseSexpr: "CloseSexpr",
|
||||
TokenID: "ID",
|
||||
TokenEquals: "Equals",
|
||||
TokenString: "String",
|
||||
TokenNumber: "Number",
|
||||
TokenBoolean: "Boolean",
|
||||
TokenData: "Data",
|
||||
TokenSep: "Sep",
|
||||
}
|
||||
|
||||
// String returns the token kind string representation for debugging.
|
||||
func (k TokenKind) String() string {
|
||||
s := tokenName[k]
|
||||
if s == "" {
|
||||
return fmt.Sprintf("Token-%d", int(k))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// String returns the token string representation for debugging.
|
||||
func (t Token) String() string {
|
||||
result := ""
|
||||
|
||||
if dumpTokenPos {
|
||||
result += fmt.Sprintf("%d:", t.Pos)
|
||||
}
|
||||
|
||||
result += fmt.Sprintf("%s", t.Kind)
|
||||
|
||||
if (dumpAllTokensVal || (t.Kind >= TokenContent)) && len(t.Val) > 0 {
|
||||
if len(t.Val) > 100 {
|
||||
result += fmt.Sprintf("{%.20q...}", t.Val)
|
||||
} else {
|
||||
result += fmt.Sprintf("{%q}", t.Val)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
//
|
||||
// Note, as the JS implementation, the divergences from mustache spec:
|
||||
// - we don't support alternative delimeters
|
||||
// - the mustache lambda spec differs
|
||||
//
|
||||
|
||||
type mustacheTest struct {
|
||||
Name string
|
||||
Desc string
|
||||
Data interface{}
|
||||
Template string
|
||||
Expected string
|
||||
Partials map[string]string
|
||||
}
|
||||
|
||||
type mustacheTestFile struct {
|
||||
Overview string
|
||||
Tests []mustacheTest
|
||||
}
|
||||
|
||||
var (
|
||||
rAltDelim = regexp.MustCompile(regexp.QuoteMeta("{{="))
|
||||
)
|
||||
|
||||
var (
|
||||
musTestLambdaInterMult = 0
|
||||
)
|
||||
|
||||
func TestMustache(t *testing.T) {
|
||||
skipFiles := map[string]bool{
|
||||
// mustache lambdas differ from handlebars lambdas
|
||||
"~lambdas.yml": true,
|
||||
}
|
||||
|
||||
for _, fileName := range mustacheTestFiles() {
|
||||
if skipFiles[fileName] {
|
||||
// fmt.Printf("Skipped file: %s\n", fileName)
|
||||
continue
|
||||
}
|
||||
|
||||
launchTests(t, testsFromMustacheFile(fileName))
|
||||
}
|
||||
}
|
||||
|
||||
func testsFromMustacheFile(fileName string) []Test {
|
||||
result := []Test{}
|
||||
|
||||
fileData, err := ioutil.ReadFile(path.Join("mustache", "specs", fileName))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var testFile mustacheTestFile
|
||||
if err := yaml.Unmarshal(fileData, &testFile); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, mustacheTest := range testFile.Tests {
|
||||
if mustBeSkipped(mustacheTest, fileName) {
|
||||
// fmt.Printf("Skipped test: %s\n", mustacheTest.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
test := Test{
|
||||
name: mustacheTest.Name,
|
||||
input: mustacheTest.Template,
|
||||
data: mustacheTest.Data,
|
||||
partials: mustacheTest.Partials,
|
||||
output: mustacheTest.Expected,
|
||||
}
|
||||
|
||||
result = append(result, test)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// returns true if test must be skipped
|
||||
func mustBeSkipped(test mustacheTest, fileName string) bool {
|
||||
// handlebars does not support alternative delimiters
|
||||
return haveAltDelimiter(test) ||
|
||||
// the JS implementation skips those tests
|
||||
fileName == "partials.yml" && (test.Name == "Failed Lookup" || test.Name == "Standalone Indentation")
|
||||
}
|
||||
|
||||
// returns true if test have alternative delimeter in template or in partials
|
||||
func haveAltDelimiter(test mustacheTest) bool {
|
||||
// check template
|
||||
if rAltDelim.MatchString(test.Template) {
|
||||
return true
|
||||
}
|
||||
|
||||
// check partials
|
||||
for _, partial := range test.Partials {
|
||||
if rAltDelim.MatchString(partial) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func mustacheTestFiles() []string {
|
||||
var result []string
|
||||
|
||||
files, err := ioutil.ReadDir(path.Join("mustache", "specs"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
fileName := file.Name()
|
||||
|
||||
if !file.IsDir() && strings.HasSuffix(fileName, ".yml") {
|
||||
result = append(result, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
//
|
||||
// Following tests come fron ~lambdas.yml
|
||||
//
|
||||
|
||||
var mustacheLambdasTests = []Test{
|
||||
{
|
||||
"Interpolation",
|
||||
"Hello, {{lambda}}!",
|
||||
map[string]interface{}{"lambda": func() string { return "world" }},
|
||||
nil, nil, nil,
|
||||
"Hello, world!",
|
||||
},
|
||||
|
||||
// // SKIP: lambda return value is not parsed
|
||||
// {
|
||||
// "Interpolation - Expansion",
|
||||
// "Hello, {{lambda}}!",
|
||||
// map[string]interface{}{"lambda": func() string { return "{{planet}}" }},
|
||||
// nil, nil, nil,
|
||||
// "Hello, world!",
|
||||
// },
|
||||
|
||||
// SKIP "Interpolation - Alternate Delimiters"
|
||||
|
||||
{
|
||||
"Interpolation - Multiple Calls",
|
||||
"{{lambda}} == {{{lambda}}} == {{lambda}}",
|
||||
map[string]interface{}{"lambda": func() string {
|
||||
musTestLambdaInterMult++
|
||||
return Str(musTestLambdaInterMult)
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"1 == 2 == 3",
|
||||
},
|
||||
|
||||
{
|
||||
"Escaping",
|
||||
"<{{lambda}}{{{lambda}}}",
|
||||
map[string]interface{}{"lambda": func() string { return ">" }},
|
||||
nil, nil, nil,
|
||||
"<>>",
|
||||
},
|
||||
|
||||
// // SKIP: "Lambdas used for sections should receive the raw section string."
|
||||
// {
|
||||
// "Section",
|
||||
// "<{{#lambda}}{{x}}{{/lambda}}>",
|
||||
// map[string]interface{}{"lambda": func(param string) string {
|
||||
// if param == "{{x}}" {
|
||||
// return "yes"
|
||||
// }
|
||||
|
||||
// return "false"
|
||||
// }, "x": "Error!"},
|
||||
// nil, nil, nil,
|
||||
// "<yes>",
|
||||
// },
|
||||
|
||||
// // SKIP: lambda return value is not parsed
|
||||
// {
|
||||
// "Section - Expansion",
|
||||
// "<{{#lambda}}-{{/lambda}}>",
|
||||
// map[string]interface{}{"lambda": func(param string) string {
|
||||
// return param + "{{planet}}" + param
|
||||
// }, "planet": "Earth"},
|
||||
// nil, nil, nil,
|
||||
// "<-Earth->",
|
||||
// },
|
||||
|
||||
// SKIP: "Section - Alternate Delimiters"
|
||||
|
||||
{
|
||||
"Section - Multiple Calls",
|
||||
"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}",
|
||||
map[string]interface{}{"lambda": func(options *Options) string {
|
||||
return "__" + options.Fn() + "__"
|
||||
}},
|
||||
nil, nil, nil,
|
||||
"__FILE__ != __LINE__",
|
||||
},
|
||||
|
||||
// // SKIP: "Lambdas used for inverted sections should be considered truthy."
|
||||
// {
|
||||
// "Inverted Section",
|
||||
// "<{{^lambda}}{{static}}{{/lambda}}>",
|
||||
// map[string]interface{}{
|
||||
// "lambda": func() interface{} {
|
||||
// return false
|
||||
// },
|
||||
// "static": "static",
|
||||
// },
|
||||
// nil, nil, nil,
|
||||
// "<>",
|
||||
// },
|
||||
}
|
||||
|
||||
func TestMustacheLambdas(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
launchTests(t, mustacheLambdasTests)
|
||||
}
|
||||
+846
@@ -0,0 +1,846 @@
|
||||
// Package parser provides a handlebars syntax analyser. It consumes the tokens provided by the lexer to build an AST.
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
"github.com/aymerick/raymond/lexer"
|
||||
)
|
||||
|
||||
// References:
|
||||
// - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.yy
|
||||
// - https://github.com/golang/go/blob/master/src/text/template/parse/parse.go
|
||||
|
||||
// parser is a syntax analyzer.
|
||||
type parser struct {
|
||||
// Lexer
|
||||
lex *lexer.Lexer
|
||||
|
||||
// Root node
|
||||
root ast.Node
|
||||
|
||||
// Tokens parsed but not consumed yet
|
||||
tokens []*lexer.Token
|
||||
|
||||
// All tokens have been retreieved from lexer
|
||||
lexOver bool
|
||||
}
|
||||
|
||||
var (
|
||||
rOpenComment = regexp.MustCompile(`^\{\{~?!-?-?`)
|
||||
rCloseComment = regexp.MustCompile(`-?-?~?\}\}$`)
|
||||
rOpenAmp = regexp.MustCompile(`^\{\{~?&`)
|
||||
)
|
||||
|
||||
// new instanciates a new parser
|
||||
func new(input string) *parser {
|
||||
return &parser{
|
||||
lex: lexer.Scan(input),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse analyzes given input and returns the AST root node.
|
||||
func Parse(input string) (result *ast.Program, err error) {
|
||||
// recover error
|
||||
defer errRecover(&err)
|
||||
|
||||
parser := new(input)
|
||||
|
||||
// parse
|
||||
result = parser.parseProgram()
|
||||
|
||||
// check last token
|
||||
token := parser.shift()
|
||||
if token.Kind != lexer.TokenEOF {
|
||||
// Parsing ended before EOF
|
||||
errToken(token, "Syntax error")
|
||||
}
|
||||
|
||||
// fix whitespaces
|
||||
processWhitespaces(result)
|
||||
|
||||
// named returned values
|
||||
return
|
||||
}
|
||||
|
||||
// errRecover recovers parsing panic
|
||||
func errRecover(errp *error) {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
switch err := e.(type) {
|
||||
case runtime.Error:
|
||||
panic(e)
|
||||
case error:
|
||||
*errp = err
|
||||
default:
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errPanic panics
|
||||
func errPanic(err error, line int) {
|
||||
panic(fmt.Errorf("Parse error on line %d:\n%s", line, err))
|
||||
}
|
||||
|
||||
// errNode panics with given node infos
|
||||
func errNode(node ast.Node, msg string) {
|
||||
errPanic(fmt.Errorf("%s\nNode: %s", msg, node), node.Location().Line)
|
||||
}
|
||||
|
||||
// errNode panics with given Token infos
|
||||
func errToken(tok *lexer.Token, msg string) {
|
||||
errPanic(fmt.Errorf("%s\nToken: %s", msg, tok), tok.Line)
|
||||
}
|
||||
|
||||
// errNode panics because of an unexpected Token kind
|
||||
func errExpected(expect lexer.TokenKind, tok *lexer.Token) {
|
||||
errPanic(fmt.Errorf("Expecting %s, got: '%s'", expect, tok), tok.Line)
|
||||
}
|
||||
|
||||
// program : statement*
|
||||
func (p *parser) parseProgram() *ast.Program {
|
||||
result := ast.NewProgram(p.next().Pos, p.next().Line)
|
||||
|
||||
for p.isStatement() {
|
||||
result.AddStatement(p.parseStatement())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// statement : mustache | block | rawBlock | partial | content | COMMENT
|
||||
func (p *parser) parseStatement() ast.Node {
|
||||
var result ast.Node
|
||||
|
||||
tok := p.next()
|
||||
|
||||
switch tok.Kind {
|
||||
case lexer.TokenOpen, lexer.TokenOpenUnescaped:
|
||||
// mustache
|
||||
result = p.parseMustache()
|
||||
case lexer.TokenOpenBlock:
|
||||
// block
|
||||
result = p.parseBlock()
|
||||
case lexer.TokenOpenInverse:
|
||||
// block
|
||||
result = p.parseInverse()
|
||||
case lexer.TokenOpenRawBlock:
|
||||
// rawBlock
|
||||
result = p.parseRawBlock()
|
||||
case lexer.TokenOpenPartial:
|
||||
// partial
|
||||
result = p.parsePartial()
|
||||
case lexer.TokenContent:
|
||||
// content
|
||||
result = p.parseContent()
|
||||
case lexer.TokenComment:
|
||||
// COMMENT
|
||||
result = p.parseComment()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// isStatement returns true if next token starts a statement
|
||||
func (p *parser) isStatement() bool {
|
||||
if !p.have(1) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch p.next().Kind {
|
||||
case lexer.TokenOpen, lexer.TokenOpenUnescaped, lexer.TokenOpenBlock,
|
||||
lexer.TokenOpenInverse, lexer.TokenOpenRawBlock, lexer.TokenOpenPartial,
|
||||
lexer.TokenContent, lexer.TokenComment:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// content : CONTENT
|
||||
func (p *parser) parseContent() *ast.ContentStatement {
|
||||
// CONTENT
|
||||
tok := p.shift()
|
||||
if tok.Kind != lexer.TokenContent {
|
||||
// @todo This check can be removed if content is optional in a raw block
|
||||
errExpected(lexer.TokenContent, tok)
|
||||
}
|
||||
|
||||
return ast.NewContentStatement(tok.Pos, tok.Line, tok.Val)
|
||||
}
|
||||
|
||||
// COMMENT
|
||||
func (p *parser) parseComment() *ast.CommentStatement {
|
||||
// COMMENT
|
||||
tok := p.shift()
|
||||
|
||||
value := rOpenComment.ReplaceAllString(tok.Val, "")
|
||||
value = rCloseComment.ReplaceAllString(value, "")
|
||||
|
||||
result := ast.NewCommentStatement(tok.Pos, tok.Line, value)
|
||||
result.Strip = ast.NewStripForStr(tok.Val)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// param* hash?
|
||||
func (p *parser) parseExpressionParamsHash() ([]ast.Node, *ast.Hash) {
|
||||
var params []ast.Node
|
||||
var hash *ast.Hash
|
||||
|
||||
// params*
|
||||
if p.isParam() {
|
||||
params = p.parseParams()
|
||||
}
|
||||
|
||||
// hash?
|
||||
if p.isHashSegment() {
|
||||
hash = p.parseHash()
|
||||
}
|
||||
|
||||
return params, hash
|
||||
}
|
||||
|
||||
// helperName param* hash?
|
||||
func (p *parser) parseExpression(tok *lexer.Token) *ast.Expression {
|
||||
result := ast.NewExpression(tok.Pos, tok.Line)
|
||||
|
||||
// helperName
|
||||
result.Path = p.parseHelperName()
|
||||
|
||||
// param* hash?
|
||||
result.Params, result.Hash = p.parseExpressionParamsHash()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// rawBlock : openRawBlock content endRawBlock
|
||||
// openRawBlock : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK
|
||||
// endRawBlock : OPEN_END_RAW_BLOCK helperName CLOSE_RAW_BLOCK
|
||||
func (p *parser) parseRawBlock() *ast.BlockStatement {
|
||||
// OPEN_RAW_BLOCK
|
||||
tok := p.shift()
|
||||
|
||||
result := ast.NewBlockStatement(tok.Pos, tok.Line)
|
||||
|
||||
// helperName param* hash?
|
||||
result.Expression = p.parseExpression(tok)
|
||||
|
||||
openName := result.Expression.Canonical()
|
||||
|
||||
// CLOSE_RAW_BLOCK
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenCloseRawBlock {
|
||||
errExpected(lexer.TokenCloseRawBlock, tok)
|
||||
}
|
||||
|
||||
// content
|
||||
// @todo Is content mandatory in a raw block ?
|
||||
content := p.parseContent()
|
||||
|
||||
program := ast.NewProgram(tok.Pos, tok.Line)
|
||||
program.AddStatement(content)
|
||||
|
||||
result.Program = program
|
||||
|
||||
// OPEN_END_RAW_BLOCK
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenOpenEndRawBlock {
|
||||
// should never happen as it is caught by lexer
|
||||
errExpected(lexer.TokenOpenEndRawBlock, tok)
|
||||
}
|
||||
|
||||
// helperName
|
||||
endID := p.parseHelperName()
|
||||
|
||||
closeName, ok := ast.HelperNameStr(endID)
|
||||
if !ok {
|
||||
errNode(endID, "Erroneous closing expression")
|
||||
}
|
||||
|
||||
if openName != closeName {
|
||||
errNode(endID, fmt.Sprintf("%s doesn't match %s", openName, closeName))
|
||||
}
|
||||
|
||||
// CLOSE_RAW_BLOCK
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenCloseRawBlock {
|
||||
errExpected(lexer.TokenCloseRawBlock, tok)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// block : openBlock program inverseChain? closeBlock
|
||||
func (p *parser) parseBlock() *ast.BlockStatement {
|
||||
// openBlock
|
||||
result, blockParams := p.parseOpenBlock()
|
||||
|
||||
// program
|
||||
program := p.parseProgram()
|
||||
program.BlockParams = blockParams
|
||||
result.Program = program
|
||||
|
||||
// inverseChain?
|
||||
if p.isInverseChain() {
|
||||
result.Inverse = p.parseInverseChain()
|
||||
}
|
||||
|
||||
// closeBlock
|
||||
p.parseCloseBlock(result)
|
||||
|
||||
setBlockInverseStrip(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// setBlockInverseStrip is called when parsing `block` (openBlock | openInverse) and `inverseChain`
|
||||
//
|
||||
// TODO: This was totally cargo culted ! CHECK THAT !
|
||||
//
|
||||
// cf. prepareBlock() in:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/helper.js
|
||||
func setBlockInverseStrip(block *ast.BlockStatement) {
|
||||
if block.Inverse == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if block.Inverse.Chained {
|
||||
b, _ := block.Inverse.Body[0].(*ast.BlockStatement)
|
||||
b.CloseStrip = block.CloseStrip
|
||||
}
|
||||
|
||||
block.InverseStrip = block.Inverse.Strip
|
||||
}
|
||||
|
||||
// block : openInverse program inverseAndProgram? closeBlock
|
||||
func (p *parser) parseInverse() *ast.BlockStatement {
|
||||
// openInverse
|
||||
result, blockParams := p.parseOpenBlock()
|
||||
|
||||
// program
|
||||
program := p.parseProgram()
|
||||
|
||||
program.BlockParams = blockParams
|
||||
result.Inverse = program
|
||||
|
||||
// inverseAndProgram?
|
||||
if p.isInverse() {
|
||||
result.Program = p.parseInverseAndProgram()
|
||||
}
|
||||
|
||||
// closeBlock
|
||||
p.parseCloseBlock(result)
|
||||
|
||||
setBlockInverseStrip(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// helperName param* hash? blockParams?
|
||||
func (p *parser) parseOpenBlockExpression(tok *lexer.Token) (*ast.BlockStatement, []string) {
|
||||
var blockParams []string
|
||||
|
||||
result := ast.NewBlockStatement(tok.Pos, tok.Line)
|
||||
|
||||
// helperName param* hash?
|
||||
result.Expression = p.parseExpression(tok)
|
||||
|
||||
// blockParams?
|
||||
if p.isBlockParams() {
|
||||
blockParams = p.parseBlockParams()
|
||||
}
|
||||
|
||||
// named returned values
|
||||
return result, blockParams
|
||||
}
|
||||
|
||||
// inverseChain : openInverseChain program inverseChain?
|
||||
// | inverseAndProgram
|
||||
func (p *parser) parseInverseChain() *ast.Program {
|
||||
if p.isInverse() {
|
||||
// inverseAndProgram
|
||||
return p.parseInverseAndProgram()
|
||||
}
|
||||
|
||||
result := ast.NewProgram(p.next().Pos, p.next().Line)
|
||||
|
||||
// openInverseChain
|
||||
block, blockParams := p.parseOpenBlock()
|
||||
|
||||
// program
|
||||
program := p.parseProgram()
|
||||
|
||||
program.BlockParams = blockParams
|
||||
block.Program = program
|
||||
|
||||
// inverseChain?
|
||||
if p.isInverseChain() {
|
||||
block.Inverse = p.parseInverseChain()
|
||||
}
|
||||
|
||||
setBlockInverseStrip(block)
|
||||
|
||||
result.Chained = true
|
||||
result.AddStatement(block)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns true if current token starts an inverse chain
|
||||
func (p *parser) isInverseChain() bool {
|
||||
return p.isOpenInverseChain() || p.isInverse()
|
||||
}
|
||||
|
||||
// inverseAndProgram : INVERSE program
|
||||
func (p *parser) parseInverseAndProgram() *ast.Program {
|
||||
// INVERSE
|
||||
tok := p.shift()
|
||||
|
||||
// program
|
||||
result := p.parseProgram()
|
||||
result.Strip = ast.NewStripForStr(tok.Val)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// openBlock : OPEN_BLOCK helperName param* hash? blockParams? CLOSE
|
||||
// openInverse : OPEN_INVERSE helperName param* hash? blockParams? CLOSE
|
||||
// openInverseChain: OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE
|
||||
func (p *parser) parseOpenBlock() (*ast.BlockStatement, []string) {
|
||||
// OPEN_BLOCK | OPEN_INVERSE | OPEN_INVERSE_CHAIN
|
||||
tok := p.shift()
|
||||
|
||||
// helperName param* hash? blockParams?
|
||||
result, blockParams := p.parseOpenBlockExpression(tok)
|
||||
|
||||
// CLOSE
|
||||
tokClose := p.shift()
|
||||
if tokClose.Kind != lexer.TokenClose {
|
||||
errExpected(lexer.TokenClose, tokClose)
|
||||
}
|
||||
|
||||
result.OpenStrip = ast.NewStrip(tok.Val, tokClose.Val)
|
||||
|
||||
// named returned values
|
||||
return result, blockParams
|
||||
}
|
||||
|
||||
// closeBlock : OPEN_ENDBLOCK helperName CLOSE
|
||||
func (p *parser) parseCloseBlock(block *ast.BlockStatement) {
|
||||
// OPEN_ENDBLOCK
|
||||
tok := p.shift()
|
||||
if tok.Kind != lexer.TokenOpenEndBlock {
|
||||
errExpected(lexer.TokenOpenEndBlock, tok)
|
||||
}
|
||||
|
||||
// helperName
|
||||
endID := p.parseHelperName()
|
||||
|
||||
closeName, ok := ast.HelperNameStr(endID)
|
||||
if !ok {
|
||||
errNode(endID, "Erroneous closing expression")
|
||||
}
|
||||
|
||||
openName := block.Expression.Canonical()
|
||||
if openName != closeName {
|
||||
errNode(endID, fmt.Sprintf("%s doesn't match %s", openName, closeName))
|
||||
}
|
||||
|
||||
// CLOSE
|
||||
tokClose := p.shift()
|
||||
if tokClose.Kind != lexer.TokenClose {
|
||||
errExpected(lexer.TokenClose, tokClose)
|
||||
}
|
||||
|
||||
block.CloseStrip = ast.NewStrip(tok.Val, tokClose.Val)
|
||||
}
|
||||
|
||||
// mustache : OPEN helperName param* hash? CLOSE
|
||||
// | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED
|
||||
func (p *parser) parseMustache() *ast.MustacheStatement {
|
||||
// OPEN | OPEN_UNESCAPED
|
||||
tok := p.shift()
|
||||
|
||||
closeToken := lexer.TokenClose
|
||||
if tok.Kind == lexer.TokenOpenUnescaped {
|
||||
closeToken = lexer.TokenCloseUnescaped
|
||||
}
|
||||
|
||||
unescaped := false
|
||||
if (tok.Kind == lexer.TokenOpenUnescaped) || (rOpenAmp.MatchString(tok.Val)) {
|
||||
unescaped = true
|
||||
}
|
||||
|
||||
result := ast.NewMustacheStatement(tok.Pos, tok.Line, unescaped)
|
||||
|
||||
// helperName param* hash?
|
||||
result.Expression = p.parseExpression(tok)
|
||||
|
||||
// CLOSE | CLOSE_UNESCAPED
|
||||
tokClose := p.shift()
|
||||
if tokClose.Kind != closeToken {
|
||||
errExpected(closeToken, tokClose)
|
||||
}
|
||||
|
||||
result.Strip = ast.NewStrip(tok.Val, tokClose.Val)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// partial : OPEN_PARTIAL partialName param* hash? CLOSE
|
||||
func (p *parser) parsePartial() *ast.PartialStatement {
|
||||
// OPEN_PARTIAL
|
||||
tok := p.shift()
|
||||
|
||||
result := ast.NewPartialStatement(tok.Pos, tok.Line)
|
||||
|
||||
// partialName
|
||||
result.Name = p.parsePartialName()
|
||||
|
||||
// param* hash?
|
||||
result.Params, result.Hash = p.parseExpressionParamsHash()
|
||||
|
||||
// CLOSE
|
||||
tokClose := p.shift()
|
||||
if tokClose.Kind != lexer.TokenClose {
|
||||
errExpected(lexer.TokenClose, tokClose)
|
||||
}
|
||||
|
||||
result.Strip = ast.NewStrip(tok.Val, tokClose.Val)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// helperName | sexpr
|
||||
func (p *parser) parseHelperNameOrSexpr() ast.Node {
|
||||
if p.isSexpr() {
|
||||
// sexpr
|
||||
return p.parseSexpr()
|
||||
}
|
||||
|
||||
// helperName
|
||||
return p.parseHelperName()
|
||||
}
|
||||
|
||||
// param : helperName | sexpr
|
||||
func (p *parser) parseParam() ast.Node {
|
||||
return p.parseHelperNameOrSexpr()
|
||||
}
|
||||
|
||||
// Returns true if next tokens represent a `param`
|
||||
func (p *parser) isParam() bool {
|
||||
return (p.isSexpr() || p.isHelperName()) && !p.isHashSegment()
|
||||
}
|
||||
|
||||
// param*
|
||||
func (p *parser) parseParams() []ast.Node {
|
||||
var result []ast.Node
|
||||
|
||||
for p.isParam() {
|
||||
result = append(result, p.parseParam())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// sexpr : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR
|
||||
func (p *parser) parseSexpr() *ast.SubExpression {
|
||||
// OPEN_SEXPR
|
||||
tok := p.shift()
|
||||
|
||||
result := ast.NewSubExpression(tok.Pos, tok.Line)
|
||||
|
||||
// helperName param* hash?
|
||||
result.Expression = p.parseExpression(tok)
|
||||
|
||||
// CLOSE_SEXPR
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenCloseSexpr {
|
||||
errExpected(lexer.TokenCloseSexpr, tok)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// hash : hashSegment+
|
||||
func (p *parser) parseHash() *ast.Hash {
|
||||
var pairs []*ast.HashPair
|
||||
|
||||
for p.isHashSegment() {
|
||||
pairs = append(pairs, p.parseHashSegment())
|
||||
}
|
||||
|
||||
firstLoc := pairs[0].Location()
|
||||
|
||||
result := ast.NewHash(firstLoc.Pos, firstLoc.Line)
|
||||
result.Pairs = pairs
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// returns true if next tokens represents a `hashSegment`
|
||||
func (p *parser) isHashSegment() bool {
|
||||
return p.have(2) && (p.next().Kind == lexer.TokenID) && (p.nextAt(1).Kind == lexer.TokenEquals)
|
||||
}
|
||||
|
||||
// hashSegment : ID EQUALS param
|
||||
func (p *parser) parseHashSegment() *ast.HashPair {
|
||||
// ID
|
||||
tok := p.shift()
|
||||
|
||||
// EQUALS
|
||||
p.shift()
|
||||
|
||||
// param
|
||||
param := p.parseParam()
|
||||
|
||||
result := ast.NewHashPair(tok.Pos, tok.Line)
|
||||
result.Key = tok.Val
|
||||
result.Val = param
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// blockParams : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS
|
||||
func (p *parser) parseBlockParams() []string {
|
||||
var result []string
|
||||
|
||||
// OPEN_BLOCK_PARAMS
|
||||
tok := p.shift()
|
||||
|
||||
// ID+
|
||||
for p.isID() {
|
||||
result = append(result, p.shift().Val)
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
errExpected(lexer.TokenID, p.next())
|
||||
}
|
||||
|
||||
// CLOSE_BLOCK_PARAMS
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenCloseBlockParams {
|
||||
errExpected(lexer.TokenCloseBlockParams, tok)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// helperName : path | dataName | STRING | NUMBER | BOOLEAN | UNDEFINED | NULL
|
||||
func (p *parser) parseHelperName() ast.Node {
|
||||
var result ast.Node
|
||||
|
||||
tok := p.next()
|
||||
|
||||
switch tok.Kind {
|
||||
case lexer.TokenBoolean:
|
||||
// BOOLEAN
|
||||
p.shift()
|
||||
result = ast.NewBooleanLiteral(tok.Pos, tok.Line, (tok.Val == "true"), tok.Val)
|
||||
case lexer.TokenNumber:
|
||||
// NUMBER
|
||||
p.shift()
|
||||
|
||||
val, isInt := parseNumber(tok)
|
||||
result = ast.NewNumberLiteral(tok.Pos, tok.Line, val, isInt, tok.Val)
|
||||
case lexer.TokenString:
|
||||
// STRING
|
||||
p.shift()
|
||||
result = ast.NewStringLiteral(tok.Pos, tok.Line, tok.Val)
|
||||
case lexer.TokenData:
|
||||
// dataName
|
||||
result = p.parseDataName()
|
||||
default:
|
||||
// path
|
||||
result = p.parsePath(false)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseNumber parses a number
|
||||
func parseNumber(tok *lexer.Token) (result float64, isInt bool) {
|
||||
var valInt int
|
||||
var err error
|
||||
|
||||
valInt, err = strconv.Atoi(tok.Val)
|
||||
if err == nil {
|
||||
isInt = true
|
||||
|
||||
result = float64(valInt)
|
||||
} else {
|
||||
isInt = false
|
||||
|
||||
result, err = strconv.ParseFloat(tok.Val, 64)
|
||||
if err != nil {
|
||||
errToken(tok, fmt.Sprintf("Failed to parse number: %s", tok.Val))
|
||||
}
|
||||
}
|
||||
|
||||
// named returned values
|
||||
return
|
||||
}
|
||||
|
||||
// Returns true if next tokens represent a `helperName`
|
||||
func (p *parser) isHelperName() bool {
|
||||
switch p.next().Kind {
|
||||
case lexer.TokenBoolean, lexer.TokenNumber, lexer.TokenString, lexer.TokenData, lexer.TokenID:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// partialName : helperName | sexpr
|
||||
func (p *parser) parsePartialName() ast.Node {
|
||||
return p.parseHelperNameOrSexpr()
|
||||
}
|
||||
|
||||
// dataName : DATA pathSegments
|
||||
func (p *parser) parseDataName() *ast.PathExpression {
|
||||
// DATA
|
||||
p.shift()
|
||||
|
||||
// pathSegments
|
||||
return p.parsePath(true)
|
||||
}
|
||||
|
||||
// path : pathSegments
|
||||
// pathSegments : pathSegments SEP ID
|
||||
// | ID
|
||||
func (p *parser) parsePath(data bool) *ast.PathExpression {
|
||||
var tok *lexer.Token
|
||||
|
||||
// ID
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenID {
|
||||
errExpected(lexer.TokenID, tok)
|
||||
}
|
||||
|
||||
result := ast.NewPathExpression(tok.Pos, tok.Line, data)
|
||||
result.Part(tok.Val)
|
||||
|
||||
for p.isPathSep() {
|
||||
// SEP
|
||||
tok = p.shift()
|
||||
result.Sep(tok.Val)
|
||||
|
||||
// ID
|
||||
tok = p.shift()
|
||||
if tok.Kind != lexer.TokenID {
|
||||
errExpected(lexer.TokenID, tok)
|
||||
}
|
||||
|
||||
result.Part(tok.Val)
|
||||
|
||||
if len(result.Parts) > 0 {
|
||||
switch tok.Val {
|
||||
case "..", ".", "this":
|
||||
errToken(tok, "Invalid path: "+result.Original)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Ensures there is token to parse at given index
|
||||
func (p *parser) ensure(index int) {
|
||||
if p.lexOver {
|
||||
// nothing more to grab
|
||||
return
|
||||
}
|
||||
|
||||
nb := index + 1
|
||||
|
||||
for len(p.tokens) < nb {
|
||||
// fetch next token
|
||||
tok := p.lex.NextToken()
|
||||
|
||||
// queue it
|
||||
p.tokens = append(p.tokens, &tok)
|
||||
|
||||
if (tok.Kind == lexer.TokenEOF) || (tok.Kind == lexer.TokenError) {
|
||||
p.lexOver = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// have returns true is there are a list given number of tokens to consume left
|
||||
func (p *parser) have(nb int) bool {
|
||||
p.ensure(nb - 1)
|
||||
|
||||
return len(p.tokens) >= nb
|
||||
}
|
||||
|
||||
// nextAt returns next token at given index, without consuming it
|
||||
func (p *parser) nextAt(index int) *lexer.Token {
|
||||
p.ensure(index)
|
||||
|
||||
return p.tokens[index]
|
||||
}
|
||||
|
||||
// next returns next token without consuming it
|
||||
func (p *parser) next() *lexer.Token {
|
||||
return p.nextAt(0)
|
||||
}
|
||||
|
||||
// shift returns next token and remove it from the tokens buffer
|
||||
//
|
||||
// Panics if next token is `TokenError`
|
||||
func (p *parser) shift() *lexer.Token {
|
||||
var result *lexer.Token
|
||||
|
||||
p.ensure(0)
|
||||
|
||||
result, p.tokens = p.tokens[0], p.tokens[1:]
|
||||
|
||||
// check error token
|
||||
if result.Kind == lexer.TokenError {
|
||||
errToken(result, "Lexer error")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// isToken returns true if next token is of given type
|
||||
func (p *parser) isToken(kind lexer.TokenKind) bool {
|
||||
return p.have(1) && p.next().Kind == kind
|
||||
}
|
||||
|
||||
// isSexpr returns true if next token starts a sexpr
|
||||
func (p *parser) isSexpr() bool {
|
||||
return p.isToken(lexer.TokenOpenSexpr)
|
||||
}
|
||||
|
||||
// isPathSep returns true if next token is a path separator
|
||||
func (p *parser) isPathSep() bool {
|
||||
return p.isToken(lexer.TokenSep)
|
||||
}
|
||||
|
||||
// isID returns true if next token is an ID
|
||||
func (p *parser) isID() bool {
|
||||
return p.isToken(lexer.TokenID)
|
||||
}
|
||||
|
||||
// isBlockParams returns true if next token starts a block params
|
||||
func (p *parser) isBlockParams() bool {
|
||||
return p.isToken(lexer.TokenOpenBlockParams)
|
||||
}
|
||||
|
||||
// isInverse returns true if next token starts an INVERSE sequence
|
||||
func (p *parser) isInverse() bool {
|
||||
return p.isToken(lexer.TokenInverse)
|
||||
}
|
||||
|
||||
// isOpenInverseChain returns true if next token is OPEN_INVERSE_CHAIN
|
||||
func (p *parser) isOpenInverseChain() bool {
|
||||
return p.isToken(lexer.TokenOpenInverseChain)
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
"github.com/aymerick/raymond/lexer"
|
||||
)
|
||||
|
||||
type parserTest struct {
|
||||
name string
|
||||
input string
|
||||
output string
|
||||
}
|
||||
|
||||
var parserTests = []parserTest{
|
||||
//
|
||||
// Next tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/parser.js
|
||||
//
|
||||
{"parses simple mustaches (1)", `{{123}}`, "{{ NUMBER{123} [] }}\n"},
|
||||
{"parses simple mustaches (2)", `{{"foo"}}`, "{{ \"foo\" [] }}\n"},
|
||||
{"parses simple mustaches (3)", `{{false}}`, "{{ BOOLEAN{false} [] }}\n"},
|
||||
{"parses simple mustaches (4)", `{{true}}`, "{{ BOOLEAN{true} [] }}\n"},
|
||||
{"parses simple mustaches (5)", `{{foo}}`, "{{ PATH:foo [] }}\n"},
|
||||
{"parses simple mustaches (6)", `{{foo?}}`, "{{ PATH:foo? [] }}\n"},
|
||||
{"parses simple mustaches (7)", `{{foo_}}`, "{{ PATH:foo_ [] }}\n"},
|
||||
{"parses simple mustaches (8)", `{{foo-}}`, "{{ PATH:foo- [] }}\n"},
|
||||
{"parses simple mustaches (9)", `{{foo:}}`, "{{ PATH:foo: [] }}\n"},
|
||||
|
||||
{"parses simple mustaches with data", `{{@foo}}`, "{{ @PATH:foo [] }}\n"},
|
||||
{"parses simple mustaches with data paths", `{{@../foo}}`, "{{ @PATH:foo [] }}\n"},
|
||||
{"parses mustaches with paths", `{{foo/bar}}`, "{{ PATH:foo/bar [] }}\n"},
|
||||
{"parses mustaches with this/foo", `{{this/foo}}`, "{{ PATH:foo [] }}\n"},
|
||||
{"parses mustaches with - in a path", `{{foo-bar}}`, "{{ PATH:foo-bar [] }}\n"},
|
||||
{"parses mustaches with parameters", `{{foo bar}}`, "{{ PATH:foo [PATH:bar] }}\n"},
|
||||
{"parses mustaches with string parameters", `{{foo bar "baz" }}`, "{{ PATH:foo [PATH:bar, \"baz\"] }}\n"},
|
||||
{"parses mustaches with NUMBER parameters", `{{foo 1}}`, "{{ PATH:foo [NUMBER{1}] }}\n"},
|
||||
{"parses mustaches with BOOLEAN parameters (1)", `{{foo true}}`, "{{ PATH:foo [BOOLEAN{true}] }}\n"},
|
||||
{"parses mustaches with BOOLEAN parameters (2)", `{{foo false}}`, "{{ PATH:foo [BOOLEAN{false}] }}\n"},
|
||||
{"parses mustaches with DATA parameters", `{{foo @bar}}`, "{{ PATH:foo [@PATH:bar] }}\n"},
|
||||
|
||||
{"parses mustaches with hash arguments (01)", `{{foo bar=baz}}`, "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n"},
|
||||
{"parses mustaches with hash arguments (02)", `{{foo bar=1}}`, "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n"},
|
||||
{"parses mustaches with hash arguments (03)", `{{foo bar=true}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n"},
|
||||
{"parses mustaches with hash arguments (04)", `{{foo bar=false}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n"},
|
||||
{"parses mustaches with hash arguments (05)", `{{foo bar=@baz}}`, "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n"},
|
||||
{"parses mustaches with hash arguments (06)", `{{foo bar=baz bat=bam}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n"},
|
||||
{"parses mustaches with hash arguments (07)", `{{foo bar=baz bat="bam"}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"},
|
||||
{"parses mustaches with hash arguments (08)", `{{foo bat='bam'}}`, "{{ PATH:foo [] HASH{bat=\"bam\"} }}\n"},
|
||||
{"parses mustaches with hash arguments (09)", `{{foo omg bar=baz bat="bam"}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"},
|
||||
{"parses mustaches with hash arguments (10)", `{{foo omg bar=baz bat="bam" baz=1}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=NUMBER{1}} }}\n"},
|
||||
{"parses mustaches with hash arguments (11)", `{{foo omg bar=baz bat="bam" baz=true}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{true}} }}\n"},
|
||||
{"parses mustaches with hash arguments (12)", `{{foo omg bar=baz bat="bam" baz=false}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{false}} }}\n"},
|
||||
|
||||
{"parses contents followed by a mustache", `foo bar {{baz}}`, "CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"},
|
||||
|
||||
{"parses a partial (1)", `{{> foo }}`, "{{> PARTIAL:foo }}\n"},
|
||||
{"parses a partial (2)", `{{> "foo" }}`, "{{> PARTIAL:foo }}\n"},
|
||||
{"parses a partial (3)", `{{> 1 }}`, "{{> PARTIAL:1 }}\n"},
|
||||
{"parses a partial with context", `{{> foo bar}}`, "{{> PARTIAL:foo PATH:bar }}\n"},
|
||||
{"parses a partial with hash", `{{> foo bar=bat}}`, "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n"},
|
||||
{"parses a partial with context and hash", `{{> foo bar bat=baz}}`, "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n"},
|
||||
{"parses a partial with a complex name", `{{> shared/partial?.bar}}`, "{{> PARTIAL:shared/partial?.bar }}\n"},
|
||||
|
||||
{"parses a comment", `{{! this is a comment }}`, "{{! ' this is a comment ' }}\n"},
|
||||
{"parses a multi-line comment", "{{!\nthis is a multi-line comment\n}}", "{{! '\nthis is a multi-line comment\n' }}\n"},
|
||||
|
||||
{"parses an inverse section", `{{#foo}} bar {{^}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
||||
{"parses an inverse (else-style) section", `{{#foo}} bar {{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
||||
{"parses multiple inverse sections", `{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"},
|
||||
{"parses empty blocks", `{{#foo}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n"},
|
||||
{"parses empty blocks with empty inverse section", `{{#foo}}{{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"},
|
||||
{"parses empty blocks with empty inverse (else-style) section", `{{#foo}}{{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"},
|
||||
{"parses non-empty blocks with empty inverse section", `{{#foo}} bar {{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"},
|
||||
{"parses non-empty blocks with empty inverse (else-style) section", `{{#foo}} bar {{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"},
|
||||
{"parses empty blocks with non-empty inverse section", `{{#foo}}{{^}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"},
|
||||
{"parses empty blocks with non-empty inverse (else-style) section", `{{#foo}}{{else}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"},
|
||||
{"parses a standalone inverse section", `{{^foo}}bar{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"},
|
||||
{"parses block with block params", `{{#foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
||||
{"parses inverse block with block params", `{{^foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
||||
{"parses chained inverse block with block params", `{{#foo}}{{else foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"},
|
||||
}
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range parserTests {
|
||||
output := ""
|
||||
|
||||
node, err := Parse(test.input)
|
||||
if err == nil {
|
||||
output = ast.Print(node)
|
||||
}
|
||||
|
||||
if (err != nil) || (test.output != output) {
|
||||
t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q\nerror:\n\t%s", test.name, test.input, test.output, output, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var parserErrorTests = []parserTest{
|
||||
{"lexer error", `{{! unclosed comment`, "Lexer error"},
|
||||
{"syntax error", `foo{{^}}`, "Syntax error"},
|
||||
|
||||
{"open raw block must be closed", `{{{{raw foo}} bar {{{{/raw}}}}`, "Expecting CloseRawBlock"},
|
||||
{"end raw block must be closed", `{{{{raw foo}}}} bar {{{{/raw}}`, "Expecting CloseRawBlock"},
|
||||
|
||||
{"raw block names must match (1)", `{{{{1}}}}{{foo}}{{{{/raw}}}}`, "1 doesn't match raw"},
|
||||
{"raw block names must match (2)", `{{{{raw}}}}{{foo}}{{{{/1}}}}`, "raw doesn't match 1"},
|
||||
{"raw block names must match (3)", `{{{{goodbyes}}}}test{{{{/hellos}}}}`, "goodbyes doesn't match hellos"},
|
||||
|
||||
{"open block must be closed", `{{#foo bar}}}{{/foo}}`, "Expecting Close"},
|
||||
{"end block must be closed", `{{#foo bar}}{{/foo}}}`, "Expecting Close"},
|
||||
{"an open block must have a end block", `{{#foo}}test`, "Expecting OpenEndBlock"},
|
||||
|
||||
{"block names must match (1)", `{{#1 bar}}{{/foo}}`, "1 doesn't match foo"},
|
||||
{"block names must match (2)", `{{#foo bar}}{{/1}}`, "foo doesn't match 1"},
|
||||
{"block names must match (3)", `{{#foo}}test{{/bar}}`, "foo doesn't match bar"},
|
||||
|
||||
{"an mustache must terminate with a close mustache", `{{foo}}}`, "Expecting Close"},
|
||||
{"an unescaped mustache must terminate with a close unescaped mustache", `{{{foo}}`, "Expecting CloseUnescaped"},
|
||||
|
||||
{"an partial must terminate with a close mustache", `{{> foo}}}`, "Expecting Close"},
|
||||
{"a subexpression must terminate with a close subexpression", `{{foo (false}}`, "Expecting CloseSexpr"},
|
||||
|
||||
{"raises on missing hash value (1)", `{{foo bar=}}`, "Parse error on line 1"},
|
||||
{"raises on missing hash value (2)", `{{foo bar=baz bim=}}`, "Parse error on line 1"},
|
||||
|
||||
{"block param must have at least one param", `{{#foo as ||}}content{{/foo}}`, "Expecting ID"},
|
||||
{"open block params must be closed", `{{#foo as |}}content{{/foo}}`, "Expecting ID"},
|
||||
|
||||
{"a path must start with an ID", `{{#/}}content{{/foo}}`, "Expecting ID"},
|
||||
{"a path must end with an ID", `{{foo/bar/}}`, "Expecting ID"},
|
||||
|
||||
//
|
||||
// Next tests come from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/spec/parser.js
|
||||
//
|
||||
{"throws on old inverse section", `{{else foo}}bar{{/foo}}`, ""},
|
||||
|
||||
{"raises if there's a parser error (1)", `foo{{^}}bar`, "Parse error on line 1"},
|
||||
{"raises if there's a parser error (2)", `{{foo}`, "Parse error on line 1"},
|
||||
{"raises if there's a parser error (3)", `{{foo &}}`, "Parse error on line 1"},
|
||||
{"raises if there's a parser error (4)", `{{#goodbyes}}{{/hellos}}`, "Parse error on line 1"},
|
||||
{"raises if there's a parser error (5)", `{{#goodbyes}}{{/hellos}}`, "goodbyes doesn't match hellos"},
|
||||
|
||||
{"should handle invalid paths (1)", `{{foo/../bar}}`, `Invalid path: foo/..`},
|
||||
{"should handle invalid paths (2)", `{{foo/./bar}}`, `Invalid path: foo/.`},
|
||||
{"should handle invalid paths (3)", `{{foo/this/bar}}`, `Invalid path: foo/this`},
|
||||
|
||||
{"knows how to report the correct line number in errors (1)", "hello\nmy\n{{foo}", "Parse error on line 3"},
|
||||
{"knows how to report the correct line number in errors (2)", "hello\n\nmy\n\n{{foo}", "Parse error on line 5"},
|
||||
|
||||
{"knows how to report the correct line number in errors when the first character is a newline", "\n\nhello\n\nmy\n\n{{foo}", "Parse error on line 7"},
|
||||
}
|
||||
|
||||
func TestParserErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range parserErrorTests {
|
||||
node, err := Parse(test.input)
|
||||
if err == nil {
|
||||
output := ast.Print(node)
|
||||
tokens := lexer.Collect(test.input)
|
||||
|
||||
t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\ntokens:\n\t%q", test.name, test.input, output, tokens)
|
||||
} else if test.output != "" {
|
||||
matched, errMatch := regexp.MatchString(regexp.QuoteMeta(test.output), fmt.Sprint(err))
|
||||
if errMatch != nil {
|
||||
panic("Failed to match regexp")
|
||||
}
|
||||
|
||||
if !matched {
|
||||
t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, test.output, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// package example
|
||||
func Example() {
|
||||
source := "You know {{nothing}} John Snow"
|
||||
|
||||
// parse template
|
||||
program, err := Parse(source)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// print AST
|
||||
output := ast.Print(program)
|
||||
|
||||
fmt.Print(output)
|
||||
// CONTENT[ 'You know ' ]
|
||||
// {{ PATH:nothing [] }}
|
||||
// CONTENT[ ' John Snow' ]
|
||||
}
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
)
|
||||
|
||||
// whitespaceVisitor walks through the AST to perform whitespace control
|
||||
//
|
||||
// The logic was shamelessly borrowed from:
|
||||
// https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/whitespace-control.js
|
||||
type whitespaceVisitor struct {
|
||||
isRootSeen bool
|
||||
}
|
||||
|
||||
var (
|
||||
rTrimLeft = regexp.MustCompile(`^[ \t]*\r?\n?`)
|
||||
rTrimLeftMultiple = regexp.MustCompile(`^\s+`)
|
||||
|
||||
rTrimRight = regexp.MustCompile(`[ \t]+$`)
|
||||
rTrimRightMultiple = regexp.MustCompile(`\s+$`)
|
||||
|
||||
rPrevWhitespace = regexp.MustCompile(`\r?\n\s*?$`)
|
||||
rPrevWhitespaceStart = regexp.MustCompile(`(^|\r?\n)\s*?$`)
|
||||
|
||||
rNextWhitespace = regexp.MustCompile(`^\s*?\r?\n`)
|
||||
rNextWhitespaceEnd = regexp.MustCompile(`^\s*?(\r?\n|$)`)
|
||||
|
||||
rPartialIndent = regexp.MustCompile(`([ \t]+$)`)
|
||||
)
|
||||
|
||||
// newWhitespaceVisitor instanciates a new whitespaceVisitor
|
||||
func newWhitespaceVisitor() *whitespaceVisitor {
|
||||
return &whitespaceVisitor{}
|
||||
}
|
||||
|
||||
// processWhitespaces performs whitespace control on given AST
|
||||
//
|
||||
// WARNING: It must be called only once on AST.
|
||||
func processWhitespaces(node ast.Node) {
|
||||
node.Accept(newWhitespaceVisitor())
|
||||
}
|
||||
|
||||
func omitRightFirst(body []ast.Node, multiple bool) {
|
||||
omitRight(body, -1, multiple)
|
||||
}
|
||||
|
||||
func omitRight(body []ast.Node, i int, multiple bool) {
|
||||
if i+1 >= len(body) {
|
||||
return
|
||||
}
|
||||
|
||||
current := body[i+1]
|
||||
|
||||
node, ok := current.(*ast.ContentStatement)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if !multiple && node.RightStripped {
|
||||
return
|
||||
}
|
||||
|
||||
original := node.Value
|
||||
|
||||
r := rTrimLeft
|
||||
if multiple {
|
||||
r = rTrimLeftMultiple
|
||||
}
|
||||
|
||||
node.Value = r.ReplaceAllString(node.Value, "")
|
||||
|
||||
node.RightStripped = (original != node.Value)
|
||||
}
|
||||
|
||||
func omitLeftLast(body []ast.Node, multiple bool) {
|
||||
omitLeft(body, len(body), multiple)
|
||||
}
|
||||
|
||||
func omitLeft(body []ast.Node, i int, multiple bool) bool {
|
||||
if i-1 < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
current := body[i-1]
|
||||
|
||||
node, ok := current.(*ast.ContentStatement)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !multiple && node.LeftStripped {
|
||||
return false
|
||||
}
|
||||
|
||||
original := node.Value
|
||||
|
||||
r := rTrimRight
|
||||
if multiple {
|
||||
r = rTrimRightMultiple
|
||||
}
|
||||
|
||||
node.Value = r.ReplaceAllString(node.Value, "")
|
||||
|
||||
node.LeftStripped = (original != node.Value)
|
||||
|
||||
return node.LeftStripped
|
||||
}
|
||||
|
||||
func isPrevWhitespace(body []ast.Node) bool {
|
||||
return isPrevWhitespaceProgram(body, len(body), false)
|
||||
}
|
||||
|
||||
func isPrevWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
|
||||
if i < 1 {
|
||||
return isRoot
|
||||
}
|
||||
|
||||
prev := body[i-1]
|
||||
|
||||
if node, ok := prev.(*ast.ContentStatement); ok {
|
||||
if (node.Value == "") && node.RightStripped {
|
||||
// already stripped, so it may be an empty string not catched by regexp
|
||||
return true
|
||||
}
|
||||
|
||||
r := rPrevWhitespaceStart
|
||||
if (i > 1) || !isRoot {
|
||||
r = rPrevWhitespace
|
||||
}
|
||||
|
||||
return r.MatchString(node.Value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isNextWhitespace(body []ast.Node) bool {
|
||||
return isNextWhitespaceProgram(body, -1, false)
|
||||
}
|
||||
|
||||
func isNextWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
|
||||
if i+1 >= len(body) {
|
||||
return isRoot
|
||||
}
|
||||
|
||||
next := body[i+1]
|
||||
|
||||
if node, ok := next.(*ast.ContentStatement); ok {
|
||||
if (node.Value == "") && node.LeftStripped {
|
||||
// already stripped, so it may be an empty string not catched by regexp
|
||||
return true
|
||||
}
|
||||
|
||||
r := rNextWhitespaceEnd
|
||||
if (i+2 > len(body)) || !isRoot {
|
||||
r = rNextWhitespace
|
||||
}
|
||||
|
||||
return r.MatchString(node.Value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//
|
||||
// Visitor interface
|
||||
//
|
||||
|
||||
func (v *whitespaceVisitor) VisitProgram(program *ast.Program) interface{} {
|
||||
isRoot := !v.isRootSeen
|
||||
v.isRootSeen = true
|
||||
|
||||
body := program.Body
|
||||
for i, current := range body {
|
||||
strip, _ := current.Accept(v).(*ast.Strip)
|
||||
if strip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_isPrevWhitespace := isPrevWhitespaceProgram(body, i, isRoot)
|
||||
_isNextWhitespace := isNextWhitespaceProgram(body, i, isRoot)
|
||||
|
||||
openStandalone := strip.OpenStandalone && _isPrevWhitespace
|
||||
closeStandalone := strip.CloseStandalone && _isNextWhitespace
|
||||
inlineStandalone := strip.InlineStandalone && _isPrevWhitespace && _isNextWhitespace
|
||||
|
||||
if strip.Close {
|
||||
omitRight(body, i, true)
|
||||
}
|
||||
|
||||
if strip.Open && (i > 0) {
|
||||
omitLeft(body, i, true)
|
||||
}
|
||||
|
||||
if inlineStandalone {
|
||||
omitRight(body, i, false)
|
||||
|
||||
if omitLeft(body, i, false) {
|
||||
// If we are on a standalone node, save the indent info for partials
|
||||
if partial, ok := current.(*ast.PartialStatement); ok {
|
||||
// Pull out the whitespace from the final line
|
||||
if i > 0 {
|
||||
if prevContent, ok := body[i-1].(*ast.ContentStatement); ok {
|
||||
partial.Indent = rPartialIndent.FindString(prevContent.Original)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b, ok := current.(*ast.BlockStatement); ok {
|
||||
if openStandalone {
|
||||
prog := b.Program
|
||||
if prog == nil {
|
||||
prog = b.Inverse
|
||||
}
|
||||
|
||||
omitRightFirst(prog.Body, false)
|
||||
|
||||
// Strip out the previous content node if it's whitespace only
|
||||
omitLeft(body, i, false)
|
||||
}
|
||||
|
||||
if closeStandalone {
|
||||
prog := b.Inverse
|
||||
if prog == nil {
|
||||
prog = b.Program
|
||||
}
|
||||
|
||||
// Always strip the next node
|
||||
omitRight(body, i, false)
|
||||
|
||||
omitLeftLast(prog.Body, false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *whitespaceVisitor) VisitBlock(block *ast.BlockStatement) interface{} {
|
||||
if block.Program != nil {
|
||||
block.Program.Accept(v)
|
||||
}
|
||||
|
||||
if block.Inverse != nil {
|
||||
block.Inverse.Accept(v)
|
||||
}
|
||||
|
||||
program := block.Program
|
||||
inverse := block.Inverse
|
||||
|
||||
if program == nil {
|
||||
program = inverse
|
||||
inverse = nil
|
||||
}
|
||||
|
||||
firstInverse := inverse
|
||||
lastInverse := inverse
|
||||
|
||||
if (inverse != nil) && inverse.Chained {
|
||||
b, _ := inverse.Body[0].(*ast.BlockStatement)
|
||||
firstInverse = b.Program
|
||||
|
||||
for lastInverse.Chained {
|
||||
b, _ := lastInverse.Body[len(lastInverse.Body)-1].(*ast.BlockStatement)
|
||||
lastInverse = b.Program
|
||||
}
|
||||
}
|
||||
|
||||
closeProg := firstInverse
|
||||
if closeProg == nil {
|
||||
closeProg = program
|
||||
}
|
||||
|
||||
strip := &ast.Strip{
|
||||
Open: (block.OpenStrip != nil) && block.OpenStrip.Open,
|
||||
Close: (block.CloseStrip != nil) && block.CloseStrip.Close,
|
||||
|
||||
OpenStandalone: isNextWhitespace(program.Body),
|
||||
CloseStandalone: isPrevWhitespace(closeProg.Body),
|
||||
}
|
||||
|
||||
if (block.OpenStrip != nil) && block.OpenStrip.Close {
|
||||
omitRightFirst(program.Body, true)
|
||||
}
|
||||
|
||||
if inverse != nil {
|
||||
if block.InverseStrip != nil {
|
||||
inverseStrip := block.InverseStrip
|
||||
|
||||
if inverseStrip.Open {
|
||||
omitLeftLast(program.Body, true)
|
||||
}
|
||||
|
||||
if inverseStrip.Close {
|
||||
omitRightFirst(firstInverse.Body, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (block.CloseStrip != nil) && block.CloseStrip.Open {
|
||||
omitLeftLast(lastInverse.Body, true)
|
||||
}
|
||||
|
||||
// Find standalone else statements
|
||||
if isPrevWhitespace(program.Body) && isNextWhitespace(firstInverse.Body) {
|
||||
omitLeftLast(program.Body, false)
|
||||
|
||||
omitRightFirst(firstInverse.Body, false)
|
||||
}
|
||||
} else if (block.CloseStrip != nil) && block.CloseStrip.Open {
|
||||
omitLeftLast(program.Body, true)
|
||||
}
|
||||
|
||||
return strip
|
||||
}
|
||||
|
||||
func (v *whitespaceVisitor) VisitMustache(mustache *ast.MustacheStatement) interface{} {
|
||||
return mustache.Strip
|
||||
}
|
||||
|
||||
func _inlineStandalone(strip *ast.Strip) interface{} {
|
||||
return &ast.Strip{
|
||||
Open: strip.Open,
|
||||
Close: strip.Close,
|
||||
InlineStandalone: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *whitespaceVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
|
||||
strip := node.Strip
|
||||
if strip == nil {
|
||||
strip = &ast.Strip{}
|
||||
}
|
||||
|
||||
return _inlineStandalone(strip)
|
||||
}
|
||||
|
||||
func (v *whitespaceVisitor) VisitComment(node *ast.CommentStatement) interface{} {
|
||||
strip := node.Strip
|
||||
if strip == nil {
|
||||
strip = &ast.Strip{}
|
||||
}
|
||||
|
||||
return _inlineStandalone(strip)
|
||||
}
|
||||
|
||||
// NOOP
|
||||
func (v *whitespaceVisitor) VisitContent(node *ast.ContentStatement) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitExpression(node *ast.Expression) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitSubExpression(node *ast.SubExpression) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitPath(node *ast.PathExpression) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitString(node *ast.StringLiteral) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitNumber(node *ast.NumberLiteral) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitHash(node *ast.Hash) interface{} { return nil }
|
||||
func (v *whitespaceVisitor) VisitHashPair(node *ast.HashPair) interface{} { return nil }
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// partial represents a partial template
|
||||
type partial struct {
|
||||
name string
|
||||
source string
|
||||
tpl *Template
|
||||
}
|
||||
|
||||
// partials stores all global partials
|
||||
var partials map[string]*partial
|
||||
|
||||
// protects global partials
|
||||
var partialsMutex sync.RWMutex
|
||||
|
||||
func init() {
|
||||
partials = make(map[string]*partial)
|
||||
}
|
||||
|
||||
// newPartial instanciates a new partial
|
||||
func newPartial(name string, source string, tpl *Template) *partial {
|
||||
return &partial{
|
||||
name: name,
|
||||
source: source,
|
||||
tpl: tpl,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPartial registers a global partial. That partial will be available to all templates.
|
||||
func RegisterPartial(name string, source string) {
|
||||
partialsMutex.Lock()
|
||||
defer partialsMutex.Unlock()
|
||||
|
||||
if partials[name] != nil {
|
||||
panic(fmt.Errorf("Partial already registered: %s", name))
|
||||
}
|
||||
|
||||
partials[name] = newPartial(name, source, nil)
|
||||
}
|
||||
|
||||
// RegisterPartials registers several global partials. Those partials will be available to all templates.
|
||||
func RegisterPartials(partials map[string]string) {
|
||||
for name, p := range partials {
|
||||
RegisterPartial(name, p)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPartialTemplate registers a global partial with given parsed template. That partial will be available to all templates.
|
||||
func RegisterPartialTemplate(name string, tpl *Template) {
|
||||
partialsMutex.Lock()
|
||||
defer partialsMutex.Unlock()
|
||||
|
||||
if partials[name] != nil {
|
||||
panic(fmt.Errorf("Partial already registered: %s", name))
|
||||
}
|
||||
|
||||
partials[name] = newPartial(name, "", tpl)
|
||||
}
|
||||
|
||||
// findPartial finds a registered global partial
|
||||
func findPartial(name string) *partial {
|
||||
partialsMutex.RLock()
|
||||
defer partialsMutex.RUnlock()
|
||||
|
||||
return partials[name]
|
||||
}
|
||||
|
||||
// template returns parsed partial template
|
||||
func (p *partial) template() (*Template, error) {
|
||||
if p.tpl == nil {
|
||||
var err error
|
||||
|
||||
p.tpl, err = Parse(p.source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return p.tpl, nil
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Package raymond provides handlebars evaluation
|
||||
package raymond
|
||||
|
||||
// Render parses a template and evaluates it with given context
|
||||
//
|
||||
// Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead.
|
||||
func Render(source string, ctx interface{}) (string, error) {
|
||||
// parse template
|
||||
tpl, err := Parse(source)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// renders template
|
||||
str, err := tpl.Exec(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// MustRender parses a template and evaluates it with given context. It panics on error.
|
||||
//
|
||||
// Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead.
|
||||
func MustRender(source string, ctx interface{}) string {
|
||||
return MustParse(source).MustExec(ctx)
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
+115
@@ -0,0 +1,115 @@
|
||||
package raymond
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Example() {
|
||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// evaluate template with context
|
||||
output := tpl.MustExec(ctx)
|
||||
|
||||
// alternatively, for one shots:
|
||||
// output := MustRender(source, ctx)
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
|
||||
func Example_struct() {
|
||||
source := `<div class="post">
|
||||
<h1>By {{fullName author}}</h1>
|
||||
<div class="body">{{body}}</div>
|
||||
|
||||
<h1>Comments</h1>
|
||||
|
||||
{{#each comments}}
|
||||
<h2>By {{fullName author}}</h2>
|
||||
<div class="body">{{body}}</div>
|
||||
{{/each}}
|
||||
</div>`
|
||||
|
||||
type Person struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
Author Person
|
||||
Body string
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
Author Person
|
||||
Body string
|
||||
Comments []Comment
|
||||
}
|
||||
|
||||
ctx := Post{
|
||||
Person{"Jean", "Valjean"},
|
||||
"Life is difficult",
|
||||
[]Comment{
|
||||
Comment{
|
||||
Person{"Marcel", "Beliveau"},
|
||||
"LOL!",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
RegisterHelper("fullName", func(person Person) string {
|
||||
return person.FirstName + " " + person.LastName
|
||||
})
|
||||
|
||||
output := MustRender(source, ctx)
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <div class="post">
|
||||
// <h1>By Jean Valjean</h1>
|
||||
// <div class="body">Life is difficult</div>
|
||||
//
|
||||
// <h1>Comments</h1>
|
||||
//
|
||||
// <h2>By Marcel Beliveau</h2>
|
||||
// <div class="body">LOL!</div>
|
||||
// </div>
|
||||
}
|
||||
|
||||
func ExampleRender() {
|
||||
tpl := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// render template with context
|
||||
output, err := Render(tpl, ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
|
||||
func ExampleMustRender() {
|
||||
tpl := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// render template with context
|
||||
output := MustRender(tpl, ctx)
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SafeString represents a string that must not be escaped.
|
||||
//
|
||||
// A SafeString can be returned by helpers to disable escaping.
|
||||
type SafeString string
|
||||
|
||||
// isSafeString returns true if argument is a SafeString
|
||||
func isSafeString(value interface{}) bool {
|
||||
if _, ok := value.(SafeString); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Str returns string representation of any basic type value.
|
||||
func Str(value interface{}) string {
|
||||
return strValue(reflect.ValueOf(value))
|
||||
}
|
||||
|
||||
// strValue returns string representation of a reflect.Value
|
||||
func strValue(value reflect.Value) string {
|
||||
result := ""
|
||||
|
||||
ival, ok := printableValue(value)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("Can't print value: %q", value))
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(ival)
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
result += strValue(val.Index(i))
|
||||
}
|
||||
case reflect.Bool:
|
||||
result = "false"
|
||||
if val.Bool() {
|
||||
result = "true"
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
result = fmt.Sprintf("%d", ival)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
result = strconv.FormatFloat(val.Float(), 'f', -1, 64)
|
||||
case reflect.Invalid:
|
||||
result = ""
|
||||
default:
|
||||
result = fmt.Sprintf("%s", ival)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// printableValue returns the, possibly indirected, interface value inside v that
|
||||
// is best for a call to formatted printer.
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
func printableValue(v reflect.Value) (interface{}, bool) {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v, _ = indirect(v) // fmt.Fprint handles nil.
|
||||
}
|
||||
if !v.IsValid() {
|
||||
return "", true
|
||||
}
|
||||
|
||||
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
|
||||
if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) {
|
||||
v = v.Addr()
|
||||
} else {
|
||||
switch v.Kind() {
|
||||
case reflect.Chan, reflect.Func:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return v.Interface(), true
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type strTest struct {
|
||||
name string
|
||||
input interface{}
|
||||
output string
|
||||
}
|
||||
|
||||
var strTests = []strTest{
|
||||
{"String", "foo", "foo"},
|
||||
{"Boolean true", true, "true"},
|
||||
{"Boolean false", false, "false"},
|
||||
{"Integer", 25, "25"},
|
||||
{"Float", 25.75, "25.75"},
|
||||
{"Nil", nil, ""},
|
||||
{"[]string", []string{"foo", "bar"}, "foobar"},
|
||||
{"[]interface{} (strings)", []interface{}{"foo", "bar"}, "foobar"},
|
||||
{"[]Boolean", []bool{true, false}, "truefalse"},
|
||||
}
|
||||
|
||||
func TestStr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range strTests {
|
||||
if res := Str(test.input); res != test.output {
|
||||
t.Errorf("Failed to stringify: %s\nexpected:\n\t'%s'got:\n\t%q", test.name, test.output, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleStr() {
|
||||
output := Str(3) + " foos are " + Str(true) + " and " + Str(-1.25) + " bars are " + Str(false) + "\n"
|
||||
output += "But you know '" + Str(nil) + "' John Snow\n"
|
||||
output += "map: " + Str(map[string]string{"foo": "bar"}) + "\n"
|
||||
output += "array: " + Str([]interface{}{true, 10, "foo", 5, "bar"})
|
||||
|
||||
fmt.Println(output)
|
||||
// Output: 3 foos are true and -1.25 bars are false
|
||||
// But you know '' John Snow
|
||||
// map: map[foo:bar]
|
||||
// array: true10foo5bar
|
||||
}
|
||||
|
||||
func ExampleSafeString() {
|
||||
RegisterHelper("em", func() SafeString {
|
||||
return SafeString("<em>FOO BAR</em>")
|
||||
})
|
||||
|
||||
tpl := MustParse("{{em}}")
|
||||
|
||||
result := tpl.MustExec(nil)
|
||||
fmt.Print(result)
|
||||
// Output: <em>FOO BAR</em>
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/aymerick/raymond/ast"
|
||||
"github.com/aymerick/raymond/parser"
|
||||
)
|
||||
|
||||
// Template represents a handlebars template.
|
||||
type Template struct {
|
||||
source string
|
||||
program *ast.Program
|
||||
helpers map[string]reflect.Value
|
||||
partials map[string]*partial
|
||||
mutex sync.RWMutex // protects helpers and partials
|
||||
}
|
||||
|
||||
// newTemplate instanciate a new template without parsing it
|
||||
func newTemplate(source string) *Template {
|
||||
return &Template{
|
||||
source: source,
|
||||
helpers: make(map[string]reflect.Value),
|
||||
partials: make(map[string]*partial),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse instanciates a template by parsing given source.
|
||||
func Parse(source string) (*Template, error) {
|
||||
tpl := newTemplate(source)
|
||||
|
||||
// parse template
|
||||
if err := tpl.parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tpl, nil
|
||||
}
|
||||
|
||||
// MustParse instanciates a template by parsing given source. It panics on error.
|
||||
func MustParse(source string) *Template {
|
||||
result, err := Parse(source)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseFile reads given file and returns parsed template.
|
||||
func ParseFile(filePath string) (*Template, error) {
|
||||
b, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Parse(string(b))
|
||||
}
|
||||
|
||||
// parse parses the template
|
||||
//
|
||||
// It can be called several times, the parsing will be done only once.
|
||||
func (tpl *Template) parse() error {
|
||||
if tpl.program == nil {
|
||||
var err error
|
||||
|
||||
tpl.program, err = parser.Parse(tpl.source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone returns a copy of that template.
|
||||
func (tpl *Template) Clone() *Template {
|
||||
result := newTemplate(tpl.source)
|
||||
|
||||
result.program = tpl.program
|
||||
|
||||
tpl.mutex.RLock()
|
||||
defer tpl.mutex.RUnlock()
|
||||
|
||||
for name, helper := range tpl.helpers {
|
||||
result.RegisterHelper(name, helper.Interface())
|
||||
}
|
||||
|
||||
for name, partial := range tpl.partials {
|
||||
result.addPartial(name, partial.source, partial.tpl)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (tpl *Template) findHelper(name string) reflect.Value {
|
||||
tpl.mutex.RLock()
|
||||
defer tpl.mutex.RUnlock()
|
||||
|
||||
return tpl.helpers[name]
|
||||
}
|
||||
|
||||
// RegisterHelper registers a helper for that template.
|
||||
func (tpl *Template) RegisterHelper(name string, helper interface{}) {
|
||||
tpl.mutex.Lock()
|
||||
defer tpl.mutex.Unlock()
|
||||
|
||||
if tpl.helpers[name] != zero {
|
||||
panic(fmt.Sprintf("Helper %s already registered", name))
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(helper)
|
||||
ensureValidHelper(name, val)
|
||||
|
||||
tpl.helpers[name] = val
|
||||
}
|
||||
|
||||
// RegisterHelpers registers several helpers for that template.
|
||||
func (tpl *Template) RegisterHelpers(helpers map[string]interface{}) {
|
||||
for name, helper := range helpers {
|
||||
tpl.RegisterHelper(name, helper)
|
||||
}
|
||||
}
|
||||
|
||||
func (tpl *Template) addPartial(name string, source string, template *Template) {
|
||||
tpl.mutex.Lock()
|
||||
defer tpl.mutex.Unlock()
|
||||
|
||||
if tpl.partials[name] != nil {
|
||||
panic(fmt.Sprintf("Partial %s already registered", name))
|
||||
}
|
||||
|
||||
tpl.partials[name] = newPartial(name, source, template)
|
||||
}
|
||||
|
||||
func (tpl *Template) findPartial(name string) *partial {
|
||||
tpl.mutex.RLock()
|
||||
defer tpl.mutex.RUnlock()
|
||||
|
||||
return tpl.partials[name]
|
||||
}
|
||||
|
||||
// RegisterPartial registers a partial for that template.
|
||||
func (tpl *Template) RegisterPartial(name string, source string) {
|
||||
tpl.addPartial(name, source, nil)
|
||||
}
|
||||
|
||||
// RegisterPartials registers several partials for that template.
|
||||
func (tpl *Template) RegisterPartials(partials map[string]string) {
|
||||
for name, partial := range partials {
|
||||
tpl.RegisterPartial(name, partial)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPartialFile reads given file and registers its content as a partial with given name.
|
||||
func (tpl *Template) RegisterPartialFile(filePath string, name string) error {
|
||||
b, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpl.RegisterPartial(name, string(b))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterPartialFiles reads several files and registers them as partials, the filename base is used as the partial name.
|
||||
func (tpl *Template) RegisterPartialFiles(filePaths ...string) error {
|
||||
if len(filePaths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
name := fileBase(filePath)
|
||||
|
||||
if err := tpl.RegisterPartialFile(filePath, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterPartialTemplate registers an already parsed partial for that template.
|
||||
func (tpl *Template) RegisterPartialTemplate(name string, template *Template) {
|
||||
tpl.addPartial(name, "", template)
|
||||
}
|
||||
|
||||
// Exec evaluates template with given context.
|
||||
func (tpl *Template) Exec(ctx interface{}) (result string, err error) {
|
||||
return tpl.ExecWith(ctx, nil)
|
||||
}
|
||||
|
||||
// MustExec evaluates template with given context. It panics on error.
|
||||
func (tpl *Template) MustExec(ctx interface{}) string {
|
||||
result, err := tpl.Exec(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ExecWith evaluates template with given context and private data frame.
|
||||
func (tpl *Template) ExecWith(ctx interface{}, privData *DataFrame) (result string, err error) {
|
||||
defer errRecover(&err)
|
||||
|
||||
// parses template if necessary
|
||||
err = tpl.parse()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// setup visitor
|
||||
v := newEvalVisitor(tpl, ctx, privData)
|
||||
|
||||
// visit AST
|
||||
result, _ = tpl.program.Accept(v).(string)
|
||||
|
||||
// named return values
|
||||
return
|
||||
}
|
||||
|
||||
// errRecover recovers evaluation panic
|
||||
func errRecover(errp *error) {
|
||||
e := recover()
|
||||
if e != nil {
|
||||
switch err := e.(type) {
|
||||
case runtime.Error:
|
||||
panic(e)
|
||||
case error:
|
||||
*errp = err
|
||||
default:
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintAST returns string representation of parsed template.
|
||||
func (tpl *Template) PrintAST() string {
|
||||
if err := tpl.parse(); err != nil {
|
||||
return fmt.Sprintf("PARSER ERROR: %s", err)
|
||||
}
|
||||
|
||||
return ast.Print(tpl.program)
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var sourceBasic = `<div class="entry">
|
||||
<h1>{{title}}</h1>
|
||||
<div class="body">
|
||||
{{body}}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
var basicAST = `CONTENT[ '<div class="entry">
|
||||
<h1>' ]
|
||||
{{ PATH:title [] }}
|
||||
CONTENT[ '</h1>
|
||||
<div class="body">
|
||||
' ]
|
||||
{{ PATH:body [] }}
|
||||
CONTENT[ '
|
||||
</div>
|
||||
</div>' ]
|
||||
`
|
||||
|
||||
func TestNewTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tpl := newTemplate(sourceBasic)
|
||||
if tpl.source != sourceBasic {
|
||||
t.Errorf("Failed to instantiate template")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tpl, err := Parse(sourceBasic)
|
||||
if err != nil || (tpl.source != sourceBasic) {
|
||||
t.Errorf("Failed to parse template")
|
||||
}
|
||||
|
||||
if str := tpl.PrintAST(); str != basicAST {
|
||||
t.Errorf("Template parsing incorrect: %s", str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sourcePartial := `I am a {{wat}} partial`
|
||||
sourcePartial2 := `Partial for the {{wat}}`
|
||||
|
||||
tpl := MustParse(sourceBasic)
|
||||
tpl.RegisterPartial("p", sourcePartial)
|
||||
|
||||
if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) {
|
||||
t.Errorf("What?")
|
||||
}
|
||||
|
||||
cloned := tpl.Clone()
|
||||
|
||||
if (len(cloned.partials) != 1) || (cloned.partials["p"] == nil) {
|
||||
t.Errorf("Template partials must be cloned")
|
||||
}
|
||||
|
||||
cloned.RegisterPartial("p2", sourcePartial2)
|
||||
|
||||
if (len(cloned.partials) != 2) || (cloned.partials["p"] == nil) || (cloned.partials["p2"] == nil) {
|
||||
t.Errorf("Failed to register a partial on cloned template")
|
||||
}
|
||||
|
||||
if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) {
|
||||
t.Errorf("Modification of a cloned template MUST NOT affect original template")
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleTemplate_Exec() {
|
||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// evaluate template with context
|
||||
output, err := tpl.Exec(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
|
||||
func ExampleTemplate_MustExec() {
|
||||
source := "<h1>{{title}}</h1><p>{{body.content}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// evaluate template with context
|
||||
output := tpl.MustExec(ctx)
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar</p>
|
||||
}
|
||||
|
||||
func ExampleTemplate_ExecWith() {
|
||||
source := "<h1>{{title}}</h1><p>{{#body}}{{content}} and {{@baz.bat}}{{/body}}</p>"
|
||||
|
||||
ctx := map[string]interface{}{
|
||||
"title": "foo",
|
||||
"body": map[string]string{"content": "bar"},
|
||||
}
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// computes private data frame
|
||||
frame := NewDataFrame()
|
||||
frame.Set("baz", map[string]string{"bat": "unicorns"})
|
||||
|
||||
// evaluate template
|
||||
output, err := tpl.ExecWith(ctx, frame)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: <h1>foo</h1><p>bar and unicorns</p>
|
||||
}
|
||||
|
||||
func ExampleTemplate_PrintAST() {
|
||||
source := "<h1>{{title}}</h1><p>{{#body}}{{content}} and {{@baz.bat}}{{/body}}</p>"
|
||||
|
||||
// parse template
|
||||
tpl := MustParse(source)
|
||||
|
||||
// print AST
|
||||
output := tpl.PrintAST()
|
||||
|
||||
fmt.Print(output)
|
||||
// Output: CONTENT[ '<h1>' ]
|
||||
// {{ PATH:title [] }}
|
||||
// CONTENT[ '</h1><p>' ]
|
||||
// BLOCK:
|
||||
// PATH:body []
|
||||
// PROGRAM:
|
||||
// {{ PATH:content []
|
||||
// }}
|
||||
// CONTENT[ ' and ' ]
|
||||
// {{ @PATH:baz/bat []
|
||||
// }}
|
||||
// CONTENT[ '</p>' ]
|
||||
//
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package raymond
|
||||
|
||||
import (
|
||||
"path"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// indirect returns the item at the end of indirection, and a bool to indicate if it's nil.
|
||||
// We indirect through pointers and empty interfaces (only) because
|
||||
// non-empty interfaces have methods we might need.
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
|
||||
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
|
||||
if v.IsNil() {
|
||||
return v, true
|
||||
}
|
||||
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
// IsTrue returns true if obj is a truthy value.
|
||||
func IsTrue(obj interface{}) bool {
|
||||
thruth, ok := isTrueValue(reflect.ValueOf(obj))
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return thruth
|
||||
}
|
||||
|
||||
// isTrueValue reports whether the value is 'true', in the sense of not the zero of its type,
|
||||
// and whether the value has a meaningful truth value
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
func isTrueValue(val reflect.Value) (truth, ok bool) {
|
||||
if !val.IsValid() {
|
||||
// Something like var x interface{}, never set. It's a form of nil.
|
||||
return false, true
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
truth = val.Len() > 0
|
||||
case reflect.Bool:
|
||||
truth = val.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
truth = val.Complex() != 0
|
||||
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
|
||||
truth = !val.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
truth = val.Int() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
truth = val.Float() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
truth = val.Uint() != 0
|
||||
case reflect.Struct:
|
||||
truth = true // Struct values are always true.
|
||||
default:
|
||||
return
|
||||
}
|
||||
return truth, true
|
||||
}
|
||||
|
||||
// canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero.
|
||||
//
|
||||
// NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go
|
||||
func canBeNil(typ reflect.Type) bool {
|
||||
switch typ.Kind() {
|
||||
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// fileBase returns base file name
|
||||
//
|
||||
// example: /foo/bar/baz.png => baz
|
||||
func fileBase(filePath string) string {
|
||||
fileName := path.Base(filePath)
|
||||
fileExt := path.Ext(filePath)
|
||||
|
||||
return fileName[:len(fileName)-len(fileExt)]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package raymond
|
||||
|
||||
import "fmt"
|
||||
|
||||
func ExampleIsTrue() {
|
||||
output := "Empty array: " + Str(IsTrue([0]string{})) + "\n"
|
||||
output += "Non empty array: " + Str(IsTrue([1]string{"foo"})) + "\n"
|
||||
|
||||
output += "Empty slice: " + Str(IsTrue([]string{})) + "\n"
|
||||
output += "Non empty slice: " + Str(IsTrue([]string{"foo"})) + "\n"
|
||||
|
||||
output += "Empty map: " + Str(IsTrue(map[string]string{})) + "\n"
|
||||
output += "Non empty map: " + Str(IsTrue(map[string]string{"foo": "bar"})) + "\n"
|
||||
|
||||
output += "Empty string: " + Str(IsTrue("")) + "\n"
|
||||
output += "Non empty string: " + Str(IsTrue("foo")) + "\n"
|
||||
|
||||
output += "true bool: " + Str(IsTrue(true)) + "\n"
|
||||
output += "false bool: " + Str(IsTrue(false)) + "\n"
|
||||
|
||||
output += "0 integer: " + Str(IsTrue(0)) + "\n"
|
||||
output += "positive integer: " + Str(IsTrue(10)) + "\n"
|
||||
output += "negative integer: " + Str(IsTrue(-10)) + "\n"
|
||||
|
||||
output += "0 float: " + Str(IsTrue(0.0)) + "\n"
|
||||
output += "positive float: " + Str(IsTrue(10.0)) + "\n"
|
||||
output += "negative integer: " + Str(IsTrue(-10.0)) + "\n"
|
||||
|
||||
output += "struct: " + Str(IsTrue(struct{}{})) + "\n"
|
||||
output += "nil: " + Str(IsTrue(nil)) + "\n"
|
||||
|
||||
fmt.Println(output)
|
||||
// Output: Empty array: false
|
||||
// Non empty array: true
|
||||
// Empty slice: false
|
||||
// Non empty slice: true
|
||||
// Empty map: false
|
||||
// Non empty map: true
|
||||
// Empty string: false
|
||||
// Non empty string: true
|
||||
// true bool: true
|
||||
// false bool: false
|
||||
// 0 integer: false
|
||||
// positive integer: true
|
||||
// negative integer: true
|
||||
// 0 float: false
|
||||
// positive float: true
|
||||
// negative integer: true
|
||||
// struct: true
|
||||
// nil: false
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- tip
|
||||
script: cd semver && go test
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# go-semver - Semantic Versioning Library
|
||||
|
||||
[](https://travis-ci.org/coreos/go-semver)
|
||||
[](https://godoc.org/github.com/coreos/go-semver/semver)
|
||||
|
||||
go-semver is a [semantic versioning][semver] library for Go. It lets you parse
|
||||
and compare two semantic version strings.
|
||||
|
||||
[semver]: http://semver.org/
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
vA := semver.New("1.2.3")
|
||||
vB := semver.New("3.2.1")
|
||||
|
||||
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
|
||||
```
|
||||
|
||||
## Example Application
|
||||
|
||||
```
|
||||
$ go run example.go 1.2.3 3.2.1
|
||||
1.2.3 < 3.2.1 == true
|
||||
|
||||
$ go run example.go 5.2.3 3.2.1
|
||||
5.2.3 < 3.2.1 == false
|
||||
```
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
vA, err := semver.NewVersion(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
vB, err := semver.NewVersion(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
// Copyright 2013-2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Semantic Versions http://semver.org
|
||||
package semver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
Major int64
|
||||
Minor int64
|
||||
Patch int64
|
||||
PreRelease PreRelease
|
||||
Metadata string
|
||||
}
|
||||
|
||||
type PreRelease string
|
||||
|
||||
func splitOff(input *string, delim string) (val string) {
|
||||
parts := strings.SplitN(*input, delim, 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
*input = parts[0]
|
||||
val = parts[1]
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func New(version string) *Version {
|
||||
return Must(NewVersion(version))
|
||||
}
|
||||
|
||||
func NewVersion(version string) (*Version, error) {
|
||||
v := Version{}
|
||||
|
||||
if err := v.Set(version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
// Must is a helper for wrapping NewVersion and will panic if err is not nil.
|
||||
func Must(v *Version, err error) *Version {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Set parses and updates v from the given version string. Implements flag.Value
|
||||
func (v *Version) Set(version string) error {
|
||||
metadata := splitOff(&version, "+")
|
||||
preRelease := PreRelease(splitOff(&version, "-"))
|
||||
dotParts := strings.SplitN(version, ".", 3)
|
||||
|
||||
if len(dotParts) != 3 {
|
||||
return fmt.Errorf("%s is not in dotted-tri format", version)
|
||||
}
|
||||
|
||||
parsed := make([]int64, 3, 3)
|
||||
|
||||
for i, v := range dotParts[:3] {
|
||||
val, err := strconv.ParseInt(v, 10, 64)
|
||||
parsed[i] = val
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
v.Metadata = metadata
|
||||
v.PreRelease = preRelease
|
||||
v.Major = parsed[0]
|
||||
v.Minor = parsed[1]
|
||||
v.Patch = parsed[2]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
fmt.Fprintf(&buffer, "%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||
|
||||
if v.PreRelease != "" {
|
||||
fmt.Fprintf(&buffer, "-%s", v.PreRelease)
|
||||
}
|
||||
|
||||
if v.Metadata != "" {
|
||||
fmt.Fprintf(&buffer, "+%s", v.Metadata)
|
||||
}
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (v *Version) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var data string
|
||||
if err := unmarshal(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.Set(data)
|
||||
}
|
||||
|
||||
func (v Version) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + v.String() + `"`), nil
|
||||
}
|
||||
|
||||
func (v *Version) UnmarshalJSON(data []byte) error {
|
||||
l := len(data)
|
||||
if l == 0 || string(data) == `""` {
|
||||
return nil
|
||||
}
|
||||
if l < 2 || data[0] != '"' || data[l-1] != '"' {
|
||||
return errors.New("invalid semver string")
|
||||
}
|
||||
return v.Set(string(data[1 : l-1]))
|
||||
}
|
||||
|
||||
// Compare tests if v is less than, equal to, or greater than versionB,
|
||||
// returning -1, 0, or +1 respectively.
|
||||
func (v Version) Compare(versionB Version) int {
|
||||
if cmp := recursiveCompare(v.Slice(), versionB.Slice()); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return preReleaseCompare(v, versionB)
|
||||
}
|
||||
|
||||
// Equal tests if v is equal to versionB.
|
||||
func (v Version) Equal(versionB Version) bool {
|
||||
return v.Compare(versionB) == 0
|
||||
}
|
||||
|
||||
// LessThan tests if v is less than versionB.
|
||||
func (v Version) LessThan(versionB Version) bool {
|
||||
return v.Compare(versionB) < 0
|
||||
}
|
||||
|
||||
// Slice converts the comparable parts of the semver into a slice of integers.
|
||||
func (v Version) Slice() []int64 {
|
||||
return []int64{v.Major, v.Minor, v.Patch}
|
||||
}
|
||||
|
||||
func (p PreRelease) Slice() []string {
|
||||
preRelease := string(p)
|
||||
return strings.Split(preRelease, ".")
|
||||
}
|
||||
|
||||
func preReleaseCompare(versionA Version, versionB Version) int {
|
||||
a := versionA.PreRelease
|
||||
b := versionB.PreRelease
|
||||
|
||||
/* Handle the case where if two versions are otherwise equal it is the
|
||||
* one without a PreRelease that is greater */
|
||||
if len(a) == 0 && (len(b) > 0) {
|
||||
return 1
|
||||
} else if len(b) == 0 && (len(a) > 0) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// If there is a prerelease, check and compare each part.
|
||||
return recursivePreReleaseCompare(a.Slice(), b.Slice())
|
||||
}
|
||||
|
||||
func recursiveCompare(versionA []int64, versionB []int64) int {
|
||||
if len(versionA) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
a := versionA[0]
|
||||
b := versionB[0]
|
||||
|
||||
if a > b {
|
||||
return 1
|
||||
} else if a < b {
|
||||
return -1
|
||||
}
|
||||
|
||||
return recursiveCompare(versionA[1:], versionB[1:])
|
||||
}
|
||||
|
||||
func recursivePreReleaseCompare(versionA []string, versionB []string) int {
|
||||
// A larger set of pre-release fields has a higher precedence than a smaller set,
|
||||
// if all of the preceding identifiers are equal.
|
||||
if len(versionA) == 0 {
|
||||
if len(versionB) > 0 {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
} else if len(versionB) == 0 {
|
||||
// We're longer than versionB so return 1.
|
||||
return 1
|
||||
}
|
||||
|
||||
a := versionA[0]
|
||||
b := versionB[0]
|
||||
|
||||
aInt := false
|
||||
bInt := false
|
||||
|
||||
aI, err := strconv.Atoi(versionA[0])
|
||||
if err == nil {
|
||||
aInt = true
|
||||
}
|
||||
|
||||
bI, err := strconv.Atoi(versionB[0])
|
||||
if err == nil {
|
||||
bInt = true
|
||||
}
|
||||
|
||||
// Handle Integer Comparison
|
||||
if aInt && bInt {
|
||||
if aI > bI {
|
||||
return 1
|
||||
} else if aI < bI {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// Handle String Comparison
|
||||
if a > b {
|
||||
return 1
|
||||
} else if a < b {
|
||||
return -1
|
||||
}
|
||||
|
||||
return recursivePreReleaseCompare(versionA[1:], versionB[1:])
|
||||
}
|
||||
|
||||
// BumpMajor increments the Major field by 1 and resets all other fields to their default values
|
||||
func (v *Version) BumpMajor() {
|
||||
v.Major += 1
|
||||
v.Minor = 0
|
||||
v.Patch = 0
|
||||
v.PreRelease = PreRelease("")
|
||||
v.Metadata = ""
|
||||
}
|
||||
|
||||
// BumpMinor increments the Minor field by 1 and resets all other fields to their default values
|
||||
func (v *Version) BumpMinor() {
|
||||
v.Minor += 1
|
||||
v.Patch = 0
|
||||
v.PreRelease = PreRelease("")
|
||||
v.Metadata = ""
|
||||
}
|
||||
|
||||
// BumpPatch increments the Patch field by 1 and resets all other fields to their default values
|
||||
func (v *Version) BumpPatch() {
|
||||
v.Patch += 1
|
||||
v.PreRelease = PreRelease("")
|
||||
v.Metadata = ""
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// Copyright 2013-2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package semver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type fixture struct {
|
||||
GreaterVersion string
|
||||
LesserVersion string
|
||||
}
|
||||
|
||||
var fixtures = []fixture{
|
||||
fixture{"0.0.0", "0.0.0-foo"},
|
||||
fixture{"0.0.1", "0.0.0"},
|
||||
fixture{"1.0.0", "0.9.9"},
|
||||
fixture{"0.10.0", "0.9.0"},
|
||||
fixture{"0.99.0", "0.10.0"},
|
||||
fixture{"2.0.0", "1.2.3"},
|
||||
fixture{"0.0.0", "0.0.0-foo"},
|
||||
fixture{"0.0.1", "0.0.0"},
|
||||
fixture{"1.0.0", "0.9.9"},
|
||||
fixture{"0.10.0", "0.9.0"},
|
||||
fixture{"0.99.0", "0.10.0"},
|
||||
fixture{"2.0.0", "1.2.3"},
|
||||
fixture{"0.0.0", "0.0.0-foo"},
|
||||
fixture{"0.0.1", "0.0.0"},
|
||||
fixture{"1.0.0", "0.9.9"},
|
||||
fixture{"0.10.0", "0.9.0"},
|
||||
fixture{"0.99.0", "0.10.0"},
|
||||
fixture{"2.0.0", "1.2.3"},
|
||||
fixture{"1.2.3", "1.2.3-asdf"},
|
||||
fixture{"1.2.3", "1.2.3-4"},
|
||||
fixture{"1.2.3", "1.2.3-4-foo"},
|
||||
fixture{"1.2.3-5-foo", "1.2.3-5"},
|
||||
fixture{"1.2.3-5", "1.2.3-4"},
|
||||
fixture{"1.2.3-5-foo", "1.2.3-5-Foo"},
|
||||
fixture{"3.0.0", "2.7.2+asdf"},
|
||||
fixture{"3.0.0+foobar", "2.7.2"},
|
||||
fixture{"1.2.3-a.10", "1.2.3-a.5"},
|
||||
fixture{"1.2.3-a.b", "1.2.3-a.5"},
|
||||
fixture{"1.2.3-a.b", "1.2.3-a"},
|
||||
fixture{"1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"},
|
||||
fixture{"1.0.0", "1.0.0-rc.1"},
|
||||
fixture{"1.0.0-rc.2", "1.0.0-rc.1"},
|
||||
fixture{"1.0.0-rc.1", "1.0.0-beta.11"},
|
||||
fixture{"1.0.0-beta.11", "1.0.0-beta.2"},
|
||||
fixture{"1.0.0-beta.2", "1.0.0-beta"},
|
||||
fixture{"1.0.0-beta", "1.0.0-alpha.beta"},
|
||||
fixture{"1.0.0-alpha.beta", "1.0.0-alpha.1"},
|
||||
fixture{"1.0.0-alpha.1", "1.0.0-alpha"},
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for _, v := range fixtures {
|
||||
gt, err := NewVersion(v.GreaterVersion)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
lt, err := NewVersion(v.LesserVersion)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if gt.LessThan(*lt) {
|
||||
t.Errorf("%s should not be less than %s", gt, lt)
|
||||
}
|
||||
if gt.Equal(*lt) {
|
||||
t.Errorf("%s should not be equal to %s", gt, lt)
|
||||
}
|
||||
if gt.Compare(*lt) <= 0 {
|
||||
t.Errorf("%s should be greater than %s", gt, lt)
|
||||
}
|
||||
if !lt.LessThan(*gt) {
|
||||
t.Errorf("%s should be less than %s", lt, gt)
|
||||
}
|
||||
if !lt.Equal(*lt) {
|
||||
t.Errorf("%s should be equal to %s", lt, lt)
|
||||
}
|
||||
if lt.Compare(*gt) > 0 {
|
||||
t.Errorf("%s should not be greater than %s", lt, gt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testString(t *testing.T, orig string, version *Version) {
|
||||
if orig != version.String() {
|
||||
t.Errorf("%s != %s", orig, version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
for _, v := range fixtures {
|
||||
gt, err := NewVersion(v.GreaterVersion)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
testString(t, v.GreaterVersion, gt)
|
||||
|
||||
lt, err := NewVersion(v.LesserVersion)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
testString(t, v.LesserVersion, lt)
|
||||
}
|
||||
}
|
||||
|
||||
func shuffleStringSlice(src []string) []string {
|
||||
dest := make([]string, len(src))
|
||||
rand.Seed(time.Now().Unix())
|
||||
perm := rand.Perm(len(src))
|
||||
for i, v := range perm {
|
||||
dest[v] = src[i]
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
sortedVersions := []string{"1.0.0", "1.0.2", "1.2.0", "3.1.1"}
|
||||
unsortedVersions := shuffleStringSlice(sortedVersions)
|
||||
|
||||
semvers := []*Version{}
|
||||
for _, v := range unsortedVersions {
|
||||
sv, err := NewVersion(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
semvers = append(semvers, sv)
|
||||
}
|
||||
|
||||
Sort(semvers)
|
||||
|
||||
for idx, sv := range semvers {
|
||||
if sv.String() != sortedVersions[idx] {
|
||||
t.Fatalf("incorrect sort at index %v", idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpMajor(t *testing.T) {
|
||||
version, _ := NewVersion("1.0.0")
|
||||
version.BumpMajor()
|
||||
if version.Major != 2 {
|
||||
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
version, _ = NewVersion("1.5.2")
|
||||
version.BumpMajor()
|
||||
if version.Minor != 0 && version.Patch != 0 {
|
||||
t.Fatalf("bumping major on 1.5.2 resulted in %v", version)
|
||||
}
|
||||
|
||||
version, _ = NewVersion("1.0.0+build.1-alpha.1")
|
||||
version.BumpMajor()
|
||||
if version.PreRelease != "" && version.PreRelease != "" {
|
||||
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpMinor(t *testing.T) {
|
||||
version, _ := NewVersion("1.0.0")
|
||||
version.BumpMinor()
|
||||
|
||||
if version.Major != 1 {
|
||||
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
if version.Minor != 1 {
|
||||
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
version, _ = NewVersion("1.0.0+build.1-alpha.1")
|
||||
version.BumpMinor()
|
||||
if version.PreRelease != "" && version.PreRelease != "" {
|
||||
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBumpPatch(t *testing.T) {
|
||||
version, _ := NewVersion("1.0.0")
|
||||
version.BumpPatch()
|
||||
|
||||
if version.Major != 1 {
|
||||
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
if version.Minor != 0 {
|
||||
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
if version.Patch != 1 {
|
||||
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
|
||||
}
|
||||
|
||||
version, _ = NewVersion("1.0.0+build.1-alpha.1")
|
||||
version.BumpPatch()
|
||||
if version.PreRelease != "" && version.PreRelease != "" {
|
||||
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMust(t *testing.T) {
|
||||
tests := []struct {
|
||||
versionStr string
|
||||
|
||||
version *Version
|
||||
recov interface{}
|
||||
}{
|
||||
{
|
||||
versionStr: "1.0.0",
|
||||
version: &Version{Major: 1},
|
||||
},
|
||||
{
|
||||
versionStr: "version number",
|
||||
recov: errors.New("version number is not in dotted-tri format"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
func() {
|
||||
defer func() {
|
||||
recov := recover()
|
||||
if !reflect.DeepEqual(tt.recov, recov) {
|
||||
t.Fatalf("incorrect panic for %q: want %v, got %v", tt.versionStr, tt.recov, recov)
|
||||
}
|
||||
}()
|
||||
|
||||
version := Must(NewVersion(tt.versionStr))
|
||||
if !reflect.DeepEqual(tt.version, version) {
|
||||
t.Fatalf("incorrect version for %q: want %+v, got %+v", tt.versionStr, tt.version, version)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
type fixtureJSON struct {
|
||||
GreaterVersion *Version
|
||||
LesserVersion *Version
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
fj := make([]fixtureJSON, len(fixtures))
|
||||
for i, v := range fixtures {
|
||||
var err error
|
||||
fj[i].GreaterVersion, err = NewVersion(v.GreaterVersion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fj[i].LesserVersion, err = NewVersion(v.LesserVersion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fromStrings, err := json.Marshal(fixtures)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fromVersions, err := json.Marshal(fj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(fromStrings, fromVersions) {
|
||||
t.Errorf("Expected: %s", fromStrings)
|
||||
t.Errorf("Unexpected: %s", fromVersions)
|
||||
}
|
||||
|
||||
fromJson := make([]fixtureJSON, 0, len(fj))
|
||||
err = json.Unmarshal(fromStrings, &fromJson)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(fromJson, fj) {
|
||||
t.Error("Expected: ", fj)
|
||||
t.Error("Unexpected: ", fromJson)
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAML(t *testing.T) {
|
||||
document, err := yaml.Marshal(fixtures)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := make([]fixtureJSON, len(fixtures))
|
||||
for i, v := range fixtures {
|
||||
var err error
|
||||
expected[i].GreaterVersion, err = NewVersion(v.GreaterVersion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected[i].LesserVersion, err = NewVersion(v.LesserVersion)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fromYAML := make([]fixtureJSON, 0, len(fixtures))
|
||||
err = yaml.Unmarshal(document, &fromYAML)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(fromYAML, expected) {
|
||||
t.Error("Expected: ", expected)
|
||||
t.Error("Unexpected: ", fromYAML)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadInput(t *testing.T) {
|
||||
bad := []string{
|
||||
"1.2",
|
||||
"1.2.3x",
|
||||
"0x1.3.4",
|
||||
"-1.2.3",
|
||||
"1.2.3.4",
|
||||
}
|
||||
for _, b := range bad {
|
||||
if _, err := NewVersion(b); err == nil {
|
||||
t.Error("Improperly accepted value: ", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlag(t *testing.T) {
|
||||
v := Version{}
|
||||
f := flag.NewFlagSet("version", flag.ContinueOnError)
|
||||
f.Var(&v, "version", "set version")
|
||||
|
||||
if err := f.Set("version", "1.2.3"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if v.String() != "1.2.3" {
|
||||
t.Errorf("Set wrong value %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleVersion_LessThan() {
|
||||
vA := New("1.2.3")
|
||||
vB := New("3.2.1")
|
||||
|
||||
fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB))
|
||||
// Output:
|
||||
// 1.2.3 < 3.2.1 == true
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2013-2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package semver
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Versions []*Version
|
||||
|
||||
func (s Versions) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s Versions) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s Versions) Less(i, j int) bool {
|
||||
return s[i].LessThan(*s[j])
|
||||
}
|
||||
|
||||
// Sort sorts the given slice of Version
|
||||
func Sort(versions []*Version) {
|
||||
sort.Sort(Versions(versions))
|
||||
}
|
||||
Generated
+24
@@ -0,0 +1,24 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
language: go
|
||||
go_import_path: github.com/pkg/errors
|
||||
go:
|
||||
- 1.4.3
|
||||
- 1.5.4
|
||||
- 1.6.2
|
||||
- 1.7.1
|
||||
- tip
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2015, Dave Cheney <dave@cheney.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# errors [](https://travis-ci.org/pkg/errors) [](https://ci.appveyor.com/project/davecheney/errors/branch/master) [](http://godoc.org/github.com/pkg/errors) [](https://goreportcard.com/report/github.com/pkg/errors)
|
||||
|
||||
Package errors provides simple error handling primitives.
|
||||
|
||||
`go get github.com/pkg/errors`
|
||||
|
||||
The traditional error handling idiom in Go is roughly akin to
|
||||
```go
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
|
||||
|
||||
## Adding context to an error
|
||||
|
||||
The errors.Wrap function returns a new error that adds context to the original error. For example
|
||||
```go
|
||||
_, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read failed")
|
||||
}
|
||||
```
|
||||
## Retrieving the cause of an error
|
||||
|
||||
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
|
||||
```go
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
```
|
||||
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
|
||||
```go
|
||||
switch err := errors.Cause(err).(type) {
|
||||
case *MyError:
|
||||
// handle specifically
|
||||
default:
|
||||
// unknown error
|
||||
}
|
||||
```
|
||||
|
||||
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
|
||||
|
||||
Before proposing a change, please discuss your change by raising an issue.
|
||||
|
||||
## Licence
|
||||
|
||||
BSD-2-Clause
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
version: build-{build}.{branch}
|
||||
|
||||
clone_folder: C:\gopath\src\github.com\pkg\errors
|
||||
shallow_clone: true # for startup speed
|
||||
|
||||
environment:
|
||||
GOPATH: C:\gopath
|
||||
|
||||
platform:
|
||||
- x64
|
||||
|
||||
# http://www.appveyor.com/docs/installed-software
|
||||
install:
|
||||
# some helpful output for debugging builds
|
||||
- go version
|
||||
- go env
|
||||
# pre-installed MinGW at C:\MinGW is 32bit only
|
||||
# but MSYS2 at C:\msys64 has mingw64
|
||||
- set PATH=C:\msys64\mingw64\bin;%PATH%
|
||||
- gcc --version
|
||||
- g++ --version
|
||||
|
||||
build_script:
|
||||
- go install -v ./...
|
||||
|
||||
test_script:
|
||||
- set PATH=C:\gopath\bin;%PATH%
|
||||
- go test -v ./...
|
||||
|
||||
#artifacts:
|
||||
# - path: '%GOPATH%\bin\*.exe'
|
||||
deploy: off
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// +build go1.7
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
stderrors "errors"
|
||||
)
|
||||
|
||||
func noErrors(at, depth int) error {
|
||||
if at >= depth {
|
||||
return stderrors.New("no error")
|
||||
}
|
||||
return noErrors(at+1, depth)
|
||||
}
|
||||
func yesErrors(at, depth int) error {
|
||||
if at >= depth {
|
||||
return New("ye error")
|
||||
}
|
||||
return yesErrors(at+1, depth)
|
||||
}
|
||||
|
||||
func BenchmarkErrors(b *testing.B) {
|
||||
var toperr error
|
||||
type run struct {
|
||||
stack int
|
||||
std bool
|
||||
}
|
||||
runs := []run{
|
||||
{10, false},
|
||||
{10, true},
|
||||
{100, false},
|
||||
{100, true},
|
||||
{1000, false},
|
||||
{1000, true},
|
||||
}
|
||||
for _, r := range runs {
|
||||
part := "pkg/errors"
|
||||
if r.std {
|
||||
part = "errors"
|
||||
}
|
||||
name := fmt.Sprintf("%s-stack-%d", part, r.stack)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
var err error
|
||||
f := yesErrors
|
||||
if r.std {
|
||||
f = noErrors
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = f(0, r.stack)
|
||||
}
|
||||
b.StopTimer()
|
||||
toperr = err
|
||||
})
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
// Package errors provides simple error handling primitives.
|
||||
//
|
||||
// The traditional error handling idiom in Go is roughly akin to
|
||||
//
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// which applied recursively up the call stack results in error reports
|
||||
// without context or debugging information. The errors package allows
|
||||
// programmers to add context to the failure path in their code in a way
|
||||
// that does not destroy the original value of the error.
|
||||
//
|
||||
// Adding context to an error
|
||||
//
|
||||
// The errors.Wrap function returns a new error that adds context to the
|
||||
// original error by recording a stack trace at the point Wrap is called,
|
||||
// and the supplied message. For example
|
||||
//
|
||||
// _, err := ioutil.ReadAll(r)
|
||||
// if err != nil {
|
||||
// return errors.Wrap(err, "read failed")
|
||||
// }
|
||||
//
|
||||
// If additional control is required the errors.WithStack and errors.WithMessage
|
||||
// functions destructure errors.Wrap into its component operations of annotating
|
||||
// an error with a stack trace and an a message, respectively.
|
||||
//
|
||||
// Retrieving the cause of an error
|
||||
//
|
||||
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||
// preceding error. Depending on the nature of the error it may be necessary
|
||||
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||
// for inspection. Any error value which implements this interface
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||
// the topmost error which does not implement causer, which is assumed to be
|
||||
// the original cause. For example:
|
||||
//
|
||||
// switch err := errors.Cause(err).(type) {
|
||||
// case *MyError:
|
||||
// // handle specifically
|
||||
// default:
|
||||
// // unknown error
|
||||
// }
|
||||
//
|
||||
// causer interface is not exported by this package, but is considered a part
|
||||
// of stable public API.
|
||||
//
|
||||
// Formatted printing of errors
|
||||
//
|
||||
// All error values returned from this package implement fmt.Formatter and can
|
||||
// be formatted by the fmt package. The following verbs are supported
|
||||
//
|
||||
// %s print the error. If the error has a Cause it will be
|
||||
// printed recursively
|
||||
// %v see %s
|
||||
// %+v extended format. Each Frame of the error's StackTrace will
|
||||
// be printed in detail.
|
||||
//
|
||||
// Retrieving the stack trace of an error or wrapper
|
||||
//
|
||||
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||
// invoked. This information can be retrieved with the following interface.
|
||||
//
|
||||
// type stackTracer interface {
|
||||
// StackTrace() errors.StackTrace
|
||||
// }
|
||||
//
|
||||
// Where errors.StackTrace is defined as
|
||||
//
|
||||
// type StackTrace []Frame
|
||||
//
|
||||
// The Frame type represents a call site in the stack trace. Frame supports
|
||||
// the fmt.Formatter interface that can be used for printing information about
|
||||
// the stack trace of this error. For example:
|
||||
//
|
||||
// if err, ok := err.(stackTracer); ok {
|
||||
// for _, f := range err.StackTrace() {
|
||||
// fmt.Printf("%+s:%d", f)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// stackTracer interface is not exported by this package, but is considered a part
|
||||
// of stable public API.
|
||||
//
|
||||
// See the documentation for Frame.Format for more details.
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// New returns an error with the supplied message.
|
||||
// New also records the stack trace at the point it was called.
|
||||
func New(message string) error {
|
||||
return &fundamental{
|
||||
msg: message,
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf formats according to a format specifier and returns the string
|
||||
// as a value that satisfies error.
|
||||
// Errorf also records the stack trace at the point it was called.
|
||||
func Errorf(format string, args ...interface{}) error {
|
||||
return &fundamental{
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
stack: callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// fundamental is an error that has a message and a stack, but no caller.
|
||||
type fundamental struct {
|
||||
msg string
|
||||
*stack
|
||||
}
|
||||
|
||||
func (f *fundamental) Error() string { return f.msg }
|
||||
|
||||
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
io.WriteString(s, f.msg)
|
||||
f.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, f.msg)
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", f.msg)
|
||||
}
|
||||
}
|
||||
|
||||
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||
// If err is nil, WithStack returns nil.
|
||||
func WithStack(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
type withStack struct {
|
||||
error
|
||||
*stack
|
||||
}
|
||||
|
||||
func (w *withStack) Cause() error { return w.error }
|
||||
|
||||
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v", w.Cause())
|
||||
w.stack.Format(s, verb)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's':
|
||||
io.WriteString(s, w.Error())
|
||||
case 'q':
|
||||
fmt.Fprintf(s, "%q", w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap returns an error annotating err with a stack trace
|
||||
// at the point Wrap is called, and the supplied message.
|
||||
// If err is nil, Wrap returns nil.
|
||||
func Wrap(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf returns an error annotating err with a stack trace
|
||||
// at the point Wrapf is call, and the format specifier.
|
||||
// If err is nil, Wrapf returns nil.
|
||||
func Wrapf(err error, format string, args ...interface{}) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
err = &withMessage{
|
||||
cause: err,
|
||||
msg: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return &withStack{
|
||||
err,
|
||||
callers(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessage annotates err with a new message.
|
||||
// If err is nil, WithMessage returns nil.
|
||||
func WithMessage(err error, message string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &withMessage{
|
||||
cause: err,
|
||||
msg: message,
|
||||
}
|
||||
}
|
||||
|
||||
type withMessage struct {
|
||||
cause error
|
||||
msg string
|
||||
}
|
||||
|
||||
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||
func (w *withMessage) Cause() error { return w.cause }
|
||||
|
||||
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||
io.WriteString(s, w.msg)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case 's', 'q':
|
||||
io.WriteString(s, w.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Cause returns the underlying cause of the error, if possible.
|
||||
// An error value has a cause if it implements the following
|
||||
// interface:
|
||||
//
|
||||
// type causer interface {
|
||||
// Cause() error
|
||||
// }
|
||||
//
|
||||
// If the error does not implement Cause, the original error will
|
||||
// be returned. If the error is nil, nil will be returned without further
|
||||
// investigation.
|
||||
func Cause(err error) error {
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
for err != nil {
|
||||
cause, ok := err.(causer)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
err = cause.Cause()
|
||||
}
|
||||
return err
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
err string
|
||||
want error
|
||||
}{
|
||||
{"", fmt.Errorf("")},
|
||||
{"foo", fmt.Errorf("foo")},
|
||||
{"foo", New("foo")},
|
||||
{"string with format specifiers: %v", errors.New("string with format specifiers: %v")},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := New(tt.err)
|
||||
if got.Error() != tt.want.Error() {
|
||||
t.Errorf("New.Error(): got: %q, want %q", got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapNil(t *testing.T) {
|
||||
got := Wrap(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := Wrap(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type nilError struct{}
|
||||
|
||||
func (nilError) Error() string { return "nil error" }
|
||||
|
||||
func TestCause(t *testing.T) {
|
||||
x := New("error")
|
||||
tests := []struct {
|
||||
err error
|
||||
want error
|
||||
}{{
|
||||
// nil error is nil
|
||||
err: nil,
|
||||
want: nil,
|
||||
}, {
|
||||
// explicit nil error is nil
|
||||
err: (error)(nil),
|
||||
want: nil,
|
||||
}, {
|
||||
// typed nil is nil
|
||||
err: (*nilError)(nil),
|
||||
want: (*nilError)(nil),
|
||||
}, {
|
||||
// uncaused error is unaffected
|
||||
err: io.EOF,
|
||||
want: io.EOF,
|
||||
}, {
|
||||
// caused error returns cause
|
||||
err: Wrap(io.EOF, "ignored"),
|
||||
want: io.EOF,
|
||||
}, {
|
||||
err: x, // return from errors.New
|
||||
want: x,
|
||||
}, {
|
||||
WithMessage(nil, "whoops"),
|
||||
nil,
|
||||
}, {
|
||||
WithMessage(io.EOF, "whoops"),
|
||||
io.EOF,
|
||||
}, {
|
||||
WithStack(nil),
|
||||
nil,
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
io.EOF,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := Cause(tt.err)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapfNil(t *testing.T) {
|
||||
got := Wrapf(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapf(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"},
|
||||
{Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := Wrapf(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorf(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{Errorf("read error without format specifiers"), "read error without format specifiers"},
|
||||
{Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.err.Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithStackNil(t *testing.T) {
|
||||
got := WithStack(nil)
|
||||
if got != nil {
|
||||
t.Errorf("WithStack(nil): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithStack(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "EOF"},
|
||||
{WithStack(io.EOF), "EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := WithStack(tt.err).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMessageNil(t *testing.T) {
|
||||
got := WithMessage(nil, "no error")
|
||||
if got != nil {
|
||||
t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
message string
|
||||
want string
|
||||
}{
|
||||
{io.EOF, "read error", "read error: EOF"},
|
||||
{WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := WithMessage(tt.err, tt.message).Error()
|
||||
if got != tt.want {
|
||||
t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// errors.New, etc values are not expected to be compared by value
|
||||
// but the change in errors#27 made them incomparable. Assert that
|
||||
// various kinds of errors have a functional equality operator, even
|
||||
// if the result of that equality is always false.
|
||||
func TestErrorEquality(t *testing.T) {
|
||||
vals := []error{
|
||||
nil,
|
||||
io.EOF,
|
||||
errors.New("EOF"),
|
||||
New("EOF"),
|
||||
Errorf("EOF"),
|
||||
Wrap(io.EOF, "EOF"),
|
||||
Wrapf(io.EOF, "EOF%d", 2),
|
||||
WithMessage(nil, "whoops"),
|
||||
WithMessage(io.EOF, "whoops"),
|
||||
WithStack(io.EOF),
|
||||
WithStack(nil),
|
||||
}
|
||||
|
||||
for i := range vals {
|
||||
for j := range vals {
|
||||
_ = vals[i] == vals[j] // mustn't panic
|
||||
}
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package errors_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func ExampleNew() {
|
||||
err := errors.New("whoops")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: whoops
|
||||
}
|
||||
|
||||
func ExampleNew_printf() {
|
||||
err := errors.New("whoops")
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example output:
|
||||
// whoops
|
||||
// github.com/pkg/errors_test.ExampleNew_printf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:17
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
}
|
||||
|
||||
func ExampleWithMessage() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithMessage(cause, "oh noes")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes: whoops
|
||||
}
|
||||
|
||||
func ExampleWithStack() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithStack(cause)
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: whoops
|
||||
}
|
||||
|
||||
func ExampleWithStack_printf() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.WithStack(cause)
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example Output:
|
||||
// whoops
|
||||
// github.com/pkg/errors_test.ExampleWithStack_printf
|
||||
// /home/fabstu/go/src/github.com/pkg/errors/example_test.go:55
|
||||
// testing.runExample
|
||||
// /usr/lib/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /usr/lib/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /usr/lib/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /usr/lib/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /usr/lib/go/src/runtime/asm_amd64.s:2086
|
||||
// github.com/pkg/errors_test.ExampleWithStack_printf
|
||||
// /home/fabstu/go/src/github.com/pkg/errors/example_test.go:56
|
||||
// testing.runExample
|
||||
// /usr/lib/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /usr/lib/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /usr/lib/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// github.com/pkg/errors/_test/_testmain.go:106
|
||||
// runtime.main
|
||||
// /usr/lib/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /usr/lib/go/src/runtime/asm_amd64.s:2086
|
||||
}
|
||||
|
||||
func ExampleWrap() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.Wrap(cause, "oh noes")
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes: whoops
|
||||
}
|
||||
|
||||
func fn() error {
|
||||
e1 := errors.New("error")
|
||||
e2 := errors.Wrap(e1, "inner")
|
||||
e3 := errors.Wrap(e2, "middle")
|
||||
return errors.Wrap(e3, "outer")
|
||||
}
|
||||
|
||||
func ExampleCause() {
|
||||
err := fn()
|
||||
fmt.Println(err)
|
||||
fmt.Println(errors.Cause(err))
|
||||
|
||||
// Output: outer: middle: inner: error
|
||||
// error
|
||||
}
|
||||
|
||||
func ExampleWrap_extended() {
|
||||
err := fn()
|
||||
fmt.Printf("%+v\n", err)
|
||||
|
||||
// Example output:
|
||||
// error
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:47
|
||||
// github.com/pkg/errors_test.ExampleCause_printf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:63
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:104
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:48: inner
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:49: middle
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:50: outer
|
||||
}
|
||||
|
||||
func ExampleWrapf() {
|
||||
cause := errors.New("whoops")
|
||||
err := errors.Wrapf(cause, "oh noes #%d", 2)
|
||||
fmt.Println(err)
|
||||
|
||||
// Output: oh noes #2: whoops
|
||||
}
|
||||
|
||||
func ExampleErrorf_extended() {
|
||||
err := errors.Errorf("whoops: %s", "foo")
|
||||
fmt.Printf("%+v", err)
|
||||
|
||||
// Example output:
|
||||
// whoops: foo
|
||||
// github.com/pkg/errors_test.ExampleErrorf
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:101
|
||||
// testing.runExample
|
||||
// /home/dfc/go/src/testing/example.go:114
|
||||
// testing.RunExamples
|
||||
// /home/dfc/go/src/testing/example.go:38
|
||||
// testing.(*M).Run
|
||||
// /home/dfc/go/src/testing/testing.go:744
|
||||
// main.main
|
||||
// /github.com/pkg/errors/_test/_testmain.go:102
|
||||
// runtime.main
|
||||
// /home/dfc/go/src/runtime/proc.go:183
|
||||
// runtime.goexit
|
||||
// /home/dfc/go/src/runtime/asm_amd64.s:2059
|
||||
}
|
||||
|
||||
func Example_stackTrace() {
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
err, ok := errors.Cause(fn()).(stackTracer)
|
||||
if !ok {
|
||||
panic("oops, err does not implement stackTracer")
|
||||
}
|
||||
|
||||
st := err.StackTrace()
|
||||
fmt.Printf("%+v", st[0:2]) // top two frames
|
||||
|
||||
// Example output:
|
||||
// github.com/pkg/errors_test.fn
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:47
|
||||
// github.com/pkg/errors_test.Example_stackTrace
|
||||
// /home/dfc/src/github.com/pkg/errors/example_test.go:127
|
||||
}
|
||||
|
||||
func ExampleCause_printf() {
|
||||
err := errors.Wrap(func() error {
|
||||
return func() error {
|
||||
return errors.Errorf("hello %s", fmt.Sprintf("world"))
|
||||
}()
|
||||
}(), "failed")
|
||||
|
||||
fmt.Printf("%v", err)
|
||||
|
||||
// Output: failed: hello world
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
New("error"),
|
||||
"%s",
|
||||
"error",
|
||||
}, {
|
||||
New("error"),
|
||||
"%v",
|
||||
"error",
|
||||
}, {
|
||||
New("error"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatNew\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:26",
|
||||
}, {
|
||||
New("error"),
|
||||
"%q",
|
||||
`"error"`,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatErrorf(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Errorf("%s", "error"),
|
||||
"%s",
|
||||
"error",
|
||||
}, {
|
||||
Errorf("%s", "error"),
|
||||
"%v",
|
||||
"error",
|
||||
}, {
|
||||
Errorf("%s", "error"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatErrorf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:56",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWrap(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Wrap(New("error"), "error2"),
|
||||
"%s",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrap(New("error"), "error2"),
|
||||
"%v",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrap(New("error"), "error2"),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:82",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%s",
|
||||
"error: EOF",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%v",
|
||||
"error: EOF",
|
||||
}, {
|
||||
Wrap(io.EOF, "error"),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:96",
|
||||
}, {
|
||||
Wrap(Wrap(io.EOF, "error1"), "error2"),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error1\n" +
|
||||
"github.com/pkg/errors.TestFormatWrap\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:103\n",
|
||||
}, {
|
||||
Wrap(New("error with space"), "context"),
|
||||
"%q",
|
||||
`"context: error with space"`,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWrapf(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%s",
|
||||
"error2: EOF",
|
||||
}, {
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%v",
|
||||
"error2: EOF",
|
||||
}, {
|
||||
Wrapf(io.EOF, "error%d", 2),
|
||||
"%+v",
|
||||
"EOF\n" +
|
||||
"error2\n" +
|
||||
"github.com/pkg/errors.TestFormatWrapf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:134",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%s",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%v",
|
||||
"error2: error",
|
||||
}, {
|
||||
Wrapf(New("error"), "error%d", 2),
|
||||
"%+v",
|
||||
"error\n" +
|
||||
"github.com/pkg/errors.TestFormatWrapf\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:149",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.error, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWithStack(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want []string
|
||||
}{{
|
||||
WithStack(io.EOF),
|
||||
"%s",
|
||||
[]string{"EOF"},
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
"%v",
|
||||
[]string{"EOF"},
|
||||
}, {
|
||||
WithStack(io.EOF),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:175"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%s",
|
||||
[]string{"error"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%v",
|
||||
[]string{"error"},
|
||||
}, {
|
||||
WithStack(New("error")),
|
||||
"%+v",
|
||||
[]string{"error",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:189",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:189"},
|
||||
}, {
|
||||
WithStack(WithStack(io.EOF)),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:197",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:197"},
|
||||
}, {
|
||||
WithStack(WithStack(Wrapf(io.EOF, "message"))),
|
||||
"%+v",
|
||||
[]string{"EOF",
|
||||
"message",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:205"},
|
||||
}, {
|
||||
WithStack(Errorf("error%d", 1)),
|
||||
"%+v",
|
||||
[]string{"error1",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:216",
|
||||
"github.com/pkg/errors.TestFormatWithStack\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:216"},
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatWithMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
error
|
||||
format string
|
||||
want []string
|
||||
}{{
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%s",
|
||||
[]string{"error2: error"},
|
||||
}, {
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%v",
|
||||
[]string{"error2: error"},
|
||||
}, {
|
||||
WithMessage(New("error"), "error2"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"error",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:244",
|
||||
"error2"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%s",
|
||||
[]string{"addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%v",
|
||||
[]string{"addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(io.EOF, "addition1"),
|
||||
"%+v",
|
||||
[]string{"EOF", "addition1"},
|
||||
}, {
|
||||
WithMessage(WithMessage(io.EOF, "addition1"), "addition2"),
|
||||
"%v",
|
||||
[]string{"addition2: addition1: EOF"},
|
||||
}, {
|
||||
WithMessage(WithMessage(io.EOF, "addition1"), "addition2"),
|
||||
"%+v",
|
||||
[]string{"EOF", "addition1", "addition2"},
|
||||
}, {
|
||||
Wrap(WithMessage(io.EOF, "error1"), "error2"),
|
||||
"%+v",
|
||||
[]string{"EOF", "error1", "error2",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:272"},
|
||||
}, {
|
||||
WithMessage(Errorf("error%d", 1), "error2"),
|
||||
"%+v",
|
||||
[]string{"error1",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:278",
|
||||
"error2"},
|
||||
}, {
|
||||
WithMessage(WithStack(io.EOF), "error"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:285",
|
||||
"error"},
|
||||
}, {
|
||||
WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"),
|
||||
"%+v",
|
||||
[]string{
|
||||
"EOF",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:293",
|
||||
"inside-error",
|
||||
"github.com/pkg/errors.TestFormatWithMessage\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:293",
|
||||
"outside-error"},
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatGeneric(t *testing.T) {
|
||||
starts := []struct {
|
||||
err error
|
||||
want []string
|
||||
}{
|
||||
{New("new-error"), []string{
|
||||
"new-error",
|
||||
"github.com/pkg/errors.TestFormatGeneric\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:315"},
|
||||
}, {Errorf("errorf-error"), []string{
|
||||
"errorf-error",
|
||||
"github.com/pkg/errors.TestFormatGeneric\n" +
|
||||
"\t.+/github.com/pkg/errors/format_test.go:319"},
|
||||
}, {errors.New("errors-new-error"), []string{
|
||||
"errors-new-error"},
|
||||
},
|
||||
}
|
||||
|
||||
wrappers := []wrapper{
|
||||
{
|
||||
func(err error) error { return WithMessage(err, "with-message") },
|
||||
[]string{"with-message"},
|
||||
}, {
|
||||
func(err error) error { return WithStack(err) },
|
||||
[]string{
|
||||
"github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:333",
|
||||
},
|
||||
}, {
|
||||
func(err error) error { return Wrap(err, "wrap-error") },
|
||||
[]string{
|
||||
"wrap-error",
|
||||
"github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:339",
|
||||
},
|
||||
}, {
|
||||
func(err error) error { return Wrapf(err, "wrapf-error%d", 1) },
|
||||
[]string{
|
||||
"wrapf-error1",
|
||||
"github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" +
|
||||
".+/github.com/pkg/errors/format_test.go:346",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for s := range starts {
|
||||
err := starts[s].err
|
||||
want := starts[s].want
|
||||
testFormatCompleteCompare(t, s, err, "%+v", want, false)
|
||||
testGenericRecursive(t, err, want, wrappers, 3)
|
||||
}
|
||||
}
|
||||
|
||||
func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) {
|
||||
got := fmt.Sprintf(format, arg)
|
||||
gotLines := strings.SplitN(got, "\n", -1)
|
||||
wantLines := strings.SplitN(want, "\n", -1)
|
||||
|
||||
if len(wantLines) > len(gotLines) {
|
||||
t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want)
|
||||
return
|
||||
}
|
||||
|
||||
for i, w := range wantLines {
|
||||
match, err := regexp.MatchString(w, gotLines[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stackLineR = regexp.MustCompile(`\.`)
|
||||
|
||||
// parseBlocks parses input into a slice, where:
|
||||
// - incase entry contains a newline, its a stacktrace
|
||||
// - incase entry contains no newline, its a solo line.
|
||||
//
|
||||
// Detecting stack boundaries only works incase the WithStack-calls are
|
||||
// to be found on the same line, thats why it is optionally here.
|
||||
//
|
||||
// Example use:
|
||||
//
|
||||
// for _, e := range blocks {
|
||||
// if strings.ContainsAny(e, "\n") {
|
||||
// // Match as stack
|
||||
// } else {
|
||||
// // Match as line
|
||||
// }
|
||||
// }
|
||||
//
|
||||
func parseBlocks(input string, detectStackboundaries bool) ([]string, error) {
|
||||
var blocks []string
|
||||
|
||||
stack := ""
|
||||
wasStack := false
|
||||
lines := map[string]bool{} // already found lines
|
||||
|
||||
for _, l := range strings.Split(input, "\n") {
|
||||
isStackLine := stackLineR.MatchString(l)
|
||||
|
||||
switch {
|
||||
case !isStackLine && wasStack:
|
||||
blocks = append(blocks, stack, l)
|
||||
stack = ""
|
||||
lines = map[string]bool{}
|
||||
case isStackLine:
|
||||
if wasStack {
|
||||
// Detecting two stacks after another, possible cause lines match in
|
||||
// our tests due to WithStack(WithStack(io.EOF)) on same line.
|
||||
if detectStackboundaries {
|
||||
if lines[l] {
|
||||
if len(stack) == 0 {
|
||||
return nil, errors.New("len of block must not be zero here")
|
||||
}
|
||||
|
||||
blocks = append(blocks, stack)
|
||||
stack = l
|
||||
lines = map[string]bool{l: true}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
stack = stack + "\n" + l
|
||||
} else {
|
||||
stack = l
|
||||
}
|
||||
lines[l] = true
|
||||
case !isStackLine && !wasStack:
|
||||
blocks = append(blocks, l)
|
||||
default:
|
||||
return nil, errors.New("must not happen")
|
||||
}
|
||||
|
||||
wasStack = isStackLine
|
||||
}
|
||||
|
||||
// Use up stack
|
||||
if stack != "" {
|
||||
blocks = append(blocks, stack)
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) {
|
||||
gotStr := fmt.Sprintf(format, arg)
|
||||
|
||||
got, err := parseBlocks(gotStr, detectStackBoundaries)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q",
|
||||
n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr)
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
if strings.ContainsAny(want[i], "\n") {
|
||||
// Match as stack
|
||||
match, err := regexp.MatchString(want[i], got[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n",
|
||||
n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want))
|
||||
}
|
||||
} else {
|
||||
// Match as message
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
wrap func(err error) error
|
||||
want []string
|
||||
}
|
||||
|
||||
func prettyBlocks(blocks []string, prefix ...string) string {
|
||||
var out []string
|
||||
|
||||
for _, b := range blocks {
|
||||
out = append(out, fmt.Sprintf("%v", b))
|
||||
}
|
||||
|
||||
return " " + strings.Join(out, "\n ")
|
||||
}
|
||||
|
||||
func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) {
|
||||
if len(beforeWant) == 0 {
|
||||
panic("beforeWant must not be empty")
|
||||
}
|
||||
for _, w := range list {
|
||||
if len(w.want) == 0 {
|
||||
panic("want must not be empty")
|
||||
}
|
||||
|
||||
err := w.wrap(beforeErr)
|
||||
|
||||
// Copy required cause append(beforeWant, ..) modified beforeWant subtly.
|
||||
beforeCopy := make([]string, len(beforeWant))
|
||||
copy(beforeCopy, beforeWant)
|
||||
|
||||
beforeWant := beforeCopy
|
||||
last := len(beforeWant) - 1
|
||||
var want []string
|
||||
|
||||
// Merge two stacks behind each other.
|
||||
if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") {
|
||||
want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...)
|
||||
} else {
|
||||
want = append(beforeWant, w.want...)
|
||||
}
|
||||
|
||||
testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false)
|
||||
if maxDepth > 0 {
|
||||
testGenericRecursive(t, err, want, list, maxDepth-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Frame represents a program counter inside a stack frame.
|
||||
type Frame uintptr
|
||||
|
||||
// pc returns the program counter for this frame;
|
||||
// multiple frames may have the same PC value.
|
||||
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||
|
||||
// file returns the full path to the file that contains the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) file() string {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
file, _ := fn.FileLine(f.pc())
|
||||
return file
|
||||
}
|
||||
|
||||
// line returns the line number of source code of the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) line() int {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return 0
|
||||
}
|
||||
_, line := fn.FileLine(f.pc())
|
||||
return line
|
||||
}
|
||||
|
||||
// Format formats the frame according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+s path of source file relative to the compile time GOPATH
|
||||
// %+v equivalent to %+s:%d
|
||||
func (f Frame) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
pc := f.pc()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
io.WriteString(s, "unknown")
|
||||
} else {
|
||||
file, _ := fn.FileLine(pc)
|
||||
fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
|
||||
}
|
||||
default:
|
||||
io.WriteString(s, path.Base(f.file()))
|
||||
}
|
||||
case 'd':
|
||||
fmt.Fprintf(s, "%d", f.line())
|
||||
case 'n':
|
||||
name := runtime.FuncForPC(f.pc()).Name()
|
||||
io.WriteString(s, funcname(name))
|
||||
case 'v':
|
||||
f.Format(s, 's')
|
||||
io.WriteString(s, ":")
|
||||
f.Format(s, 'd')
|
||||
}
|
||||
}
|
||||
|
||||
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||
type StackTrace []Frame
|
||||
|
||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
for _, f := range st {
|
||||
fmt.Fprintf(s, "\n%+v", f)
|
||||
}
|
||||
case s.Flag('#'):
|
||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
default:
|
||||
fmt.Fprintf(s, "%v", []Frame(st))
|
||||
}
|
||||
case 's':
|
||||
fmt.Fprintf(s, "%s", []Frame(st))
|
||||
}
|
||||
}
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
type stack []uintptr
|
||||
|
||||
func (s *stack) Format(st fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case st.Flag('+'):
|
||||
for _, pc := range *s {
|
||||
f := Frame(pc)
|
||||
fmt.Fprintf(st, "\n%+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stack) StackTrace() StackTrace {
|
||||
f := make([]Frame, len(*s))
|
||||
for i := 0; i < len(f); i++ {
|
||||
f[i] = Frame((*s)[i])
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func callers() *stack {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(3, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return &st
|
||||
}
|
||||
|
||||
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||
func funcname(name string) string {
|
||||
i := strings.LastIndex(name, "/")
|
||||
name = name[i+1:]
|
||||
i = strings.Index(name, ".")
|
||||
return name[i+1:]
|
||||
}
|
||||
|
||||
func trimGOPATH(name, file string) string {
|
||||
// Here we want to get the source file path relative to the compile time
|
||||
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
|
||||
// GOPATH at runtime, but we can infer the number of path segments in the
|
||||
// GOPATH. We note that fn.Name() returns the function name qualified by
|
||||
// the import path, which does not include the GOPATH. Thus we can trim
|
||||
// segments from the beginning of the file path until the number of path
|
||||
// separators remaining is one more than the number of path separators in
|
||||
// the function name. For example, given:
|
||||
//
|
||||
// GOPATH /home/user
|
||||
// file /home/user/src/pkg/sub/file.go
|
||||
// fn.Name() pkg/sub.Type.Method
|
||||
//
|
||||
// We want to produce:
|
||||
//
|
||||
// pkg/sub/file.go
|
||||
//
|
||||
// From this we can easily see that fn.Name() has one less path separator
|
||||
// than our desired output. We count separators from the end of the file
|
||||
// path until it finds two more than in the function name and then move
|
||||
// one character forward to preserve the initial path segment without a
|
||||
// leading separator.
|
||||
const sep = "/"
|
||||
goal := strings.Count(name, sep) + 2
|
||||
i := len(file)
|
||||
for n := 0; n < goal; n++ {
|
||||
i = strings.LastIndex(file[:i], sep)
|
||||
if i == -1 {
|
||||
// not enough separators found, set i so that the slice expression
|
||||
// below leaves file unmodified
|
||||
i = -len(sep)
|
||||
break
|
||||
}
|
||||
}
|
||||
// get back to 0 or trim the leading separator
|
||||
file = file[i+len(sep):]
|
||||
return file
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var initpc, _, _, _ = runtime.Caller(0)
|
||||
|
||||
func TestFrameLine(t *testing.T) {
|
||||
var tests = []struct {
|
||||
Frame
|
||||
want int
|
||||
}{{
|
||||
Frame(initpc),
|
||||
9,
|
||||
}, {
|
||||
func() Frame {
|
||||
var pc, _, _, _ = runtime.Caller(0)
|
||||
return Frame(pc)
|
||||
}(),
|
||||
20,
|
||||
}, {
|
||||
func() Frame {
|
||||
var pc, _, _, _ = runtime.Caller(1)
|
||||
return Frame(pc)
|
||||
}(),
|
||||
28,
|
||||
}, {
|
||||
Frame(0), // invalid PC
|
||||
0,
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := tt.Frame.line()
|
||||
want := tt.want
|
||||
if want != got {
|
||||
t.Errorf("Frame(%v): want: %v, got: %v", uintptr(tt.Frame), want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type X struct{}
|
||||
|
||||
func (x X) val() Frame {
|
||||
var pc, _, _, _ = runtime.Caller(0)
|
||||
return Frame(pc)
|
||||
}
|
||||
|
||||
func (x *X) ptr() Frame {
|
||||
var pc, _, _, _ = runtime.Caller(0)
|
||||
return Frame(pc)
|
||||
}
|
||||
|
||||
func TestFrameFormat(t *testing.T) {
|
||||
var tests = []struct {
|
||||
Frame
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
Frame(initpc),
|
||||
"%s",
|
||||
"stack_test.go",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%+s",
|
||||
"github.com/pkg/errors.init\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%s",
|
||||
"unknown",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%+s",
|
||||
"unknown",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%d",
|
||||
"9",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%d",
|
||||
"0",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%n",
|
||||
"init",
|
||||
}, {
|
||||
func() Frame {
|
||||
var x X
|
||||
return x.ptr()
|
||||
}(),
|
||||
"%n",
|
||||
`\(\*X\).ptr`,
|
||||
}, {
|
||||
func() Frame {
|
||||
var x X
|
||||
return x.val()
|
||||
}(),
|
||||
"%n",
|
||||
"X.val",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%n",
|
||||
"",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%v",
|
||||
"stack_test.go:9",
|
||||
}, {
|
||||
Frame(initpc),
|
||||
"%+v",
|
||||
"github.com/pkg/errors.init\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:9",
|
||||
}, {
|
||||
Frame(0),
|
||||
"%v",
|
||||
"unknown:0",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.Frame, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncname(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"runtime.main", "main"},
|
||||
{"github.com/pkg/errors.funcname", "funcname"},
|
||||
{"funcname", "funcname"},
|
||||
{"io.copyBuffer", "copyBuffer"},
|
||||
{"main.(*R).Write", "(*R).Write"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := funcname(tt.name)
|
||||
want := tt.want
|
||||
if got != want {
|
||||
t.Errorf("funcname(%q): want: %q, got %q", tt.name, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimGOPATH(t *testing.T) {
|
||||
var tests = []struct {
|
||||
Frame
|
||||
want string
|
||||
}{{
|
||||
Frame(initpc),
|
||||
"github.com/pkg/errors/stack_test.go",
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
pc := tt.Frame.pc()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
file, _ := fn.FileLine(pc)
|
||||
got := trimGOPATH(fn.Name(), file)
|
||||
testFormatRegexp(t, i, got, "%s", tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackTrace(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
want []string
|
||||
}{{
|
||||
New("ooh"), []string{
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:172",
|
||||
},
|
||||
}, {
|
||||
Wrap(New("ooh"), "ahh"), []string{
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:177", // this is the stack of Wrap, not New
|
||||
},
|
||||
}, {
|
||||
Cause(Wrap(New("ooh"), "ahh")), []string{
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:182", // this is the stack of New
|
||||
},
|
||||
}, {
|
||||
func() error { return New("ooh") }(), []string{
|
||||
`github.com/pkg/errors.(func·009|TestStackTrace.func1)` +
|
||||
"\n\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New's caller
|
||||
},
|
||||
}, {
|
||||
Cause(func() error {
|
||||
return func() error {
|
||||
return Errorf("hello %s", fmt.Sprintf("world"))
|
||||
}()
|
||||
}()), []string{
|
||||
`github.com/pkg/errors.(func·010|TestStackTrace.func2.1)` +
|
||||
"\n\t.+/github.com/pkg/errors/stack_test.go:196", // this is the stack of Errorf
|
||||
`github.com/pkg/errors.(func·011|TestStackTrace.func2)` +
|
||||
"\n\t.+/github.com/pkg/errors/stack_test.go:197", // this is the stack of Errorf's caller
|
||||
"github.com/pkg/errors.TestStackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:198", // this is the stack of Errorf's caller's caller
|
||||
},
|
||||
}}
|
||||
for i, tt := range tests {
|
||||
x, ok := tt.err.(interface {
|
||||
StackTrace() StackTrace
|
||||
})
|
||||
if !ok {
|
||||
t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err)
|
||||
continue
|
||||
}
|
||||
st := x.StackTrace()
|
||||
for j, want := range tt.want {
|
||||
testFormatRegexp(t, i, st[j], "%+v", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stackTrace() StackTrace {
|
||||
const depth = 8
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(1, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return st.StackTrace()
|
||||
}
|
||||
|
||||
func TestStackTraceFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
StackTrace
|
||||
format string
|
||||
want string
|
||||
}{{
|
||||
nil,
|
||||
"%s",
|
||||
`\[\]`,
|
||||
}, {
|
||||
nil,
|
||||
"%v",
|
||||
`\[\]`,
|
||||
}, {
|
||||
nil,
|
||||
"%+v",
|
||||
"",
|
||||
}, {
|
||||
nil,
|
||||
"%#v",
|
||||
`\[\]errors.Frame\(nil\)`,
|
||||
}, {
|
||||
make(StackTrace, 0),
|
||||
"%s",
|
||||
`\[\]`,
|
||||
}, {
|
||||
make(StackTrace, 0),
|
||||
"%v",
|
||||
`\[\]`,
|
||||
}, {
|
||||
make(StackTrace, 0),
|
||||
"%+v",
|
||||
"",
|
||||
}, {
|
||||
make(StackTrace, 0),
|
||||
"%#v",
|
||||
`\[\]errors.Frame{}`,
|
||||
}, {
|
||||
stackTrace()[:2],
|
||||
"%s",
|
||||
`\[stack_test.go stack_test.go\]`,
|
||||
}, {
|
||||
stackTrace()[:2],
|
||||
"%v",
|
||||
`\[stack_test.go:225 stack_test.go:272\]`,
|
||||
}, {
|
||||
stackTrace()[:2],
|
||||
"%+v",
|
||||
"\n" +
|
||||
"github.com/pkg/errors.stackTrace\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:225\n" +
|
||||
"github.com/pkg/errors.TestStackTraceFormat\n" +
|
||||
"\t.+/github.com/pkg/errors/stack_test.go:276",
|
||||
}, {
|
||||
stackTrace()[:2],
|
||||
"%#v",
|
||||
`\[\]errors.Frame{stack_test.go:225, stack_test.go:284}`,
|
||||
}}
|
||||
|
||||
for i, tt := range tests {
|
||||
testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want)
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
Generated
+2
@@ -0,0 +1,2 @@
|
||||
*.coverprofile
|
||||
node_modules/
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
language: go
|
||||
sudo: false
|
||||
dist: trusty
|
||||
osx_image: xcode8.3
|
||||
go: 1.8.x
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
before_script:
|
||||
- go get github.com/urfave/gfmrun/... || true
|
||||
- go get golang.org/x/tools/cmd/goimports
|
||||
- if [ ! -f node_modules/.bin/markdown-toc ] ; then
|
||||
npm install markdown-toc ;
|
||||
fi
|
||||
|
||||
script:
|
||||
- ./runtests gen
|
||||
- ./runtests vet
|
||||
- ./runtests test
|
||||
- ./runtests gfmrun
|
||||
- ./runtests toc
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
# Change Log
|
||||
|
||||
**ATTN**: This project uses [semantic versioning](http://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## 1.20.0 - 2017-08-10
|
||||
|
||||
### Fixed
|
||||
|
||||
* `HandleExitCoder` is now correctly iterates over all errors in
|
||||
a `MultiError`. The exit code is the exit code of the last error or `1` if
|
||||
there are no `ExitCoder`s in the `MultiError`.
|
||||
* Fixed YAML file loading on Windows (previously would fail validate the file path)
|
||||
* Subcommand `Usage`, `Description`, `ArgsUsage`, `OnUsageError` correctly
|
||||
propogated
|
||||
* `ErrWriter` is now passed downwards through command structure to avoid the
|
||||
need to redefine it
|
||||
* Pass `Command` context into `OnUsageError` rather than parent context so that
|
||||
all fields are avaiable
|
||||
* Errors occuring in `Before` funcs are no longer double printed
|
||||
* Use `UsageText` in the help templates for commands and subcommands if
|
||||
defined; otherwise build the usage as before (was previously ignoring this
|
||||
field)
|
||||
* `IsSet` and `GlobalIsSet` now correctly return whether a flag is set if
|
||||
a program calls `Set` or `GlobalSet` directly after flag parsing (would
|
||||
previously only return `true` if the flag was set during parsing)
|
||||
|
||||
### Changed
|
||||
|
||||
* No longer exit the program on command/subcommand error if the error raised is
|
||||
not an `OsExiter`. This exiting behavior was introduced in 1.19.0, but was
|
||||
determined to be a regression in functionality. See [the
|
||||
PR](https://github.com/urfave/cli/pull/595) for discussion.
|
||||
|
||||
### Added
|
||||
|
||||
* `CommandsByName` type was added to make it easy to sort `Command`s by name,
|
||||
alphabetically
|
||||
* `altsrc` now handles loading of string and int arrays from TOML
|
||||
* Support for definition of custom help templates for `App` via
|
||||
`CustomAppHelpTemplate`
|
||||
* Support for arbitrary key/value fields on `App` to be used with
|
||||
`CustomAppHelpTemplate` via `ExtraInfo`
|
||||
* `HelpFlag`, `VersionFlag`, and `BashCompletionFlag` changed to explictly be
|
||||
`cli.Flag`s allowing for the use of custom flags satisfying the `cli.Flag`
|
||||
interface to be used.
|
||||
|
||||
|
||||
## [1.19.1] - 2016-11-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixes regression introduced in 1.19.0 where using an `ActionFunc` as
|
||||
the `Action` for a command would cause it to error rather than calling the
|
||||
function. Should not have a affected declarative cases using `func(c
|
||||
*cli.Context) err)`.
|
||||
- Shell completion now handles the case where the user specifies
|
||||
`--generate-bash-completion` immediately after a flag that takes an argument.
|
||||
Previously it call the application with `--generate-bash-completion` as the
|
||||
flag value.
|
||||
|
||||
## [1.19.0] - 2016-11-19
|
||||
### Added
|
||||
- `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`)
|
||||
- A `Description` field was added to `App` for a more detailed description of
|
||||
the application (similar to the existing `Description` field on `Command`)
|
||||
- Flag type code generation via `go generate`
|
||||
- Write to stderr and exit 1 if action returns non-nil error
|
||||
- Added support for TOML to the `altsrc` loader
|
||||
- `SkipArgReorder` was added to allow users to skip the argument reordering.
|
||||
This is useful if you want to consider all "flags" after an argument as
|
||||
arguments rather than flags (the default behavior of the stdlib `flag`
|
||||
library). This is backported functionality from the [removal of the flag
|
||||
reordering](https://github.com/urfave/cli/pull/398) in the unreleased version
|
||||
2
|
||||
- For formatted errors (those implementing `ErrorFormatter`), the errors will
|
||||
be formatted during output. Compatible with `pkg/errors`.
|
||||
|
||||
### Changed
|
||||
- Raise minimum tested/supported Go version to 1.2+
|
||||
|
||||
### Fixed
|
||||
- Consider empty environment variables as set (previously environment variables
|
||||
with the equivalent of `""` would be skipped rather than their value used).
|
||||
- Return an error if the value in a given environment variable cannot be parsed
|
||||
as the flag type. Previously these errors were silently swallowed.
|
||||
- Print full error when an invalid flag is specified (which includes the invalid flag)
|
||||
- `App.Writer` defaults to `stdout` when `nil`
|
||||
- If no action is specified on a command or app, the help is now printed instead of `panic`ing
|
||||
- `App.Metadata` is initialized automatically now (previously was `nil` unless initialized)
|
||||
- Correctly show help message if `-h` is provided to a subcommand
|
||||
- `context.(Global)IsSet` now respects environment variables. Previously it
|
||||
would return `false` if a flag was specified in the environment rather than
|
||||
as an argument
|
||||
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||
- `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This
|
||||
fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well
|
||||
as `altsrc` where Go would complain that the types didn't match
|
||||
|
||||
## [1.18.1] - 2016-08-28
|
||||
### Fixed
|
||||
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported)
|
||||
|
||||
## [1.18.0] - 2016-06-27
|
||||
### Added
|
||||
- `./runtests` test runner with coverage tracking by default
|
||||
- testing on OS X
|
||||
- testing on Windows
|
||||
- `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code
|
||||
|
||||
### Changed
|
||||
- Use spaces for alignment in help/usage output instead of tabs, making the
|
||||
output alignment consistent regardless of tab width
|
||||
|
||||
### Fixed
|
||||
- Printing of command aliases in help text
|
||||
- Printing of visible flags for both struct and struct pointer flags
|
||||
- Display the `help` subcommand when using `CommandCategories`
|
||||
- No longer swallows `panic`s that occur within the `Action`s themselves when
|
||||
detecting the signature of the `Action` field
|
||||
|
||||
## [1.17.1] - 2016-08-28
|
||||
### Fixed
|
||||
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||
|
||||
## [1.17.0] - 2016-05-09
|
||||
### Added
|
||||
- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc`
|
||||
- `context.GlobalBoolT` was added as an analogue to `context.GlobalBool`
|
||||
- Support for hiding commands by setting `Hidden: true` -- this will hide the
|
||||
commands in help output
|
||||
|
||||
### Changed
|
||||
- `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer
|
||||
quoted in help text output.
|
||||
- All flag types now include `(default: {value})` strings following usage when a
|
||||
default value can be (reasonably) detected.
|
||||
- `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent
|
||||
with non-slice flag types
|
||||
- Apps now exit with a code of 3 if an unknown subcommand is specified
|
||||
(previously they printed "No help topic for...", but still exited 0. This
|
||||
makes it easier to script around apps built using `cli` since they can trust
|
||||
that a 0 exit code indicated a successful execution.
|
||||
- cleanups based on [Go Report Card
|
||||
feedback](https://goreportcard.com/report/github.com/urfave/cli)
|
||||
|
||||
## [1.16.1] - 2016-08-28
|
||||
### Fixed
|
||||
- Removed deprecation warnings to STDERR to avoid them leaking to the end-user
|
||||
|
||||
## [1.16.0] - 2016-05-02
|
||||
### Added
|
||||
- `Hidden` field on all flag struct types to omit from generated help text
|
||||
|
||||
### Changed
|
||||
- `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from
|
||||
generated help text via the `Hidden` field
|
||||
|
||||
### Fixed
|
||||
- handling of error values in `HandleAction` and `HandleExitCoder`
|
||||
|
||||
## [1.15.0] - 2016-04-30
|
||||
### Added
|
||||
- This file!
|
||||
- Support for placeholders in flag usage strings
|
||||
- `App.Metadata` map for arbitrary data/state management
|
||||
- `Set` and `GlobalSet` methods on `*cli.Context` for altering values after
|
||||
parsing.
|
||||
- Support for nested lookup of dot-delimited keys in structures loaded from
|
||||
YAML.
|
||||
|
||||
### Changed
|
||||
- The `App.Action` and `Command.Action` now prefer a return signature of
|
||||
`func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil
|
||||
`error` is returned, there may be two outcomes:
|
||||
- If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called
|
||||
automatically
|
||||
- Else the error is bubbled up and returned from `App.Run`
|
||||
- Specifying an `Action` with the legacy return signature of
|
||||
`func(*cli.Context)` will produce a deprecation message to stderr
|
||||
- Specifying an `Action` that is not a `func` type will produce a non-zero exit
|
||||
from `App.Run`
|
||||
- Specifying an `Action` func that has an invalid (input) signature will
|
||||
produce a non-zero exit from `App.Run`
|
||||
|
||||
### Deprecated
|
||||
- <a name="deprecated-cli-app-runandexitonerror"></a>
|
||||
`cli.App.RunAndExitOnError`, which should now be done by returning an error
|
||||
that fulfills `cli.ExitCoder` to `cli.App.Run`.
|
||||
- <a name="deprecated-cli-app-action-signature"></a> the legacy signature for
|
||||
`cli.App.Action` of `func(*cli.Context)`, which should now have a return
|
||||
signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`.
|
||||
|
||||
### Fixed
|
||||
- Added missing `*cli.Context.GlobalFloat64` method
|
||||
|
||||
## [1.14.0] - 2016-04-03 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Codebeat badge
|
||||
- Support for categorization via `CategorizedHelp` and `Categories` on app.
|
||||
|
||||
### Changed
|
||||
- Use `filepath.Base` instead of `path.Base` in `Name` and `HelpName`.
|
||||
|
||||
### Fixed
|
||||
- Ensure version is not shown in help text when `HideVersion` set.
|
||||
|
||||
## [1.13.0] - 2016-03-06 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- YAML file input support.
|
||||
- `NArg` method on context.
|
||||
|
||||
## [1.12.0] - 2016-02-17 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Custom usage error handling.
|
||||
- Custom text support in `USAGE` section of help output.
|
||||
- Improved help messages for empty strings.
|
||||
- AppVeyor CI configuration.
|
||||
|
||||
### Changed
|
||||
- Removed `panic` from default help printer func.
|
||||
- De-duping and optimizations.
|
||||
|
||||
### Fixed
|
||||
- Correctly handle `Before`/`After` at command level when no subcommands.
|
||||
- Case of literal `-` argument causing flag reordering.
|
||||
- Environment variable hints on Windows.
|
||||
- Docs updates.
|
||||
|
||||
## [1.11.1] - 2015-12-21 (backfilled 2016-04-25)
|
||||
### Changed
|
||||
- Use `path.Base` in `Name` and `HelpName`
|
||||
- Export `GetName` on flag types.
|
||||
|
||||
### Fixed
|
||||
- Flag parsing when skipping is enabled.
|
||||
- Test output cleanup.
|
||||
- Move completion check to account for empty input case.
|
||||
|
||||
## [1.11.0] - 2015-11-15 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Destination scan support for flags.
|
||||
- Testing against `tip` in Travis CI config.
|
||||
|
||||
### Changed
|
||||
- Go version in Travis CI config.
|
||||
|
||||
### Fixed
|
||||
- Removed redundant tests.
|
||||
- Use correct example naming in tests.
|
||||
|
||||
## [1.10.2] - 2015-10-29 (backfilled 2016-04-25)
|
||||
### Fixed
|
||||
- Remove unused var in bash completion.
|
||||
|
||||
## [1.10.1] - 2015-10-21 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Coverage and reference logos in README.
|
||||
|
||||
### Fixed
|
||||
- Use specified values in help and version parsing.
|
||||
- Only display app version and help message once.
|
||||
|
||||
## [1.10.0] - 2015-10-06 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- More tests for existing functionality.
|
||||
- `ArgsUsage` at app and command level for help text flexibility.
|
||||
|
||||
### Fixed
|
||||
- Honor `HideHelp` and `HideVersion` in `App.Run`.
|
||||
- Remove juvenile word from README.
|
||||
|
||||
## [1.9.0] - 2015-09-08 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- `FullName` on command with accompanying help output update.
|
||||
- Set default `$PROG` in bash completion.
|
||||
|
||||
### Changed
|
||||
- Docs formatting.
|
||||
|
||||
### Fixed
|
||||
- Removed self-referential imports in tests.
|
||||
|
||||
## [1.8.0] - 2015-06-30 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Support for `Copyright` at app level.
|
||||
- `Parent` func at context level to walk up context lineage.
|
||||
|
||||
### Fixed
|
||||
- Global flag processing at top level.
|
||||
|
||||
## [1.7.1] - 2015-06-11 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Aggregate errors from `Before`/`After` funcs.
|
||||
- Doc comments on flag structs.
|
||||
- Include non-global flags when checking version and help.
|
||||
- Travis CI config updates.
|
||||
|
||||
### Fixed
|
||||
- Ensure slice type flags have non-nil values.
|
||||
- Collect global flags from the full command hierarchy.
|
||||
- Docs prose.
|
||||
|
||||
## [1.7.0] - 2015-05-03 (backfilled 2016-04-25)
|
||||
### Changed
|
||||
- `HelpPrinter` signature includes output writer.
|
||||
|
||||
### Fixed
|
||||
- Specify go 1.1+ in docs.
|
||||
- Set `Writer` when running command as app.
|
||||
|
||||
## [1.6.0] - 2015-03-23 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Multiple author support.
|
||||
- `NumFlags` at context level.
|
||||
- `Aliases` at command level.
|
||||
|
||||
### Deprecated
|
||||
- `ShortName` at command level.
|
||||
|
||||
### Fixed
|
||||
- Subcommand help output.
|
||||
- Backward compatible support for deprecated `Author` and `Email` fields.
|
||||
- Docs regarding `Names`/`Aliases`.
|
||||
|
||||
## [1.5.0] - 2015-02-20 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- `After` hook func support at app and command level.
|
||||
|
||||
### Fixed
|
||||
- Use parsed context when running command as subcommand.
|
||||
- Docs prose.
|
||||
|
||||
## [1.4.1] - 2015-01-09 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Support for hiding `-h / --help` flags, but not `help` subcommand.
|
||||
- Stop flag parsing after `--`.
|
||||
|
||||
### Fixed
|
||||
- Help text for generic flags to specify single value.
|
||||
- Use double quotes in output for defaults.
|
||||
- Use `ParseInt` instead of `ParseUint` for int environment var values.
|
||||
- Use `0` as base when parsing int environment var values.
|
||||
|
||||
## [1.4.0] - 2014-12-12 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Support for environment variable lookup "cascade".
|
||||
- Support for `Stdout` on app for output redirection.
|
||||
|
||||
### Fixed
|
||||
- Print command help instead of app help in `ShowCommandHelp`.
|
||||
|
||||
## [1.3.1] - 2014-11-13 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- Docs and example code updates.
|
||||
|
||||
### Changed
|
||||
- Default `-v / --version` flag made optional.
|
||||
|
||||
## [1.3.0] - 2014-08-10 (backfilled 2016-04-25)
|
||||
### Added
|
||||
- `FlagNames` at context level.
|
||||
- Exposed `VersionPrinter` var for more control over version output.
|
||||
- Zsh completion hook.
|
||||
- `AUTHOR` section in default app help template.
|
||||
- Contribution guidelines.
|
||||
- `DurationFlag` type.
|
||||
|
||||
## [1.2.0] - 2014-08-02
|
||||
### Added
|
||||
- Support for environment variable defaults on flags plus tests.
|
||||
|
||||
## [1.1.0] - 2014-07-15
|
||||
### Added
|
||||
- Bash completion.
|
||||
- Optional hiding of built-in help command.
|
||||
- Optional skipping of flag parsing at command level.
|
||||
- `Author`, `Email`, and `Compiled` metadata on app.
|
||||
- `Before` hook func support at app and command level.
|
||||
- `CommandNotFound` func support at app level.
|
||||
- Command reference available on context.
|
||||
- `GenericFlag` type.
|
||||
- `Float64Flag` type.
|
||||
- `BoolTFlag` type.
|
||||
- `IsSet` flag helper on context.
|
||||
- More flag lookup funcs at context level.
|
||||
- More tests & docs.
|
||||
|
||||
### Changed
|
||||
- Help template updates to account for presence/absence of flags.
|
||||
- Separated subcommand help template.
|
||||
- Exposed `HelpPrinter` var for more control over help output.
|
||||
|
||||
## [1.0.0] - 2013-11-01
|
||||
### Added
|
||||
- `help` flag in default app flag set and each command flag set.
|
||||
- Custom handling of argument parsing errors.
|
||||
- Command lookup by name at app level.
|
||||
- `StringSliceFlag` type and supporting `StringSlice` type.
|
||||
- `IntSliceFlag` type and supporting `IntSlice` type.
|
||||
- Slice type flag lookups by name at context level.
|
||||
- Export of app and command help functions.
|
||||
- More tests & docs.
|
||||
|
||||
## 0.1.0 - 2013-07-22
|
||||
### Added
|
||||
- Initial implementation.
|
||||
|
||||
[Unreleased]: https://github.com/urfave/cli/compare/v1.18.0...HEAD
|
||||
[1.18.0]: https://github.com/urfave/cli/compare/v1.17.0...v1.18.0
|
||||
[1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0
|
||||
[1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0
|
||||
[1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0
|
||||
[1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0
|
||||
[1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0
|
||||
[1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0
|
||||
[1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1
|
||||
[1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0
|
||||
[1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2
|
||||
[1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1
|
||||
[1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0
|
||||
[1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0
|
||||
[1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0
|
||||
[1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1
|
||||
[1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0
|
||||
[1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0
|
||||
[1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0
|
||||
[1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1
|
||||
[1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0
|
||||
[1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1
|
||||
[1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0
|
||||
[1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0
|
||||
[1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0
|
||||
[1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Jeremy Saenz & Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+1448
File diff suppressed because it is too large
Load Diff
+3
@@ -0,0 +1,3 @@
|
||||
package altsrc
|
||||
|
||||
//go:generate python ../generate-flag-types altsrc -i ../flag-types.json -o flag_generated.go
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
package altsrc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// FlagInputSourceExtension is an extension interface of cli.Flag that
|
||||
// allows a value to be set on the existing parsed flags.
|
||||
type FlagInputSourceExtension interface {
|
||||
cli.Flag
|
||||
ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error
|
||||
}
|
||||
|
||||
// ApplyInputSourceValues iterates over all provided flags and
|
||||
// executes ApplyInputSourceValue on flags implementing the
|
||||
// FlagInputSourceExtension interface to initialize these flags
|
||||
// to an alternate input source.
|
||||
func ApplyInputSourceValues(context *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error {
|
||||
for _, f := range flags {
|
||||
inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension)
|
||||
if isType {
|
||||
err := inputSourceExtendedFlag.ApplyInputSourceValue(context, inputSourceContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitInputSource is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new
|
||||
// input source based on the func provided. If there is no error it will then apply the new input source to any flags
|
||||
// that are supported by the input source
|
||||
func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc {
|
||||
return func(context *cli.Context) error {
|
||||
inputSource, err := createInputSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error())
|
||||
}
|
||||
|
||||
return ApplyInputSourceValues(context, inputSource, flags)
|
||||
}
|
||||
}
|
||||
|
||||
// InitInputSourceWithContext is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new
|
||||
// input source based on the func provided with potentially using existing cli.Context values to initialize itself. If there is
|
||||
// no error it will then apply the new input source to any flags that are supported by the input source
|
||||
func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(context *cli.Context) (InputSourceContext, error)) cli.BeforeFunc {
|
||||
return func(context *cli.Context) error {
|
||||
inputSource, err := createInputSource(context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error())
|
||||
}
|
||||
|
||||
return ApplyInputSourceValues(context, inputSource, flags)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a generic value to the flagSet if required
|
||||
func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
|
||||
value, err := isc.Generic(f.GenericFlag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value != nil {
|
||||
eachName(f.Name, func(name string) {
|
||||
f.set.Set(f.Name, value.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a StringSlice value to the flagSet if required
|
||||
func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
|
||||
value, err := isc.StringSlice(f.StringSliceFlag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value != nil {
|
||||
var sliceValue cli.StringSlice = value
|
||||
eachName(f.Name, func(name string) {
|
||||
underlyingFlag := f.set.Lookup(f.Name)
|
||||
if underlyingFlag != nil {
|
||||
underlyingFlag.Value = &sliceValue
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a IntSlice value if required
|
||||
func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
|
||||
value, err := isc.IntSlice(f.IntSliceFlag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value != nil {
|
||||
var sliceValue cli.IntSlice = value
|
||||
eachName(f.Name, func(name string) {
|
||||
underlyingFlag := f.set.Lookup(f.Name)
|
||||
if underlyingFlag != nil {
|
||||
underlyingFlag.Value = &sliceValue
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a Bool value to the flagSet if required
|
||||
func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
|
||||
value, err := isc.Bool(f.BoolFlag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value {
|
||||
eachName(f.Name, func(name string) {
|
||||
f.set.Set(f.Name, strconv.FormatBool(value))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a BoolT value to the flagSet if required
|
||||
func (f *BoolTFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !context.IsSet(f.Name) && !isEnvVarSet(f.EnvVar) {
|
||||
value, err := isc.BoolT(f.BoolTFlag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !value {
|
||||
eachName(f.Name, func(name string) {
|
||||
f.set.Set(f.Name, strconv.FormatBool(value))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a String value to the flagSet if required
|
||||
func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) {
|
||||
value, err := isc.String(f.StringFlag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value != "" {
|
||||
eachName(f.Name, func(name string) {
|
||||
f.set.Set(f.Name, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a int value to the flagSet if required
|
||||
func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) {
|
||||
value, err := isc.Int(f.IntFlag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value > 0 {
|
||||
eachName(f.Name, func(name string) {
|
||||
f.set.Set(f.Name, strconv.FormatInt(int64(value), 10))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a Duration value to the flagSet if required
|
||||
func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) {
|
||||
value, err := isc.Duration(f.DurationFlag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value > 0 {
|
||||
eachName(f.Name, func(name string) {
|
||||
f.set.Set(f.Name, value.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyInputSourceValue applies a Float64 value to the flagSet if required
|
||||
func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourceContext) error {
|
||||
if f.set != nil {
|
||||
if !(context.IsSet(f.Name) || isEnvVarSet(f.EnvVar)) {
|
||||
value, err := isc.Float64(f.Float64Flag.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value > 0 {
|
||||
floatStr := float64ToString(value)
|
||||
eachName(f.Name, func(name string) {
|
||||
f.set.Set(f.Name, floatStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isEnvVarSet(envVars string) bool {
|
||||
for _, envVar := range strings.Split(envVars, ",") {
|
||||
envVar = strings.TrimSpace(envVar)
|
||||
if _, ok := syscall.Getenv(envVar); ok {
|
||||
// TODO: Can't use this for bools as
|
||||
// set means that it was true or false based on
|
||||
// Bool flag type, should work for other types
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func float64ToString(f float64) string {
|
||||
return fmt.Sprintf("%v", f)
|
||||
}
|
||||
|
||||
func eachName(longName string, fn func(string)) {
|
||||
parts := strings.Split(longName, ",")
|
||||
for _, name := range parts {
|
||||
name = strings.Trim(name, " ")
|
||||
fn(name)
|
||||
}
|
||||
}
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
package altsrc
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// WARNING: This file is generated!
|
||||
|
||||
// BoolFlag is the flag type that wraps cli.BoolFlag to allow
|
||||
// for other values to be specified
|
||||
type BoolFlag struct {
|
||||
cli.BoolFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewBoolFlag creates a new BoolFlag
|
||||
func NewBoolFlag(fl cli.BoolFlag) *BoolFlag {
|
||||
return &BoolFlag{BoolFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped BoolFlag.Apply
|
||||
func (f *BoolFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.BoolFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped BoolFlag.ApplyWithError
|
||||
func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.BoolFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// BoolTFlag is the flag type that wraps cli.BoolTFlag to allow
|
||||
// for other values to be specified
|
||||
type BoolTFlag struct {
|
||||
cli.BoolTFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewBoolTFlag creates a new BoolTFlag
|
||||
func NewBoolTFlag(fl cli.BoolTFlag) *BoolTFlag {
|
||||
return &BoolTFlag{BoolTFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped BoolTFlag.Apply
|
||||
func (f *BoolTFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.BoolTFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped BoolTFlag.ApplyWithError
|
||||
func (f *BoolTFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.BoolTFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// DurationFlag is the flag type that wraps cli.DurationFlag to allow
|
||||
// for other values to be specified
|
||||
type DurationFlag struct {
|
||||
cli.DurationFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewDurationFlag creates a new DurationFlag
|
||||
func NewDurationFlag(fl cli.DurationFlag) *DurationFlag {
|
||||
return &DurationFlag{DurationFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped DurationFlag.Apply
|
||||
func (f *DurationFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.DurationFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped DurationFlag.ApplyWithError
|
||||
func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.DurationFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// Float64Flag is the flag type that wraps cli.Float64Flag to allow
|
||||
// for other values to be specified
|
||||
type Float64Flag struct {
|
||||
cli.Float64Flag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewFloat64Flag creates a new Float64Flag
|
||||
func NewFloat64Flag(fl cli.Float64Flag) *Float64Flag {
|
||||
return &Float64Flag{Float64Flag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Float64Flag.Apply
|
||||
func (f *Float64Flag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.Float64Flag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Float64Flag.ApplyWithError
|
||||
func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.Float64Flag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// GenericFlag is the flag type that wraps cli.GenericFlag to allow
|
||||
// for other values to be specified
|
||||
type GenericFlag struct {
|
||||
cli.GenericFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewGenericFlag creates a new GenericFlag
|
||||
func NewGenericFlag(fl cli.GenericFlag) *GenericFlag {
|
||||
return &GenericFlag{GenericFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped GenericFlag.Apply
|
||||
func (f *GenericFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.GenericFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped GenericFlag.ApplyWithError
|
||||
func (f *GenericFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.GenericFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// Int64Flag is the flag type that wraps cli.Int64Flag to allow
|
||||
// for other values to be specified
|
||||
type Int64Flag struct {
|
||||
cli.Int64Flag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewInt64Flag creates a new Int64Flag
|
||||
func NewInt64Flag(fl cli.Int64Flag) *Int64Flag {
|
||||
return &Int64Flag{Int64Flag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Int64Flag.Apply
|
||||
func (f *Int64Flag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.Int64Flag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Int64Flag.ApplyWithError
|
||||
func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.Int64Flag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// IntFlag is the flag type that wraps cli.IntFlag to allow
|
||||
// for other values to be specified
|
||||
type IntFlag struct {
|
||||
cli.IntFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewIntFlag creates a new IntFlag
|
||||
func NewIntFlag(fl cli.IntFlag) *IntFlag {
|
||||
return &IntFlag{IntFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped IntFlag.Apply
|
||||
func (f *IntFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.IntFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped IntFlag.ApplyWithError
|
||||
func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.IntFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow
|
||||
// for other values to be specified
|
||||
type IntSliceFlag struct {
|
||||
cli.IntSliceFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewIntSliceFlag creates a new IntSliceFlag
|
||||
func NewIntSliceFlag(fl cli.IntSliceFlag) *IntSliceFlag {
|
||||
return &IntSliceFlag{IntSliceFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped IntSliceFlag.Apply
|
||||
func (f *IntSliceFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.IntSliceFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped IntSliceFlag.ApplyWithError
|
||||
func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.IntSliceFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow
|
||||
// for other values to be specified
|
||||
type Int64SliceFlag struct {
|
||||
cli.Int64SliceFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewInt64SliceFlag creates a new Int64SliceFlag
|
||||
func NewInt64SliceFlag(fl cli.Int64SliceFlag) *Int64SliceFlag {
|
||||
return &Int64SliceFlag{Int64SliceFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Int64SliceFlag.Apply
|
||||
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.Int64SliceFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Int64SliceFlag.ApplyWithError
|
||||
func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.Int64SliceFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// StringFlag is the flag type that wraps cli.StringFlag to allow
|
||||
// for other values to be specified
|
||||
type StringFlag struct {
|
||||
cli.StringFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewStringFlag creates a new StringFlag
|
||||
func NewStringFlag(fl cli.StringFlag) *StringFlag {
|
||||
return &StringFlag{StringFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped StringFlag.Apply
|
||||
func (f *StringFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.StringFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped StringFlag.ApplyWithError
|
||||
func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.StringFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow
|
||||
// for other values to be specified
|
||||
type StringSliceFlag struct {
|
||||
cli.StringSliceFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewStringSliceFlag creates a new StringSliceFlag
|
||||
func NewStringSliceFlag(fl cli.StringSliceFlag) *StringSliceFlag {
|
||||
return &StringSliceFlag{StringSliceFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped StringSliceFlag.Apply
|
||||
func (f *StringSliceFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.StringSliceFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped StringSliceFlag.ApplyWithError
|
||||
func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.StringSliceFlag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// Uint64Flag is the flag type that wraps cli.Uint64Flag to allow
|
||||
// for other values to be specified
|
||||
type Uint64Flag struct {
|
||||
cli.Uint64Flag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewUint64Flag creates a new Uint64Flag
|
||||
func NewUint64Flag(fl cli.Uint64Flag) *Uint64Flag {
|
||||
return &Uint64Flag{Uint64Flag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Uint64Flag.Apply
|
||||
func (f *Uint64Flag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.Uint64Flag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Uint64Flag.ApplyWithError
|
||||
func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.Uint64Flag.ApplyWithError(set)
|
||||
}
|
||||
|
||||
// UintFlag is the flag type that wraps cli.UintFlag to allow
|
||||
// for other values to be specified
|
||||
type UintFlag struct {
|
||||
cli.UintFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewUintFlag creates a new UintFlag
|
||||
func NewUintFlag(fl cli.UintFlag) *UintFlag {
|
||||
return &UintFlag{UintFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped UintFlag.Apply
|
||||
func (f *UintFlag) Apply(set *flag.FlagSet) {
|
||||
f.set = set
|
||||
f.UintFlag.Apply(set)
|
||||
}
|
||||
|
||||
// ApplyWithError saves the flagSet for later usage calls, then calls the
|
||||
// wrapped UintFlag.ApplyWithError
|
||||
func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.UintFlag.ApplyWithError(set)
|
||||
}
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
package altsrc
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
type testApplyInputSource struct {
|
||||
Flag FlagInputSourceExtension
|
||||
FlagName string
|
||||
FlagSetName string
|
||||
Expected string
|
||||
ContextValueString string
|
||||
ContextValue flag.Value
|
||||
EnvVarValue string
|
||||
EnvVarName string
|
||||
MapValue interface{}
|
||||
}
|
||||
|
||||
func TestGenericApplyInputSourceValue(t *testing.T) {
|
||||
v := &Parser{"abc", "def"}
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}}),
|
||||
FlagName: "test",
|
||||
MapValue: v,
|
||||
})
|
||||
expect(t, v, c.Generic("test"))
|
||||
}
|
||||
|
||||
func TestGenericApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
p := &Parser{"abc", "def"}
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}}),
|
||||
FlagName: "test",
|
||||
MapValue: &Parser{"efg", "hig"},
|
||||
ContextValueString: p.String(),
|
||||
})
|
||||
expect(t, p, c.Generic("test"))
|
||||
}
|
||||
|
||||
func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewGenericFlag(cli.GenericFlag{Name: "test", Value: &Parser{}, EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: &Parser{"efg", "hij"},
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: "abc,def",
|
||||
})
|
||||
expect(t, &Parser{"abc", "def"}, c.Generic("test"))
|
||||
}
|
||||
|
||||
func TestStringSliceApplyInputSourceValue(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: []interface{}{"hello", "world"},
|
||||
})
|
||||
expect(t, c.StringSlice("test"), []string{"hello", "world"})
|
||||
}
|
||||
|
||||
func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: []interface{}{"hello", "world"},
|
||||
ContextValueString: "ohno",
|
||||
})
|
||||
expect(t, c.StringSlice("test"), []string{"ohno"})
|
||||
}
|
||||
|
||||
func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test", EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: []interface{}{"hello", "world"},
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: "oh,no",
|
||||
})
|
||||
expect(t, c.StringSlice("test"), []string{"oh", "no"})
|
||||
}
|
||||
|
||||
func TestIntSliceApplyInputSourceValue(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: []interface{}{1, 2},
|
||||
})
|
||||
expect(t, c.IntSlice("test"), []int{1, 2})
|
||||
}
|
||||
|
||||
func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: []interface{}{1, 2},
|
||||
ContextValueString: "3",
|
||||
})
|
||||
expect(t, c.IntSlice("test"), []int{3})
|
||||
}
|
||||
|
||||
func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test", EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: []interface{}{1, 2},
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: "3,4",
|
||||
})
|
||||
expect(t, c.IntSlice("test"), []int{3, 4})
|
||||
}
|
||||
|
||||
func TestBoolApplyInputSourceMethodSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewBoolFlag(cli.BoolFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: true,
|
||||
})
|
||||
expect(t, true, c.Bool("test"))
|
||||
}
|
||||
|
||||
func TestBoolApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewBoolFlag(cli.BoolFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: false,
|
||||
ContextValueString: "true",
|
||||
})
|
||||
expect(t, true, c.Bool("test"))
|
||||
}
|
||||
|
||||
func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewBoolFlag(cli.BoolFlag{Name: "test", EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: false,
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: "true",
|
||||
})
|
||||
expect(t, true, c.Bool("test"))
|
||||
}
|
||||
|
||||
func TestBoolTApplyInputSourceMethodSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: false,
|
||||
})
|
||||
expect(t, false, c.BoolT("test"))
|
||||
}
|
||||
|
||||
func TestBoolTApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: true,
|
||||
ContextValueString: "false",
|
||||
})
|
||||
expect(t, false, c.BoolT("test"))
|
||||
}
|
||||
|
||||
func TestBoolTApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewBoolTFlag(cli.BoolTFlag{Name: "test", EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: true,
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: "false",
|
||||
})
|
||||
expect(t, false, c.BoolT("test"))
|
||||
}
|
||||
|
||||
func TestStringApplyInputSourceMethodSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewStringFlag(cli.StringFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: "hello",
|
||||
})
|
||||
expect(t, "hello", c.String("test"))
|
||||
}
|
||||
|
||||
func TestStringApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewStringFlag(cli.StringFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: "hello",
|
||||
ContextValueString: "goodbye",
|
||||
})
|
||||
expect(t, "goodbye", c.String("test"))
|
||||
}
|
||||
|
||||
func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewStringFlag(cli.StringFlag{Name: "test", EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: "hello",
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: "goodbye",
|
||||
})
|
||||
expect(t, "goodbye", c.String("test"))
|
||||
}
|
||||
|
||||
func TestIntApplyInputSourceMethodSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewIntFlag(cli.IntFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: 15,
|
||||
})
|
||||
expect(t, 15, c.Int("test"))
|
||||
}
|
||||
|
||||
func TestIntApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewIntFlag(cli.IntFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: 15,
|
||||
ContextValueString: "7",
|
||||
})
|
||||
expect(t, 7, c.Int("test"))
|
||||
}
|
||||
|
||||
func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewIntFlag(cli.IntFlag{Name: "test", EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: 15,
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: "12",
|
||||
})
|
||||
expect(t, 12, c.Int("test"))
|
||||
}
|
||||
|
||||
func TestDurationApplyInputSourceMethodSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewDurationFlag(cli.DurationFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: time.Duration(30 * time.Second),
|
||||
})
|
||||
expect(t, time.Duration(30*time.Second), c.Duration("test"))
|
||||
}
|
||||
|
||||
func TestDurationApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewDurationFlag(cli.DurationFlag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: time.Duration(30 * time.Second),
|
||||
ContextValueString: time.Duration(15 * time.Second).String(),
|
||||
})
|
||||
expect(t, time.Duration(15*time.Second), c.Duration("test"))
|
||||
}
|
||||
|
||||
func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewDurationFlag(cli.DurationFlag{Name: "test", EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: time.Duration(30 * time.Second),
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: time.Duration(15 * time.Second).String(),
|
||||
})
|
||||
expect(t, time.Duration(15*time.Second), c.Duration("test"))
|
||||
}
|
||||
|
||||
func TestFloat64ApplyInputSourceMethodSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewFloat64Flag(cli.Float64Flag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: 1.3,
|
||||
})
|
||||
expect(t, 1.3, c.Float64("test"))
|
||||
}
|
||||
|
||||
func TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewFloat64Flag(cli.Float64Flag{Name: "test"}),
|
||||
FlagName: "test",
|
||||
MapValue: 1.3,
|
||||
ContextValueString: fmt.Sprintf("%v", 1.4),
|
||||
})
|
||||
expect(t, 1.4, c.Float64("test"))
|
||||
}
|
||||
|
||||
func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) {
|
||||
c := runTest(t, testApplyInputSource{
|
||||
Flag: NewFloat64Flag(cli.Float64Flag{Name: "test", EnvVar: "TEST"}),
|
||||
FlagName: "test",
|
||||
MapValue: 1.3,
|
||||
EnvVarName: "TEST",
|
||||
EnvVarValue: fmt.Sprintf("%v", 1.4),
|
||||
})
|
||||
expect(t, 1.4, c.Float64("test"))
|
||||
}
|
||||
|
||||
func runTest(t *testing.T, test testApplyInputSource) *cli.Context {
|
||||
inputSource := &MapInputSource{valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue}}
|
||||
set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError)
|
||||
c := cli.NewContext(nil, set, nil)
|
||||
if test.EnvVarName != "" && test.EnvVarValue != "" {
|
||||
os.Setenv(test.EnvVarName, test.EnvVarValue)
|
||||
defer os.Setenv(test.EnvVarName, "")
|
||||
}
|
||||
|
||||
test.Flag.Apply(set)
|
||||
if test.ContextValue != nil {
|
||||
flag := set.Lookup(test.FlagName)
|
||||
flag.Value = test.ContextValue
|
||||
}
|
||||
if test.ContextValueString != "" {
|
||||
set.Set(test.FlagName, test.ContextValueString)
|
||||
}
|
||||
test.Flag.ApplyInputSourceValue(c, inputSource)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type Parser [2]string
|
||||
|
||||
func (p *Parser) Set(value string) error {
|
||||
parts := strings.Split(value, ",")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid format")
|
||||
}
|
||||
|
||||
(*p)[0] = parts[0]
|
||||
(*p)[1] = parts[1]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) String() string {
|
||||
return fmt.Sprintf("%s,%s", p[0], p[1])
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package altsrc
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func expect(t *testing.T, a interface{}, b interface{}) {
|
||||
if !reflect.DeepEqual(b, a) {
|
||||
t.Errorf("Expected %#v (type %v) - Got %#v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
|
||||
func refute(t *testing.T, a interface{}, b interface{}) {
|
||||
if a == b {
|
||||
t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package altsrc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// InputSourceContext is an interface used to allow
|
||||
// other input sources to be implemented as needed.
|
||||
type InputSourceContext interface {
|
||||
Int(name string) (int, error)
|
||||
Duration(name string) (time.Duration, error)
|
||||
Float64(name string) (float64, error)
|
||||
String(name string) (string, error)
|
||||
StringSlice(name string) ([]string, error)
|
||||
IntSlice(name string) ([]int, error)
|
||||
Generic(name string) (cli.Generic, error)
|
||||
Bool(name string) (bool, error)
|
||||
BoolT(name string) (bool, error)
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
package altsrc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/urfave/cli.v1"
|
||||
)
|
||||
|
||||
// MapInputSource implements InputSourceContext to return
|
||||
// data from the map that is loaded.
|
||||
type MapInputSource struct {
|
||||
valueMap map[interface{}]interface{}
|
||||
}
|
||||
|
||||
// nestedVal checks if the name has '.' delimiters.
|
||||
// If so, it tries to traverse the tree by the '.' delimited sections to find
|
||||
// a nested value for the key.
|
||||
func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool) {
|
||||
if sections := strings.Split(name, "."); len(sections) > 1 {
|
||||
node := tree
|
||||
for _, section := range sections[:len(sections)-1] {
|
||||
child, ok := node[section]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
ctype, ok := child.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
node = ctype
|
||||
}
|
||||
if val, ok := node[sections[len(sections)-1]]; ok {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Int returns an int from the map if it exists otherwise returns 0
|
||||
func (fsm *MapInputSource) Int(name string) (int, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if exists {
|
||||
otherValue, isType := otherGenericValue.(int)
|
||||
if !isType {
|
||||
return 0, incorrectTypeForFlagError(name, "int", otherGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
|
||||
if exists {
|
||||
otherValue, isType := nestedGenericValue.(int)
|
||||
if !isType {
|
||||
return 0, incorrectTypeForFlagError(name, "int", nestedGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Duration returns a duration from the map if it exists otherwise returns 0
|
||||
func (fsm *MapInputSource) Duration(name string) (time.Duration, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if exists {
|
||||
otherValue, isType := otherGenericValue.(time.Duration)
|
||||
if !isType {
|
||||
return 0, incorrectTypeForFlagError(name, "duration", otherGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
|
||||
if exists {
|
||||
otherValue, isType := nestedGenericValue.(time.Duration)
|
||||
if !isType {
|
||||
return 0, incorrectTypeForFlagError(name, "duration", nestedGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Float64 returns an float64 from the map if it exists otherwise returns 0
|
||||
func (fsm *MapInputSource) Float64(name string) (float64, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if exists {
|
||||
otherValue, isType := otherGenericValue.(float64)
|
||||
if !isType {
|
||||
return 0, incorrectTypeForFlagError(name, "float64", otherGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
|
||||
if exists {
|
||||
otherValue, isType := nestedGenericValue.(float64)
|
||||
if !isType {
|
||||
return 0, incorrectTypeForFlagError(name, "float64", nestedGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// String returns a string from the map if it exists otherwise returns an empty string
|
||||
func (fsm *MapInputSource) String(name string) (string, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if exists {
|
||||
otherValue, isType := otherGenericValue.(string)
|
||||
if !isType {
|
||||
return "", incorrectTypeForFlagError(name, "string", otherGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
|
||||
if exists {
|
||||
otherValue, isType := nestedGenericValue.(string)
|
||||
if !isType {
|
||||
return "", incorrectTypeForFlagError(name, "string", nestedGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// StringSlice returns an []string from the map if it exists otherwise returns nil
|
||||
func (fsm *MapInputSource) StringSlice(name string) ([]string, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if !exists {
|
||||
otherGenericValue, exists = nestedVal(name, fsm.valueMap)
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
otherValue, isType := otherGenericValue.([]interface{})
|
||||
if !isType {
|
||||
return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue)
|
||||
}
|
||||
|
||||
var stringSlice = make([]string, 0, len(otherValue))
|
||||
for i, v := range otherValue {
|
||||
stringValue, isType := v.(string)
|
||||
|
||||
if !isType {
|
||||
return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "string", v)
|
||||
}
|
||||
|
||||
stringSlice = append(stringSlice, stringValue)
|
||||
}
|
||||
|
||||
return stringSlice, nil
|
||||
}
|
||||
|
||||
// IntSlice returns an []int from the map if it exists otherwise returns nil
|
||||
func (fsm *MapInputSource) IntSlice(name string) ([]int, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if !exists {
|
||||
otherGenericValue, exists = nestedVal(name, fsm.valueMap)
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
otherValue, isType := otherGenericValue.([]interface{})
|
||||
if !isType {
|
||||
return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue)
|
||||
}
|
||||
|
||||
var intSlice = make([]int, 0, len(otherValue))
|
||||
for i, v := range otherValue {
|
||||
intValue, isType := v.(int)
|
||||
|
||||
if !isType {
|
||||
return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v)
|
||||
}
|
||||
|
||||
intSlice = append(intSlice, intValue)
|
||||
}
|
||||
|
||||
return intSlice, nil
|
||||
}
|
||||
|
||||
// Generic returns an cli.Generic from the map if it exists otherwise returns nil
|
||||
func (fsm *MapInputSource) Generic(name string) (cli.Generic, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if exists {
|
||||
otherValue, isType := otherGenericValue.(cli.Generic)
|
||||
if !isType {
|
||||
return nil, incorrectTypeForFlagError(name, "cli.Generic", otherGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
|
||||
if exists {
|
||||
otherValue, isType := nestedGenericValue.(cli.Generic)
|
||||
if !isType {
|
||||
return nil, incorrectTypeForFlagError(name, "cli.Generic", nestedGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Bool returns an bool from the map otherwise returns false
|
||||
func (fsm *MapInputSource) Bool(name string) (bool, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if exists {
|
||||
otherValue, isType := otherGenericValue.(bool)
|
||||
if !isType {
|
||||
return false, incorrectTypeForFlagError(name, "bool", otherGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
|
||||
if exists {
|
||||
otherValue, isType := nestedGenericValue.(bool)
|
||||
if !isType {
|
||||
return false, incorrectTypeForFlagError(name, "bool", nestedGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// BoolT returns an bool from the map otherwise returns true
|
||||
func (fsm *MapInputSource) BoolT(name string) (bool, error) {
|
||||
otherGenericValue, exists := fsm.valueMap[name]
|
||||
if exists {
|
||||
otherValue, isType := otherGenericValue.(bool)
|
||||
if !isType {
|
||||
return true, incorrectTypeForFlagError(name, "bool", otherGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
nestedGenericValue, exists := nestedVal(name, fsm.valueMap)
|
||||
if exists {
|
||||
otherValue, isType := nestedGenericValue.(bool)
|
||||
if !isType {
|
||||
return true, incorrectTypeForFlagError(name, "bool", nestedGenericValue)
|
||||
}
|
||||
return otherValue, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error {
|
||||
valueType := reflect.TypeOf(value)
|
||||
valueTypeName := ""
|
||||
if valueType != nil {
|
||||
valueTypeName = valueType.Name()
|
||||
}
|
||||
|
||||
return fmt.Errorf("Mismatched type for flag '%s'. Expected '%s' but actual is '%s'", name, expectedTypeName, valueTypeName)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user