embedmd.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. // Copyright 2016 Google Inc. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0 (the "License");
  3. // you may not use this file except in compliance with the License.
  4. // You may obtain a copy of the License at
  5. // http://www.apache.org/licenses/LICENSE-2.0
  6. //
  7. // Unless required by applicable law or agreed to writing, software distributed
  8. // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
  9. // CONDITIONS OF ANY KIND, either express or implied.
  10. //
  11. // See the License for the specific language governing permissions and
  12. // limitations under the License.
  13. // Package embedmd provides a single function, Process, that parses markdown
  14. // searching for markdown comments.
  15. //
  16. // The format of an embedmd command is:
  17. //
  18. // [embedmd]:# (pathOrURL language /start regexp/ /end regexp/)
  19. //
  20. // The embedded code will be extracted from the file at pathOrURL,
  21. // which can either be a relative path to a file in the local file
  22. // system (using always forward slashes as directory separator) or
  23. // a url starting with http:// or https://.
  24. // If the pathOrURL is a url the tool will fetch the content in that url.
  25. // The embedded content starts at the first line that matches /start regexp/
  26. // and finishes at the first line matching /end regexp/.
  27. //
  28. // Omitting the the second regular expression will embed only the piece of
  29. // text that matches /regexp/:
  30. //
  31. // [embedmd]:# (pathOrURL language /regexp/)
  32. //
  33. // To embed the whole line matching a regular expression you can use:
  34. //
  35. // [embedmd]:# (pathOrURL language /.*regexp.*\n/)
  36. //
  37. // If you want to embed from a point to the end you should use:
  38. //
  39. // [embedmd]:# (pathOrURL language /start regexp/ $)
  40. //
  41. // Finally you can embed a whole file by omitting both regular expressions:
  42. //
  43. // [embedmd]:# (pathOrURL language)
  44. //
  45. // You can ommit the language in any of the previous commands, and the extension
  46. // of the file will be used for the snippet syntax highlighting. Note that while
  47. // this works Go files, since the file extension .go matches the name of the language
  48. // go, this will fail with other files like .md whose language name is markdown.
  49. //
  50. // [embedmd]:# (file.ext)
  51. //
  52. package embedmd
  53. import (
  54. "fmt"
  55. "io"
  56. "regexp"
  57. )
  58. // Process reads markdown from the given io.Reader searching for an embedmd
  59. // command. When a command is found, it is executed and the output is written
  60. // into the given io.Writer with the rest of standard markdown.
  61. func Process(out io.Writer, in io.Reader, opts ...Option) error {
  62. e := embedder{Fetcher: fetcher{}}
  63. for _, opt := range opts {
  64. opt.f(&e)
  65. }
  66. return process(out, in, e.runCommand)
  67. }
  68. // An Option provides a way to adapt the Process function to your needs.
  69. type Option struct{ f func(*embedder) }
  70. // WithBaseDir indicates that the given path should be used to resolve relative
  71. // paths.
  72. func WithBaseDir(path string) Option {
  73. return Option{func(e *embedder) { e.baseDir = path }}
  74. }
  75. // WithFetcher provides a custom Fetcher to be used whenever a path or url needs
  76. // to be fetched.
  77. func WithFetcher(c Fetcher) Option {
  78. return Option{func(e *embedder) { e.Fetcher = c }}
  79. }
  80. type embedder struct {
  81. Fetcher
  82. baseDir string
  83. }
  84. func (e *embedder) runCommand(w io.Writer, cmd *command) error {
  85. b, err := e.Fetch(e.baseDir, cmd.path)
  86. if err != nil {
  87. return fmt.Errorf("could not read %s: %v", cmd.path, err)
  88. }
  89. b, err = extract(b, cmd.start, cmd.end)
  90. if err != nil {
  91. return fmt.Errorf("could not extract content from %s: %v", cmd.path, err)
  92. }
  93. if len(b) > 0 && b[len(b)-1] != '\n' {
  94. b = append(b, '\n')
  95. }
  96. fmt.Fprintln(w, "```"+cmd.lang)
  97. w.Write(b)
  98. fmt.Fprintln(w, "```")
  99. return nil
  100. }
  101. func extract(b []byte, start, end *string) ([]byte, error) {
  102. if start == nil && end == nil {
  103. return b, nil
  104. }
  105. match := func(s string) ([]int, error) {
  106. if len(s) <= 2 || s[0] != '/' || s[len(s)-1] != '/' {
  107. return nil, fmt.Errorf("missing slashes (/) around %q", s)
  108. }
  109. re, err := regexp.CompilePOSIX(s[1 : len(s)-1])
  110. if err != nil {
  111. return nil, err
  112. }
  113. loc := re.FindIndex(b)
  114. if loc == nil {
  115. return nil, fmt.Errorf("could not match %q", s)
  116. }
  117. return loc, nil
  118. }
  119. if *start != "" {
  120. loc, err := match(*start)
  121. if err != nil {
  122. return nil, err
  123. }
  124. if end == nil {
  125. return b[loc[0]:loc[1]], nil
  126. }
  127. b = b[loc[0]:]
  128. }
  129. if *end != "$" {
  130. loc, err := match(*end)
  131. if err != nil {
  132. return nil, err
  133. }
  134. b = b[:loc[1]]
  135. }
  136. return b, nil
  137. }