├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── README.md ├── action.yml ├── go.mod ├── go.sum ├── main.go ├── parse.go ├── util.go ├── walk.go └── write.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # File generated by github.com/posener/goaction. DO NOT EDIT. 2 | 3 | FROM golang:1.16-alpine3.14 4 | RUN apk add git 5 | 6 | COPY . /home/src 7 | WORKDIR /home/src 8 | RUN go build -o /bin/action ./ 9 | 10 | ENTRYPOINT [ "/bin/action" ] 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 jackyzha0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Link Scraper 2 | Used by [Quartz](https://github.com/jackyzha0/quartz) 3 | 4 | This repository comes to you in two parts. 5 | 6 | 1. GitHub Action (scrapes links into a `.json` file) 7 | 2. Hugo Partial (turns `.json` file into graphs and tables) 8 | 9 | ## GitHub Action 10 | GitHub action and binary to scrape [Obsidian](http://obsidian.md/) vault for links and exposes them as a `.json` file for easy consumption by [Hugo](https://gohugo.io/). 11 | ### Example Usage (Binary) 12 | Read Markdown from the `/content` folder and place the resulting `linkIndex.json` (and `contentIndex.yaml` if the `index` flag is enabled) into `/data` 13 | 14 | ```shell 15 | # Installation 16 | go install github.com/jackyzha0/hugo-obsidian@latest 17 | 18 | # Run 19 | hugo-obsidian -input=content -output=data -index=true 20 | ``` 21 | 22 | ### Example Usage (GitHub Action) 23 | 24 | Add 'Build Link Index' as a build step in your workflow file (e.g. `.github/workflows/deploy.yaml`) 25 | ```yaml 26 | ... 27 | 28 | jobs: 29 | deploy: 30 | runs-on: ubuntu-18.04 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Build Link Index 34 | uses: jackyzha0/hugo-obsidian@v2.1 35 | with: 36 | input: content # input folder 37 | output: data # output folder 38 | index: true # whether to index content 39 | ... 40 | ``` 41 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: hugo-obsidian 2 | inputs: 3 | input: 4 | default: . 5 | description: "Input Directory" 6 | required: false 7 | output: 8 | default: . 9 | description: "Output Directory" 10 | required: false 11 | index: 12 | default: "false" 13 | description: "Index content" 14 | required: false 15 | root: 16 | default: "." 17 | description: "Root of Repository" 18 | required: false 19 | description: simple GitHub action to parse Markdown Links into a .yaml file for Hugo 20 | branding: 21 | icon: anchor 22 | color: purple 23 | runs: 24 | using: docker 25 | image: Dockerfile 26 | args: 27 | - "-input=${{ inputs.input }}" 28 | - "-output=${{ inputs.output }}" 29 | - "-index=${{ inputs.index }}" 30 | - "-root=${{ inputs.root }}" 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jackyzha0/hugo-obsidian 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.4.1 // indirect 7 | github.com/PuerkitoBio/goquery v1.8.0 8 | github.com/abhinav/goldmark-wikilink v0.3.0 9 | github.com/adrg/frontmatter v0.2.0 // indirect 10 | github.com/yuin/goldmark v1.4.4 11 | gopkg.in/yaml.v2 v2.4.0 // indirect 12 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= 3 | github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= 5 | github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= 6 | github.com/abhinav/goldmark-wikilink v0.3.0 h1:ry8CBaULn410PKCSkwLz/WVI2f/g7EB+yqY7LKHDcPQ= 7 | github.com/abhinav/goldmark-wikilink v0.3.0/go.mod h1:MHRZiLRE1ZDZDjHCFYwKEEgITXGbB7N0Yr00dbmfHM8= 8 | github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= 9 | github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= 10 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= 11 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 12 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gernest/front v0.0.0-20210301115436-8a0b0a782d0a h1:z7BePknRd4Nz3CeWDhcmCkuCliM2YY/RnjWpdPUuQQo= 15 | github.com/gernest/front v0.0.0-20210301115436-8a0b0a782d0a/go.mod h1:FwEMwQ5+xky8tbzDLj72k2RAqXnFByLNwxg+9UZDtqU= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 20 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 22 | github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs= 23 | github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= 24 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= 25 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 26 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 29 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 30 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 34 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 35 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 36 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 38 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/BurntSushi/toml" 6 | wikilink "github.com/abhinav/goldmark-wikilink" 7 | "github.com/yuin/goldmark" 8 | "io/ioutil" 9 | "path/filepath" 10 | "time" 11 | ) 12 | 13 | var md goldmark.Markdown 14 | 15 | func init() { 16 | md = goldmark.New( 17 | goldmark.WithExtensions(&wikilink.Extender{}), 18 | ) 19 | } 20 | 21 | type Link struct { 22 | Source string `json:"source"` 23 | Target string `json:"target"` 24 | Text string `json:"text"` 25 | } 26 | 27 | type LinkTable = map[string][]Link 28 | type Index struct { 29 | Links LinkTable `json:"links"` 30 | Backlinks LinkTable `json:"backlinks"` 31 | } 32 | 33 | type Content struct { 34 | Title string `json:"title"` 35 | Content string `json:"content"` 36 | LastModified time.Time `json:"lastmodified"` 37 | Tags []string `json:"tags"` 38 | } 39 | 40 | type ContentIndex = map[string]Content 41 | 42 | type ConfigTOML struct { 43 | IgnoredFiles []string `toml:"ignoreFiles"` 44 | } 45 | 46 | func getIgnoredFiles(base string) (res map[string]struct{}) { 47 | res = make(map[string]struct{}) 48 | 49 | source, err := ioutil.ReadFile(filepath.FromSlash(base + "/config.toml")) 50 | if err != nil { 51 | return res 52 | } 53 | 54 | var config ConfigTOML 55 | if _, err := toml.Decode(string(source), &config); err != nil { 56 | return res 57 | } 58 | 59 | for _, glb := range config.IgnoredFiles { 60 | matches, _ := filepath.Glob(base + glb) 61 | for _, match := range matches { 62 | res[match] = struct{}{} 63 | } 64 | } 65 | 66 | return res 67 | } 68 | 69 | func main() { 70 | in := flag.String("input", ".", "Input Directory") 71 | out := flag.String("output", ".", "Output Directory") 72 | root := flag.String("root", "..", "Root Directory (for config parsing)") 73 | index := flag.Bool("index", false, "Whether to index the content") 74 | flag.Parse() 75 | 76 | ignoreBlobs := getIgnoredFiles(*root) 77 | l, i := walk(*in, ".md", *index, ignoreBlobs) 78 | f := filter(l) 79 | err := write(f, i, *index, *out, *root) 80 | if err != nil { 81 | panic(err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "io/ioutil" 8 | "strings" 9 | ) 10 | 11 | // parse single file for links 12 | func parse(dir, pathPrefix string) []Link { 13 | // read file 14 | source, err := ioutil.ReadFile(dir) 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | // parse md 20 | var links []Link 21 | fmt.Printf("[Parsing note] %s => ", trim(dir, pathPrefix, ".md")) 22 | 23 | var buf bytes.Buffer 24 | if err := md.Convert(source, &buf); err != nil { 25 | panic(err) 26 | } 27 | 28 | doc, err := goquery.NewDocumentFromReader(&buf) 29 | var n int 30 | doc.Find("a").Each(func(i int, s *goquery.Selection) { 31 | text := strings.TrimSpace(s.Text()) 32 | target, ok := s.Attr("href") 33 | if !ok { 34 | target = "#" 35 | } 36 | 37 | target = processTarget(target) 38 | source := processSource(trim(dir, pathPrefix, ".md")) 39 | 40 | // fmt.Printf(" '%s' => %s\n", source, target) 41 | if !strings.HasPrefix(text, "^"){ 42 | links = append(links, Link{ 43 | Source: source, 44 | Target: target, 45 | Text: text, 46 | }) 47 | n++ 48 | } 49 | }) 50 | fmt.Printf("found: %d links\n", n) 51 | 52 | return links 53 | } 54 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "path/filepath" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | func trim(source, prefix, suffix string) string { 12 | return strings.TrimPrefix(strings.TrimSuffix(source, suffix), prefix) 13 | } 14 | 15 | func hugoPathTrim(source string) string { 16 | return strings.TrimSuffix(strings.TrimSuffix(source, "/index"), "_index") 17 | } 18 | 19 | func processTarget(source string) string { 20 | if !isInternal(source) { 21 | return source 22 | } 23 | if strings.HasPrefix(source, "/") { 24 | return strings.TrimSuffix(source, ".md") 25 | } 26 | res := strings.Split(source, "#")[0] 27 | res = "/" + strings.TrimSuffix(strings.TrimSuffix(res, ".html"), ".md") 28 | res, _ = url.PathUnescape(res) 29 | res = strings.TrimSpace(res) 30 | res = UnicodeSanitize(res) 31 | return strings.ReplaceAll(url.PathEscape(res), "%2F", "/") 32 | } 33 | 34 | func processSource(source string) string { 35 | res := filepath.ToSlash(hugoPathTrim(source)) 36 | res = UnicodeSanitize(res) 37 | return strings.ReplaceAll(url.PathEscape(res), "%2F", "/") 38 | } 39 | 40 | func isInternal(link string) bool { 41 | return !strings.HasPrefix(link, "http") 42 | } 43 | 44 | // From https://golang.org/src/net/url/url.go 45 | func ishex(c rune) bool { 46 | switch { 47 | case '0' <= c && c <= '9': 48 | return true 49 | case 'a' <= c && c <= 'f': 50 | return true 51 | case 'A' <= c && c <= 'F': 52 | return true 53 | } 54 | return false 55 | } 56 | 57 | // UnicodeSanitize sanitizes string to be used in Hugo URL's 58 | // from https://github.com/gohugoio/hugo/blob/93aad3c543828efca2adeb7f96cf50ae29878593/helpers/path.go#L94 59 | func UnicodeSanitize(s string) string { 60 | source := []rune(s) 61 | target := make([]rune, 0, len(source)) 62 | var prependHyphen bool 63 | 64 | for i, r := range source { 65 | isAllowed := r == '.' || r == '/' || r == '\\' || r == '_' || r == '#' || r == '+' || r == '~' 66 | isAllowed = isAllowed || unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsMark(r) 67 | isAllowed = isAllowed || (r == '%' && i+2 < len(source) && ishex(source[i+1]) && ishex(source[i+2])) 68 | 69 | if isAllowed { 70 | if prependHyphen { 71 | target = append(target, '-') 72 | prependHyphen = false 73 | } 74 | target = append(target, r) 75 | } else if len(target) > 0 && (r == '-' || unicode.IsSpace(r)) { 76 | prependHyphen = true 77 | } 78 | } 79 | 80 | return string(target) 81 | } 82 | 83 | // filter out certain links (e.g. to media) 84 | func filter(links []Link) (res []Link) { 85 | for _, l := range links { 86 | // filter external and non-md 87 | isMarkdown := filepath.Ext(l.Target) == "" || filepath.Ext(l.Target) == ".md" 88 | if isInternal(l.Target) && isMarkdown { 89 | res = append(res, l) 90 | } 91 | } 92 | fmt.Printf("Removed %d external and non-markdown links\n", len(links)-len(res)) 93 | return res 94 | } 95 | -------------------------------------------------------------------------------- /walk.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/adrg/frontmatter" 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type Front struct { 17 | Title string `yaml:"title"` 18 | Draft bool `yaml:"draft"` 19 | Tags []string `yaml:"tags"` 20 | } 21 | 22 | // recursively walk directory and return all files with given extension 23 | func walk(root, ext string, index bool, ignorePaths map[string]struct{}) (res []Link, i ContentIndex) { 24 | fmt.Printf("Scraping %s\n", root) 25 | i = make(ContentIndex) 26 | 27 | nPrivate := 0 28 | 29 | formats := []*frontmatter.Format{ 30 | frontmatter.NewFormat("---", "---", yaml.Unmarshal), 31 | } 32 | 33 | start := time.Now() 34 | 35 | err := filepath.WalkDir(root, func(fp string, d fs.DirEntry, e error) error { 36 | if e != nil { 37 | return e 38 | } 39 | 40 | // path normalize fp 41 | s := filepath.ToSlash(fp) 42 | if _, ignored := ignorePaths[s]; ignored { 43 | fmt.Printf("[Ignored] %s\n", d.Name()) 44 | nPrivate++ 45 | } else if filepath.Ext(d.Name()) == ext { 46 | if index { 47 | text := getText(s) 48 | 49 | var matter Front 50 | raw_body, err := frontmatter.Parse(strings.NewReader(text), &matter, formats...) 51 | body := string(raw_body) 52 | if err != nil { 53 | matter = Front{ 54 | Title: "Untitled Page", 55 | Draft: false, 56 | Tags: []string{}, 57 | } 58 | body = text 59 | } 60 | // check if page is private 61 | if !matter.Draft { 62 | info, _ := os.Stat(s) 63 | source := processSource(trim(s, root, ".md")) 64 | 65 | // default title 66 | title := matter.Title 67 | if title == "" { 68 | fileName := d.Name() 69 | title = strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName)) 70 | } 71 | 72 | // default tags 73 | if matter.Tags == nil { 74 | matter.Tags = []string{} 75 | } 76 | 77 | // add to content and link index 78 | i[source] = Content{ 79 | LastModified: info.ModTime(), 80 | Title: title, 81 | Content: body, 82 | Tags: matter.Tags, 83 | } 84 | res = append(res, parse(s, root)...) 85 | } else { 86 | fmt.Printf("[Ignored] %s\n", d.Name()) 87 | nPrivate++ 88 | } 89 | } 90 | } 91 | return nil 92 | }) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | end := time.Now() 98 | 99 | fmt.Printf("[DONE] in %s\n", end.Sub(start).Round(time.Millisecond)) 100 | fmt.Printf("Ignored %d private files \n", nPrivate) 101 | fmt.Printf("Parsed %d total links \n", len(res)) 102 | return res, i 103 | } 104 | 105 | func getText(dir string) string { 106 | // read file 107 | fileBytes, err := ioutil.ReadFile(dir) 108 | if err != nil { 109 | panic(err) 110 | } 111 | 112 | return string(fileBytes) 113 | } 114 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | ) 10 | 11 | func write(links []Link, contentIndex ContentIndex, toIndex bool, out string, root string) error { 12 | index := index(links) 13 | resStruct := struct { 14 | Index Index `json:"index"` 15 | Links []Link `json:"links"` 16 | }{ 17 | Index: index, 18 | Links: links, 19 | } 20 | marshalledIndex, mErr := json.MarshalIndent(&resStruct, "", " ") 21 | if mErr != nil { 22 | return mErr 23 | } 24 | 25 | writeErr := ioutil.WriteFile(path.Join(out, "linkIndex.json"), marshalledIndex, 0644) 26 | if writeErr != nil { 27 | return writeErr 28 | } 29 | 30 | // check whether to index content 31 | if toIndex { 32 | marshalledContentIndex, mcErr := json.MarshalIndent(&contentIndex, "", " ") 33 | if mcErr != nil { 34 | return mcErr 35 | } 36 | 37 | writeErr = ioutil.WriteFile(path.Join(out, "contentIndex.json"), marshalledContentIndex, 0644) 38 | if writeErr != nil { 39 | return writeErr 40 | } 41 | 42 | // write linkmap 43 | writeErr = writeLinkMap(&contentIndex, root) 44 | if writeErr != nil { 45 | return writeErr 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func writeLinkMap(contentIndex *ContentIndex, root string) error { 53 | fp := path.Join(root, "static", "linkmap") 54 | file, err := os.OpenFile(fp, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | datawriter := bufio.NewWriter(file) 60 | for path := range *contentIndex { 61 | if path == "/" { 62 | _, _ = datawriter.WriteString("/index.html /\n") 63 | } else { 64 | _, _ = datawriter.WriteString(path + "/index.{html} " + path + "/\n") 65 | } 66 | } 67 | datawriter.Flush() 68 | file.Close() 69 | 70 | return nil 71 | } 72 | 73 | // constructs index from links 74 | func index(links []Link) (index Index) { 75 | linkMap := make(map[string][]Link) 76 | backlinkMap := make(map[string][]Link) 77 | for _, l := range links { 78 | // backlink (only if internal) 79 | if _, ok := backlinkMap[l.Target]; ok { 80 | backlinkMap[l.Target] = append(backlinkMap[l.Target], l) 81 | } else { 82 | backlinkMap[l.Target] = []Link{l} 83 | } 84 | 85 | // regular link 86 | if _, ok := linkMap[l.Source]; ok { 87 | linkMap[l.Source] = append(linkMap[l.Source], l) 88 | } else { 89 | linkMap[l.Source] = []Link{l} 90 | } 91 | } 92 | index.Links = linkMap 93 | index.Backlinks = backlinkMap 94 | return index 95 | } 96 | --------------------------------------------------------------------------------