diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..233200f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,476 @@ +# Agent rules for generation: +# https://arran4.github.io/blog/post/2026/006-github-ci-and-deploy/ +# Built using this post as a reference/guide. +name: CI/CD + +on: + push: + branches: [main, master] + # semantic version tags + rc/beta snapshots + tags: + - 'v*' + - 'v*.*.*' + - 'v*.*.*-rc*' + - 'v*.*.*-beta*' + - 'test-*' + pull_request: + types: [opened, synchronize, reopened, ready_for_review, closed] + branches: [main, master] + release: + types: [published] + workflow_dispatch: + inputs: + mode: + description: "Pipeline mode" + required: true + default: "lint-fix" + type: choice + options: + - lint-fix + - build + - release-major + - release-minor + - release-patch + - release-test + - release-rc + - release-alpha + - monthly-maintenance + release_version_override: + description: "Optional explicit release version (for example 2.4.0 or 2.4.0-rc.2)" + required: false + default: "" + type: string + allow_prs: + description: "Allow automation to open pull requests" + required: false + default: true + type: boolean + schedule: + # preferred heavy monthly run (quota reset strategy) + - cron: '17 3 1 * *' + # optional nightly lightweight checks + - cron: '41 2 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: write + discussions: write + pull-requests: write + checks: write + packages: write + security-events: write + +jobs: + route: + name: Route event + runs-on: ubuntu-latest + outputs: + run_code_checks: ${{ steps.route.outputs.run_code_checks }} + run_pr_meta_checks: ${{ steps.route.outputs.run_pr_meta_checks }} + run_cleanup: ${{ steps.route.outputs.run_cleanup }} + run_release: ${{ steps.route.outputs.run_release }} + is_monthly: ${{ steps.route.outputs.is_monthly }} + is_nightly: ${{ steps.route.outputs.is_nightly }} + steps: + - id: route + shell: bash + run: | + set -euo pipefail + + run_code_checks=false + run_pr_meta_checks=false + run_cleanup=false + run_release=false + is_monthly=false + is_nightly=false + + case "${{ github.event_name }}" in + push) + run_code_checks=true + ;; + pull_request) + if [[ "${{ github.event.action }}" == "closed" ]]; then + run_cleanup=true + else + run_pr_meta_checks=true + # In practice, also run code checks on PRs so lint/fmt/vet/test + # show up directly in the PR UI. Use concurrency to collapse churn. + run_code_checks=true + fi + ;; + release) + run_release=true + ;; + workflow_dispatch) + run_code_checks=true + if [[ "${{ inputs.mode }}" == release-* ]]; then + run_release=true + fi + if [[ "${{ inputs.mode }}" == "monthly-maintenance" ]]; then + is_monthly=true + fi + if [[ "${{ inputs.mode }}" == "lint-fix" ]]; then + # Manual lint-fix acts as an on-demand nightly-style maintenance pass. + is_nightly=true + fi + ;; + schedule) + run_code_checks=true + if [[ "${{ github.event.schedule }}" == "17 3 1 * *" ]]; then + is_monthly=true + fi + if [[ "${{ github.event.schedule }}" == "41 2 * * *" ]]; then + is_nightly=true + fi + ;; + esac + + echo "run_code_checks=$run_code_checks" >> "$GITHUB_OUTPUT" + echo "run_pr_meta_checks=$run_pr_meta_checks" >> "$GITHUB_OUTPUT" + echo "run_cleanup=$run_cleanup" >> "$GITHUB_OUTPUT" + echo "run_release=$run_release" >> "$GITHUB_OUTPUT" + echo "is_monthly=$is_monthly" >> "$GITHUB_OUTPUT" + echo "is_nightly=$is_nightly" >> "$GITHUB_OUTPUT" + + prepare-release-tag: + name: Prepare release tag + needs: [route] + if: ${{ github.event_name == 'workflow_dispatch' && startsWith(inputs.mode, 'release-') }} + runs-on: ubuntu-latest + outputs: + release_tag: ${{ steps.tag.outputs.release_tag }} + next_version: ${{ steps.tag.outputs.next_version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Setup git-tag-inc + uses: arran4/git-tag-inc-action@v1 + with: + mode: install + - id: tag + shell: bash + run: | + set -euo pipefail + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + MODE="${{ inputs.mode }}" + OVERRIDE="${{ inputs.release_version_override }}" + + if [[ -n "$OVERRIDE" ]]; then + # Accept "1.2.3" or "v1.2.3" override input. + OVERRIDE="${OVERRIDE#v}" + next_tag="v$OVERRIDE" + else + case "$MODE" in + release-major) level="major"; suffix="" ;; + release-minor) level="minor"; suffix="" ;; + release-patch) level="patch"; suffix="" ;; + release-test) level="patch"; suffix="test" ;; + release-rc) level="patch"; suffix="rc" ;; + release-alpha) level="patch"; suffix="alpha" ;; + *) echo "Unsupported release mode: $MODE"; exit 1 ;; + esac + if command -v git-tag-inc >/dev/null 2>&1; then + # git-tag-inc uses positional commands (patch/major/minor/test/rc...) + # and NOT flag forms like -patch. + level="${level#-}" + args=(-print-version-only "$level") + [[ -n "$suffix" ]] && args+=("$suffix") + next_tag=$(git-tag-inc "${args[@]}") + else + # Fallback implementation when git-tag-inc is not available. + git fetch --tags --force + latest=$(git tag -l 'v*' | sed 's/^v//' | sort -V | tail -n 1) + [[ -z "$latest" ]] && latest='0.0.0' + + # Prefer npx semver if available (same pattern used in g2 fixes). + if command -v npx >/dev/null 2>&1; then + case "$level" in + major) bumped=$(npx --yes semver "$latest" -i major) ;; + minor) bumped=$(npx --yes semver "$latest" -i minor) ;; + *) bumped=$(npx --yes semver "$latest" -i patch) ;; + esac + next_tag="v${bumped}" + else + base="${latest%%-*}" + IFS='.' read -r maj min pat <<< "$base" + case "$level" in + major) maj=$((maj+1)); min=0; pat=0 ;; + minor) min=$((min+1)); pat=0 ;; + *) pat=$((pat+1)) ;; + esac + next_tag="v${maj}.${min}.${pat}" + fi + + if [[ -n "$suffix" ]]; then + next_tag="${next_tag}-${suffix}.1" + fi + fi + fi + + # Tagging safety guards to avoid duplicate/invalid release states. + [[ "$next_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]] || { + echo "Invalid tag format: $next_tag" >&2 + exit 1 + } + git fetch --tags --force + if git rev-parse "$next_tag" >/dev/null 2>&1; then + echo "Tag already exists: $next_tag" >&2 + echo "Choose a new mode or set release_version_override." >&2 + exit 1 + fi + + echo "release_tag=$next_tag" >> "$GITHUB_OUTPUT" + clean_tag="${next_tag#v}"; clean_tag="${clean_tag%%-*}" + IFS='.' read -r maj min pat <<< "$clean_tag" + echo "next_version=${maj:-0}.${min:-0}.$(( ${pat:-0} + 1 ))-SNAPSHOT" >> "$GITHUB_OUTPUT" + + discover: + name: Discover capabilities and cost profile + needs: route + runs-on: ubuntu-latest + outputs: + profile: ${{ steps.profile.outputs.profile }} + has_go: ${{ steps.detect.outputs.has_go }} + has_goreleaser: ${{ steps.detect.outputs.has_goreleaser }} + steps: + - uses: actions/checkout@v6 + + - id: detect + shell: bash + run: | + set -euo pipefail + # Template-time toggles + EXPECT_GO=true + EXPECT_GORELEASER=true + + echo "has_go=${EXPECT_GO:-false}" >> "$GITHUB_OUTPUT" + echo "has_goreleaser=${EXPECT_GORELEASER:-false}" >> "$GITHUB_OUTPUT" + + - id: profile + shell: bash + run: | + set -euo pipefail + # repo visibility is authoritative + if [[ "${{ github.event.repository.private }}" == "true" ]]; then + echo "profile=private" >> "$GITHUB_OUTPUT" + else + echo "profile=public" >> "$GITHUB_OUTPUT" + fi + + gitleaks: + name: Secret scan + needs: [route, discover] + if: ${{ needs.route.outputs.run_cleanup != 'true' && (needs.route.outputs.is_nightly == 'true' || needs.route.outputs.is_monthly == 'true') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + golangci: + name: lint + needs: [route, discover] + if: ${{ needs.discover.outputs.has_go == 'true' && needs.route.outputs.run_code_checks == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: latest + + go-test: + name: Go lint/test (${{ matrix.os }}) + needs: [route, discover, golangci] + if: ${{ needs.discover.outputs.has_go == 'true' && needs.route.outputs.run_code_checks == 'true' }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + - name: Go Test + run: go test -v ./... + + go-vet: + name: Go vet + needs: [route, discover] + if: ${{ needs.discover.outputs.has_go == 'true' && needs.route.outputs.run_code_checks == 'true' && needs.discover.outputs.profile == 'public' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + - run: go vet ./... + + go-fmt-pr: + name: go fmt -> PR (manual dispatch) + needs: [route, discover] + if: ${{ needs.discover.outputs.has_go == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'lint-fix' && inputs.allow_prs == true }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + - name: Run go fmt + run: go fmt ./... + - name: Create PR if go fmt changed files + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git diff --quiet && { echo "No fmt changes"; exit 0; } + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + BRANCH="ci/gofmt/${{ github.run_id }}" + git checkout -b "$BRANCH" + git add -A + git commit -m "ci: go fmt" + git push origin "$BRANCH" + gh pr create --title "ci: go fmt" --body "Automated go fmt from manual dispatch." --base main --head "$BRANCH" --label "ci-autofix" + + autofix: + name: Auto-format and open PR + needs: [route, discover] + if: ${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'lint-fix' && inputs.allow_prs == true }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Go (if needed) + if: ${{ needs.discover.outputs.has_go == 'true' }} + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Run autofix formatters + shell: bash + run: | + set -euo pipefail + if [[ "${{ needs.discover.outputs.has_go }}" == "true" ]]; then + go fix ./... || true + go fmt ./... || true + fi + + - name: Create PR if changes exist + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + if git diff --quiet; then + echo "No changes; exiting." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + PARENT_PR="${{ github.event.pull_request.number || 'none' }}" + BRANCH="ci/autofix/${{ github.run_id }}-parent-${PARENT_PR}" + + git checkout -b "$BRANCH" + git add -A + git commit -m "ci: automated formatting fixes" + git push origin "$BRANCH" + + gh pr create \ + --title "ci: automated formatting fixes" \ + --body "Automated formatting pass. Parent-PR: ${PARENT_PR}" \ + --base main \ + --head "$BRANCH" \ + --label "ci-autofix" + + cleanup-autofix-prs: + name: Cleanup autofix PRs on parent close + needs: [route] + if: ${{ needs.route.outputs.run_cleanup == 'true' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PARENT_PR: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + gh pr list --state open --search "label:ci-autofix in:title" --json number,headRefName,body | \ + jq -r '.[] | select(.body | contains("Parent-PR: '"$PARENT_PR"'")) | [.number, .headRefName] | @tsv' | \ + while IFS=$'\t' read -r pr branch; do + gh pr close "$pr" --comment "Closing auto-fix PR because parent PR #$PARENT_PR was closed." + git push origin --delete "$branch" || true + done + + goreleaser: + name: GoReleaser + # In practice, include all quality gates here (for example: go-test, go-vet, golangci). + needs: [route, discover, go-test, prepare-release-tag] + if: ${{ needs.discover.outputs.has_go == 'true' && needs.discover.outputs.has_goreleaser == 'true' && (((github.event_name == 'push') && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && startsWith(inputs.mode, 'release-'))) }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + - name: Tag commit for release (workflow_dispatch) + if: ${{ github.event_name == 'workflow_dispatch' && startsWith(inputs.mode, 'release-') }} + run: git tag ${{ needs.prepare-release-tag.outputs.release_tag }} + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: '~> v2' + args: >- + release --clean + ${{ (github.event_name == 'workflow_dispatch' && (inputs.mode == 'release-test' || inputs.mode == 'release-rc' || inputs.mode == 'release-alpha')) && '--snapshot' || '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_CURRENT_TAG: ${{ needs.prepare-release-tag.outputs.release_tag }} + + publish-draft: + name: Publish draft release assets + needs: + - goreleaser + if: ${{ !failure() && !cancelled() && needs.route.outputs.run_release == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Collect artifacts + uses: actions/download-artifact@v4 + with: + path: dist-release + - name: Publish draft GitHub release + uses: softprops/action-gh-release@v2 + with: + draft: true + files: dist-release/** + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + promote-release: + name: Promote draft to published + needs: [publish-draft] + if: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && startsWith(inputs.mode, 'release-')) }} + runs-on: ubuntu-latest + steps: + - name: Release promoted via upstream process + run: echo "Promotion step placeholder (gh api patch release draft=false)" diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..5053ce7 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,8 @@ +title = "repo gitleaks config" + +[allowlist] +description = "global allowlist" +paths = [ + '''^docs/''', + '''^testdata/''' +] diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..fec481e --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,28 @@ +project_name: mdtohtml + +before: + hooks: + - go mod tidy + +builds: + - id: app + binary: mdtohtml + main: ./ + env: + - CGO_ENABLED=0 + +archives: + - formats: [tar.gz] + format_overrides: + - goos: windows + formats: [zip] + +checksum: + name_template: checksums.txt + +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/Makefile b/Makefile deleted file mode 100644 index 999e42e..0000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -files=regexp.go gen.go css.go - -mdtohtml: main.go $(files) - go build -o mdtohtml main.go $(files) - -test: test.go mdtohtml - go build -o test test.go $(files) - ./test - -clean: - rm mdtohtml diff --git a/README.md b/README.md index 74b488b..ca18b5e 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,24 @@ Mdtohtml is a HTML generator from a Markdown file. This is implemented in Go. Th The syntax of Markdown follows [CommonMark](https://commonmark.org/) which version is [0.29 (2019-04-06)](https://spec.commonmark.org/). They have playground, [commonmark.js dingus](https://spec.commonmark.org/dingus/), for CommonMark grammar. +## Install + +### GitHub Releases +Download binaries from: https://github.com/mdtohtml/mdtohtml/releases + +### Go install +go install github.com/mdtohtml/mdtohtml@latest + ## Usage ``` -$ make mdtohtml & ./mdtohtml +$ go run . // You can avoid to generate css file with -nocss flag in order to customize style. -$ make mdtohtml & ./mdtohtml -nocss +$ go run . -nocss + +// Alternatively, if you have installed the tool using `go install`: +$ mdtohtml +$ mdtohtml -nocss ``` ## Current support notations (2019-10-14) @@ -43,4 +55,4 @@ Newline = "\n" ; ``` ## Test -Supports test written in Go. You can test this tool just by `$ make test`. +Supports test written in Go. You can test this tool just by `$ go test ./...`. diff --git a/gen.go b/gen.go index c281d60..65257d0 100644 --- a/gen.go +++ b/gen.go @@ -46,7 +46,7 @@ func generate(lines []Line) string { dep := l.dep - lines[i+1].dep for dep > 0 { html += "" - dep -= 1 + dep-- } } // insert for the end of sublists when a document ends with lists @@ -54,7 +54,7 @@ func generate(lines []Line) string { dep := l.dep for dep > 0 { html += "" - dep -= 1 + dep-- } } @@ -67,7 +67,9 @@ func generate(lines []Line) string { default: // insert a white space in a paragraph if (i > 0 && lines[i-1].ty == P) && (i < len(lines)-1 && lines[i+1].ty == P) { - html += l.val + if !lines[i-1].hasBr { + html += " " + } } } } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6558130 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/mdtohtml/mdtohtml + +go 1.25.0 + +require golang.org/x/tools v0.47.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e2cccbb --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= diff --git a/main.go b/main.go index b46ec29..067480e 100644 --- a/main.go +++ b/main.go @@ -4,34 +4,82 @@ import ( "bufio" "fmt" "os" + "path/filepath" "strings" ) func check(e error) { if e != nil { - panic(e) + fmt.Fprintf(os.Stderr, "error: %v\n", e) + os.Exit(1) } } +func usage() { + fmt.Fprintf(os.Stderr, "Usage: %s [-nocss]\n", os.Args[0]) + os.Exit(1) +} + func main() { - fname := os.Args[1] - name := strings.Split(fname, ".") + if len(os.Args) < 2 { + usage() + } + + var fname string + var noCSS bool - if strings.Compare(strings.ToLower(name[len(name)-1]), "md") != 0 { - panic("input file must be a markdown file (.md)") + for _, arg := range os.Args[1:] { + if arg == "-h" || arg == "--help" || arg == "-help" { + usage() + } else if arg == "-nocss" { + noCSS = true + } else if strings.HasPrefix(arg, "-") { + fmt.Fprintf(os.Stderr, "error: unknown flag %s\n", arg) + usage() + } else if fname != "" { + fmt.Fprintf(os.Stderr, "error: multiple input files specified\n") + usage() + } else { + fname = arg + } + } + + if fname == "" { + fmt.Fprintf(os.Stderr, "error: missing markdown filename\n") + usage() } - wfile, err := os.Create(name[0] + ".html") + ext := filepath.Ext(fname) + if strings.ToLower(ext) != ".md" { + fmt.Fprintf(os.Stderr, "error: input file must be a markdown file (.md)\n") + os.Exit(1) + } + + base := strings.TrimSuffix(fname, ext) + + wfile, err := os.Create(base + ".html") check(err) - defer wfile.Close() + + defer func() { + if err := wfile.Close(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + }() writer := bufio.NewWriter(wfile) - if len(os.Args) < 3 || os.Args[2] != "-nocss" { - fmt.Fprintln(writer, css()) + if !noCSS { + _, err = fmt.Fprintln(writer, css()) + check(err) } rfile, err := os.Open(fname) check(err) - defer rfile.Close() + + defer func() { + if err := rfile.Close(); err != nil { + fmt.Fprintf(os.Stderr, "error closing input file: %v\n", err) + } + }() reader := bufio.NewReader(rfile) lines := make([]Line, 0) @@ -47,8 +95,9 @@ func main() { lines = append(lines, convert(line)) } - writer.WriteString("") - writer.WriteString(generate(lines)) - writer.WriteString("") - writer.Flush() + _, _ = writer.WriteString("") + _, _ = writer.WriteString(generate(lines)) + _, _ = writer.WriteString("") + err = writer.Flush() + check(err) } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..4d42036 --- /dev/null +++ b/main_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "embed" + "io/fs" + "path" + "strings" + "testing" + "testing/fstest" + + "golang.org/x/tools/txtar" +) + +//go:embed testdata/txtar/*.txtar +var testdataFS embed.FS + +func SplitInputExpected(ar *txtar.Archive) (input, expected fstest.MapFS) { + input = fstest.MapFS{} + expected = fstest.MapFS{} + + for _, f := range ar.Files { + switch f.Name { + case "input.txt": + input[f.Name] = &fstest.MapFile{Data: f.Data} + case "expected.html": + expected[f.Name] = &fstest.MapFile{Data: f.Data} + } + } + return input, expected +} + +func TestTxtar(t *testing.T) { + entries, err := fs.Glob(testdataFS, "testdata/txtar/*.txtar") + if err != nil { + t.Fatalf("glob fixtures: %v", err) + } + + for _, fixture := range entries { + fixture := fixture + t.Run(strings.TrimSuffix(path.Base(fixture), ".txtar"), func(t *testing.T) { + raw, err := testdataFS.ReadFile(fixture) + if err != nil { + t.Fatalf("read fixture %s: %v", fixture, err) + } + ar := txtar.Parse(raw) + + inputFS, expectedFS := SplitInputExpected(ar) + + inputRaw, err := fs.ReadFile(inputFS, "input.txt") + if err != nil { + t.Fatalf("read input.txt: %v", err) + } + input := strings.TrimSpace(string(inputRaw)) + + lines := make([]Line, 0) + for _, in := range strings.Split(input, "\n") { + lines = append(lines, convert(in)) + } + html := generate(lines) + + expectedRaw, err := fs.ReadFile(expectedFS, "expected.html") + if err != nil { + t.Fatalf("read expected.html: %v", err) + } + expected := strings.TrimSpace(string(expectedRaw)) + + if html != expected { + t.Errorf("%q => expected %q but got %q", input, expected, html) + } + }) + } +} diff --git a/regexp.go b/regexp.go index a685fdd..f1097cf 100644 --- a/regexp.go +++ b/regexp.go @@ -7,14 +7,14 @@ import ( ) var ( - heading, _ = regexp.Compile("(^#{1,6}) (.+)") - headingIn, _ = regexp.Compile("^ *- +(#{1,6}) (.+)") - list, _ = regexp.Compile("^( *)- (.+)") - link, _ = regexp.Compile(".*(\\[.+?\\])(\\(.+?\\)).*") - emphasis, _ = regexp.Compile(".*(\\*.+\\*).*|.*(\\_.+\\_).*") - strong, _ = regexp.Compile(".*(\\*\\*.+\\*\\*).*|.*(\\_\\_.+\\_\\_).*") - horizontal, _ = regexp.Compile("^-{3}|_{3}|\\*{3}") - whitespace, _ = regexp.Compile("^( +)(.*)") + heading = regexp.MustCompile(`(^#{1,6}) (.+)`) + headingIn = regexp.MustCompile(`^ *- +(#{1,6}) (.+)`) + list = regexp.MustCompile(`^( *)- (.+)`) + link = regexp.MustCompile(`.*(\[.+?\])(\(.+?\)).*`) + emphasis = regexp.MustCompile(`.*(\*.+\*).*|.*(\_.+\_).*`) + strong = regexp.MustCompile(`.*(\*\*.+\*\*).*|.*(\_\_.+\_\_).*`) + horizontal = regexp.MustCompile(`^-{3}|_{3}|\*{3}`) + whitespace = regexp.MustCompile(`^( +)(.*)`) ) type Type int @@ -34,9 +34,10 @@ const ( ) type Line struct { - ty Type - val string - dep int + ty Type + val string + dep int + hasBr bool } func ntoh(n int) Type { @@ -56,6 +57,7 @@ func ntoh(n int) Type { default: panic(fmt.Sprintf("a heading should be in the range of 1 to 6, but got %d", n)) } + } func hton(ty Type) int { @@ -75,18 +77,21 @@ func hton(ty Type) int { default: panic(fmt.Sprintf("a heading should be in the range of 1 to 6, but got %d", ty)) } + } func convert(line string) Line { // newline if line == "\n" || len(line) == 0 { - return Line{Newline, " ", 0} + return Line{Newline, " ", 0, false} } + hasBr := false + // ----- Inline Elements ----- - match_something := true - for match_something { + matchSomething := true + for matchSomething { // inline elements are replaced with HTML in this function. for strong.MatchString(line) { // line[loc[2]:loc[3]]: **** @@ -128,10 +133,10 @@ func convert(line string) Line { litag := "" + text + "" line = line[:loc[2]] + litag + line[loc[5]:] - fmt.Println(loc) - fmt.Println(text) - fmt.Println(url) - fmt.Println(line) + fmt.Println(loc) + fmt.Println(text) + fmt.Println(url) + fmt.Println(line) continue } @@ -148,10 +153,14 @@ func convert(line string) Line { } // break at the end of line - if len(line) > 2 && line[len(line)-2:] == " " { + if len(line) >= 2 && line[len(line)-2:] == " " { line = line[:len(line)-2] + "
" + hasBr = true + } else if len(line) >= 1 && line[len(line)-1] == '\\' { + line = line[:len(line)-1] + "
" + hasBr = true } - match_something = false + matchSomething = false } // ----- Block Elements ----- @@ -162,7 +171,7 @@ func convert(line string) Line { //line[loc[4]:loc[5]]: list content loc := list.FindStringSubmatchIndex(line) dep := loc[3] / 2 - return Line{Li, line[loc[4]:loc[5]], dep} + return Line{Li, line[loc[4]:loc[5]], dep, false} } if heading.MatchString(line) { @@ -170,11 +179,11 @@ func convert(line string) Line { //line[loc[4]:loc[5]]: title loc := heading.FindStringSubmatchIndex(line) n := loc[3] - return Line{ntoh(n), line[loc[4]:loc[5]], 0} + return Line{ntoh(n), line[loc[4]:loc[5]], 0, false} } if horizontal.MatchString(line) { - return Line{Hr, "", 0} + return Line{Hr, "", 0, false} } // replace white spaces with a white space at the start of a line @@ -185,5 +194,5 @@ func convert(line string) Line { line = " " + line[loc[4]:loc[5]] } - return Line{P, line, 0} + return Line{P, line, 0, hasBr} } diff --git a/test.go b/test.go deleted file mode 100644 index 31ff746..0000000 --- a/test.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "fmt" - "strings" -) - -func test(expect string, input string) { - lines := make([]Line, 0) - for _, in := range strings.Split(input, "\n") { - lines = append(lines, convert(in)) - } - html := generate(lines) - - if html == expect { - fmt.Println(input + " => " + expect) - } else { - panic(input + " => " + expect + " but got " + html) - } -} - -func main() { - fmt.Println("\n----- paragrah -----") - test("

