|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "os" |
| 6 | + "os/exec" |
| 7 | + "path/filepath" |
| 8 | + "strings" |
| 9 | + |
| 10 | + "github.com/xyproto/env/v2" |
| 11 | + "github.com/xyproto/files" |
| 12 | + "github.com/xyproto/vt" |
| 13 | +) |
| 14 | + |
| 15 | +// FindCurrentHeading returns the heading text of the nearest Markdown heading |
| 16 | +// at or above the cursor line, or "" if none is found. |
| 17 | +func (e *Editor) FindCurrentHeading() string { |
| 18 | + for i := e.DataY(); i >= 0; i-- { |
| 19 | + line := e.Line(i) |
| 20 | + if strings.HasPrefix(line, "#") { |
| 21 | + return strings.TrimSpace(strings.TrimLeft(line, "#")) |
| 22 | + } |
| 23 | + } |
| 24 | + return "" |
| 25 | +} |
| 26 | + |
| 27 | +// NextHeadingLineIndex returns the line index of the next Markdown heading |
| 28 | +// below the current cursor position, and true if one was found. |
| 29 | +func (e *Editor) NextHeadingLineIndex() (LineIndex, bool) { |
| 30 | + for i := e.DataY() + 1; i < LineIndex(e.Len()); i++ { |
| 31 | + if strings.HasPrefix(e.Line(i), "#") { |
| 32 | + return i, true |
| 33 | + } |
| 34 | + } |
| 35 | + return 0, false |
| 36 | +} |
| 37 | + |
| 38 | +// PrevHeadingLineIndex returns the line index of the previous Markdown heading |
| 39 | +// above the current cursor position, and true if one was found. |
| 40 | +func (e *Editor) PrevHeadingLineIndex() (LineIndex, bool) { |
| 41 | + for i := e.DataY() - 1; i >= 0; i-- { |
| 42 | + if strings.HasPrefix(e.Line(i), "#") { |
| 43 | + return i, true |
| 44 | + } |
| 45 | + } |
| 46 | + return 0, false |
| 47 | +} |
| 48 | + |
| 49 | +// exportBookPDF renders the current Markdown file to a book-style PDF using |
| 50 | +// pandoc with documentclass=book, generous margins and no code-listings header. |
| 51 | +func (e *Editor) exportBookPDF(c *vt.Canvas, tty *vt.TTY, status *StatusBar, pandocPath, pdfFilename string) error { |
| 52 | + status.ClearAll(c, true) |
| 53 | + status.SetMessage("Rendering book PDF using Pandoc...") |
| 54 | + status.ShowNoTimeout(c, e) |
| 55 | + |
| 56 | + f, err := os.CreateTemp(tempDir, "_o*.md") |
| 57 | + if err != nil { |
| 58 | + return err |
| 59 | + } |
| 60 | + f.Close() |
| 61 | + tempFilename := f.Name() |
| 62 | + defer os.Remove(tempFilename) |
| 63 | + |
| 64 | + oldFilename := e.filename |
| 65 | + e.filename = tempFilename |
| 66 | + err = e.Save(c, tty) |
| 67 | + e.filename = oldFilename |
| 68 | + if err != nil { |
| 69 | + status.ClearAll(c, true) |
| 70 | + status.SetError(err) |
| 71 | + status.Show(c, e) |
| 72 | + return err |
| 73 | + } |
| 74 | + |
| 75 | + papersize := env.Str("PAPERSIZE", "a4") |
| 76 | + |
| 77 | + // Use documentclass=book for proper chapter headings, page numbering and |
| 78 | + // binding-friendly margins (wider on the spine side). |
| 79 | + pandocCommand := exec.Command(pandocPath, |
| 80 | + "-fmarkdown-implicit_figures", |
| 81 | + "--toc", |
| 82 | + "-Vdocumentclass=book", |
| 83 | + "-Vgeometry:left=3cm,top=2.5cm,right=2.5cm,bottom=3cm", |
| 84 | + "-Vpapersize:"+papersize, |
| 85 | + "-Vfontsize=12pt", |
| 86 | + "--pdf-engine=xelatex", |
| 87 | + "-o", pdfFilename, |
| 88 | + tempFilename, |
| 89 | + ) |
| 90 | + |
| 91 | + // Only add a TeX header file if a custom one already exists; skip the |
| 92 | + // code-listings boilerplate that is not useful for prose. |
| 93 | + expandedTexFilename := env.ExpandUser(pandocTexFilename) |
| 94 | + if files.Exists(expandedTexFilename) { |
| 95 | + pandocCommand.Args = append(pandocCommand.Args[:len(pandocCommand.Args)-1], |
| 96 | + "-H"+expandedTexFilename, |
| 97 | + pandocCommand.Args[len(pandocCommand.Args)-1], |
| 98 | + ) |
| 99 | + } |
| 100 | + |
| 101 | + saveCommand(pandocCommand) |
| 102 | + |
| 103 | + if output, err := pandocCommand.CombinedOutput(); err != nil { |
| 104 | + status.ClearAll(c, false) |
| 105 | + outputByteLines := bytes.Split(bytes.TrimSpace(output), []byte{'\n'}) |
| 106 | + errorMessage := string(outputByteLines[len(outputByteLines)-1]) |
| 107 | + if len(errorMessage) == 0 { |
| 108 | + errorMessage = err.Error() |
| 109 | + } |
| 110 | + status.SetErrorMessage(errorMessage) |
| 111 | + status.Show(c, e) |
| 112 | + return err |
| 113 | + } |
| 114 | + |
| 115 | + // Also generate the output filename from the first # heading if possible |
| 116 | + outName := filepath.Base(pdfFilename) |
| 117 | + status.ClearAll(c, true) |
| 118 | + status.SetMessage("Saved " + outName) |
| 119 | + status.ShowNoTimeout(c, e) |
| 120 | + return nil |
| 121 | +} |
0 commit comments