ngrab.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. // Copyright 2024 The ngrab Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // Package implements the ngrab command.
  5. package lib // import "modernc.org/ngrab/lib"
  6. import (
  7. "encoding/json"
  8. "fmt"
  9. "io"
  10. "os"
  11. "strings"
  12. )
  13. // Node represents a documentation node
  14. //
  15. // Every input .n file is represented by a separate node tree. The root node
  16. // has Type == "document" and Text == file path.
  17. type Node struct {
  18. ID int
  19. Parent int `json:",omitempty"`
  20. Type string
  21. Text string `json:",omitempty"`
  22. p *Node
  23. }
  24. func (n *Node) indent() string {
  25. i := 0
  26. for n.p != nil {
  27. i++
  28. n = n.p
  29. }
  30. return strings.Repeat("· ", i)
  31. }
  32. // Task describes a job.
  33. type Task struct {
  34. // The nodes used for producing the JSON
  35. Nodes []*Node
  36. files []string
  37. fn string
  38. lines []string
  39. w io.Writer
  40. id int
  41. lineNo int
  42. }
  43. // NewTask returns a newly created Task or an error, if any.
  44. func NewTask(w io.Writer, files []string) (r *Task, err error) {
  45. return &Task{
  46. w: w,
  47. files: files,
  48. }, nil
  49. }
  50. // Main executes 't'.
  51. func (t *Task) Main() (err error) {
  52. for _, v := range t.files {
  53. if err := t.file(v); err != nil {
  54. return err
  55. }
  56. }
  57. b, err := json.MarshalIndent(t.Nodes, "", "\t")
  58. if err != nil {
  59. return err
  60. }
  61. _, err = t.w.Write(b)
  62. return err
  63. }
  64. func (t *Task) newNode(p *Node, typ, text string) (r *Node) {
  65. var pid int
  66. if p != nil {
  67. pid = p.ID
  68. }
  69. t.id++
  70. r = &Node{ID: t.id, Parent: pid, Type: typ, Text: text, p: p}
  71. t.Nodes = append(t.Nodes, r)
  72. return r
  73. }
  74. type parseError string
  75. func (t *Task) file(fn string) (err error) {
  76. defer func() {
  77. switch x := recover().(type) {
  78. case nil:
  79. // ok
  80. case parseError:
  81. if err == nil {
  82. err = fmt.Errorf("%v:%v: %s", t.fn, t.lineNo, x)
  83. }
  84. default:
  85. panic(x)
  86. }
  87. }()
  88. b, err := os.ReadFile(fn)
  89. if err != nil {
  90. return err
  91. }
  92. t.lines = strings.Split(string(b), "\n")
  93. t.fn = fn
  94. t.parseDoc(fn)
  95. return nil
  96. }
  97. func (t *Task) peek() (r string) {
  98. if len(t.lines) != 0 {
  99. r = t.lines[0]
  100. if r == "" {
  101. r = "."
  102. }
  103. }
  104. return r
  105. }
  106. func (t *Task) consume() (r string) {
  107. if len(t.lines) != 0 {
  108. r = t.lines[0]
  109. if r == "" {
  110. r = "."
  111. }
  112. t.lines = t.lines[1:]
  113. t.lineNo++
  114. }
  115. return r
  116. }
  117. func (t *Task) parseDoc(fn string) {
  118. p := t.newNode(nil, "document", fn)
  119. t.lineNo = 1
  120. t.parse(p, 0)
  121. }
  122. func (t *Task) parse(p *Node, nest int) {
  123. for line := t.peek(); line != ""; line = t.peek() {
  124. switch {
  125. case
  126. strings.HasPrefix(line, "'"),
  127. strings.HasPrefix(line, `.\"`):
  128. t.newNode(p, "comment", t.consume()[1:])
  129. case
  130. !strings.HasPrefix(line, "."),
  131. line == ".",
  132. strings.HasPrefix(line, ". "):
  133. t.newNode(p, "text", t.consume())
  134. case
  135. strings.HasPrefix(line, ".BS"),
  136. strings.HasPrefix(line, ".CS"),
  137. strings.HasPrefix(line, ".DS"),
  138. strings.HasPrefix(line, ".RS"),
  139. strings.HasPrefix(line, ".SO"):
  140. tag := t.peek()[1:3]
  141. t.parse(t.newNode(p, tag, t.consume()[3:]), nest+1)
  142. case
  143. strings.HasPrefix(line, ".BE"),
  144. strings.HasPrefix(line, ".CE"),
  145. strings.HasPrefix(line, ".DE"),
  146. strings.HasPrefix(line, ".RE"),
  147. strings.HasPrefix(line, ".SE"):
  148. tag := t.peek()[1:3]
  149. t.newNode(p, tag, t.consume()[3:])
  150. if nest != 0 {
  151. return
  152. }
  153. default:
  154. tag := line[1:3] // .TH etc
  155. t.newNode(p, tag, t.consume()[3:])
  156. }
  157. }
  158. }