a paragraph

", "a paragraph") - test("

a paragraph
hogehoge

", "a paragraph \nhogehoge") - - fmt.Println("\n----- heading -----") - test("

h1

", "# h1") - test("

h2

", "## h2") - test("

h3

", "### h3") - test("

h4

", "#### h4") - test("
h5
", "##### h5") - test("
h6
", "###### h6") - test("

####### h7

", "####### h7") - test("

###dummyh3

", "###dummyh3") - test("

C## is not heading

", "C## is not heading") - - fmt.Println("\n----- list -----") - test("
  • list1
", "- list1") - test("
  • list1
  • list2
", "- list1\n- list2") - // TODO: Sublist is not a standard syntax. - // It should be
  • list1
    • sublist1
  • - // but now got
  • list1
    • sublist1
    - test("
    • list1
      • sublist1
    ", "- list1\n - sublist1") - test("
    • list1
      • sublist1
        • subsublist1
    ", "- list1\n - sublist1\n - subsublist1") - test("
    • list1
      • sublist1
    • list2
    ", "- list1\n - sublist1\n- list2") - test("
    • a
      • aa
        • aaa
    • b
    ", "- a\n - aa\n - aaa\n- b") - test("
    • a
      • aa
        • aaa
      • bb
    ", "- a\n - aa\n - aaa\n - bb") - test("
    • h1

    ", "- # h1") - //test("
    • a -b
    • c
    ", "- a\n -b\n- c") - - fmt.Println("\n----- link -----") - test("

    link

    ", "[link](http://example.com)") - test("

    link(2)

    ", "[link(2)](http://example.com)") - test("

    inline textlink.

    ", "inline text[link](http://example.com).") - test("

    [dummylink] (http://example.com)

    ", "[dummylink] (http://example.com)") - - fmt.Println("\n----- heading with inline elements -----") - test("

    link

    ", "# [link](http://example.com)") - test("

    - dummylist

    ", "# - dummylist") - - fmt.Println("\n----- list with inline elements -----") - test("", "- [link](http://example.com)") - test("", "- This is [link](http://example.com) list.") - test("
    • h1

    ", "- # h1") - - fmt.Println("\n----- heading after a list -----") - test("
    • list1

    h1

    ", "- list1\n# h1") - test("
    • list1

    h1

    ", "- list1\n\n# h1") - test("
    • a
      • b

    h1

    ", "- a\n - b\n# h1") - - fmt.Println("\n----- multiple lines -----") - test("

    h1

    text

    ", "# h1\ntext") - - fmt.Println("\n----- emphasis -----") - test("

    emphasis

    ", "*emphasis*") - test("

    emphasis

    ", "_emphasis_") - test("

    strong

    ", "**strong**") - test("

    strong

    ", "__strong__") - - fmt.Println("OK") -} diff --git a/testdata/txtar/emphasis.txtar b/testdata/txtar/emphasis.txtar new file mode 100644 index 0000000..0b07a7d --- /dev/null +++ b/testdata/txtar/emphasis.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +*emphasis* +-- expected.html -- +

    emphasis

    diff --git a/testdata/txtar/emphasis2.txtar b/testdata/txtar/emphasis2.txtar new file mode 100644 index 0000000..8a9290b --- /dev/null +++ b/testdata/txtar/emphasis2.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +_emphasis_ +-- expected.html -- +

    emphasis

    diff --git a/testdata/txtar/heading.txtar b/testdata/txtar/heading.txtar new file mode 100644 index 0000000..0294445 --- /dev/null +++ b/testdata/txtar/heading.txtar @@ -0,0 +1,12 @@ +-- input.txt -- +# h1 +## h2 +### h3 +#### h4 +##### h5 +###### h6 +####### h7 +###dummyh3 +C## is not heading +-- expected.html -- +

    h1

    h2

    h3

    h4

    h5
    h6

    ####### h7###dummyh3C## is not heading

    diff --git a/testdata/txtar/heading_after_list.txtar b/testdata/txtar/heading_after_list.txtar new file mode 100644 index 0000000..62094fc --- /dev/null +++ b/testdata/txtar/heading_after_list.txtar @@ -0,0 +1,5 @@ +-- input.txt -- +- list1 +# h1 +-- expected.html -- +
    • list1

    h1

    diff --git a/testdata/txtar/heading_after_list2.txtar b/testdata/txtar/heading_after_list2.txtar new file mode 100644 index 0000000..a7a9245 --- /dev/null +++ b/testdata/txtar/heading_after_list2.txtar @@ -0,0 +1,6 @@ +-- input.txt -- +- list1 + +# h1 +-- expected.html -- +
    • list1

    h1

    diff --git a/testdata/txtar/heading_after_list3.txtar b/testdata/txtar/heading_after_list3.txtar new file mode 100644 index 0000000..40a04c6 --- /dev/null +++ b/testdata/txtar/heading_after_list3.txtar @@ -0,0 +1,6 @@ +-- input.txt -- +- a + - b +# h1 +-- expected.html -- +
    • a
      • b

    h1

    diff --git a/testdata/txtar/heading_dummy_list.txtar b/testdata/txtar/heading_dummy_list.txtar new file mode 100644 index 0000000..9a693a8 --- /dev/null +++ b/testdata/txtar/heading_dummy_list.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +# - dummylist +-- expected.html -- +

    - dummylist

    diff --git a/testdata/txtar/heading_inline.txtar b/testdata/txtar/heading_inline.txtar new file mode 100644 index 0000000..2813f11 --- /dev/null +++ b/testdata/txtar/heading_inline.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +# [link](http://example.com) +-- expected.html -- +

    link

    diff --git a/testdata/txtar/link.txtar b/testdata/txtar/link.txtar new file mode 100644 index 0000000..5e22fc5 --- /dev/null +++ b/testdata/txtar/link.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +[link](http://example.com) +-- expected.html -- +

    link

    diff --git a/testdata/txtar/link2.txtar b/testdata/txtar/link2.txtar new file mode 100644 index 0000000..747cf2c --- /dev/null +++ b/testdata/txtar/link2.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +[link(2)](http://example.com) +-- expected.html -- +

    link(2)

    diff --git a/testdata/txtar/link_dummy.txtar b/testdata/txtar/link_dummy.txtar new file mode 100644 index 0000000..50be677 --- /dev/null +++ b/testdata/txtar/link_dummy.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +[dummylink] (http://example.com) +-- expected.html -- +

    [dummylink] (http://example.com)

    diff --git a/testdata/txtar/link_inline.txtar b/testdata/txtar/link_inline.txtar new file mode 100644 index 0000000..5412326 --- /dev/null +++ b/testdata/txtar/link_inline.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +inline text[link](http://example.com). +-- expected.html -- +

    inline textlink.

    diff --git a/testdata/txtar/list.txtar b/testdata/txtar/list.txtar new file mode 100644 index 0000000..06fb72c --- /dev/null +++ b/testdata/txtar/list.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +- list1 +-- expected.html -- +
    • list1
    diff --git a/testdata/txtar/list_heading.txtar b/testdata/txtar/list_heading.txtar new file mode 100644 index 0000000..ca2dee9 --- /dev/null +++ b/testdata/txtar/list_heading.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +- # h1 +-- expected.html -- +
    • h1

    diff --git a/testdata/txtar/list_inline.txtar b/testdata/txtar/list_inline.txtar new file mode 100644 index 0000000..800cdff --- /dev/null +++ b/testdata/txtar/list_inline.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +- [link](http://example.com) +-- expected.html -- + diff --git a/testdata/txtar/list_inline2.txtar b/testdata/txtar/list_inline2.txtar new file mode 100644 index 0000000..b60fefe --- /dev/null +++ b/testdata/txtar/list_inline2.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +- This is [link](http://example.com) list. +-- expected.html -- + diff --git a/testdata/txtar/list_multiple.txtar b/testdata/txtar/list_multiple.txtar new file mode 100644 index 0000000..d64df6d --- /dev/null +++ b/testdata/txtar/list_multiple.txtar @@ -0,0 +1,5 @@ +-- input.txt -- +- list1 +- list2 +-- expected.html -- +
    • list1
    • list2
    diff --git a/testdata/txtar/list_nested.txtar b/testdata/txtar/list_nested.txtar new file mode 100644 index 0000000..8450e16 --- /dev/null +++ b/testdata/txtar/list_nested.txtar @@ -0,0 +1,5 @@ +-- input.txt -- +- list1 + - sublist1 +-- expected.html -- +
    • list1
      • sublist1
    diff --git a/testdata/txtar/list_nested2.txtar b/testdata/txtar/list_nested2.txtar new file mode 100644 index 0000000..f28fd8e --- /dev/null +++ b/testdata/txtar/list_nested2.txtar @@ -0,0 +1,6 @@ +-- input.txt -- +- list1 + - sublist1 + - subsublist1 +-- expected.html -- +
    • list1
      • sublist1
        • subsublist1
    diff --git a/testdata/txtar/list_nested3.txtar b/testdata/txtar/list_nested3.txtar new file mode 100644 index 0000000..f541b4c --- /dev/null +++ b/testdata/txtar/list_nested3.txtar @@ -0,0 +1,6 @@ +-- input.txt -- +- list1 + - sublist1 +- list2 +-- expected.html -- +
    • list1
      • sublist1
    • list2
    diff --git a/testdata/txtar/list_nested4.txtar b/testdata/txtar/list_nested4.txtar new file mode 100644 index 0000000..9dfc8de --- /dev/null +++ b/testdata/txtar/list_nested4.txtar @@ -0,0 +1,7 @@ +-- input.txt -- +- a + - aa + - aaa +- b +-- expected.html -- +
    • a
      • aa
        • aaa
    • b
    diff --git a/testdata/txtar/list_nested5.txtar b/testdata/txtar/list_nested5.txtar new file mode 100644 index 0000000..b952f4e --- /dev/null +++ b/testdata/txtar/list_nested5.txtar @@ -0,0 +1,7 @@ +-- input.txt -- +- a + - aa + - aaa + - bb +-- expected.html -- +
    • a
      • aa
        • aaa
      • bb
    diff --git a/testdata/txtar/multiple_lines.txtar b/testdata/txtar/multiple_lines.txtar new file mode 100644 index 0000000..da11828 --- /dev/null +++ b/testdata/txtar/multiple_lines.txtar @@ -0,0 +1,5 @@ +-- input.txt -- +# h1 +text +-- expected.html -- +

    h1

    text

    diff --git a/testdata/txtar/paragraph.txtar b/testdata/txtar/paragraph.txtar new file mode 100644 index 0000000..a801b83 --- /dev/null +++ b/testdata/txtar/paragraph.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +a paragraph +-- expected.html -- +

    a paragraph

    diff --git a/testdata/txtar/paragraph_br.txtar b/testdata/txtar/paragraph_br.txtar new file mode 100644 index 0000000..8357b06 --- /dev/null +++ b/testdata/txtar/paragraph_br.txtar @@ -0,0 +1,5 @@ +-- input.txt -- +a paragraph\ +hogehoge +-- expected.html -- +

    a paragraph
    hogehoge

    diff --git a/testdata/txtar/strong.txtar b/testdata/txtar/strong.txtar new file mode 100644 index 0000000..122b626 --- /dev/null +++ b/testdata/txtar/strong.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +**strong** +-- expected.html -- +

    strong

    diff --git a/testdata/txtar/strong2.txtar b/testdata/txtar/strong2.txtar new file mode 100644 index 0000000..843790a --- /dev/null +++ b/testdata/txtar/strong2.txtar @@ -0,0 +1,4 @@ +-- input.txt -- +__strong__ +-- expected.html -- +

    strong