├── src
├── check.sh
├── go.mod
├── common_utils.go
├── go.sum
├── html_generation.go
├── linkitall_assets
│ ├── template.html
│ ├── style.css
│ └── main.js
├── gdf_loading.go
├── main.go
├── node_computation.go
└── linkitall_vendor
│ └── leader-line
│ └── leader-line-v1.1.5.min.js
├── aux
├── start_server.bat
└── RELEASE_README.md
├── docs
└── algo-config
│ ├── images
│ ├── node-sorting.png
│ └── level-strategy.png
│ └── README.md
├── examples
└── simple
│ ├── resources
│ ├── microorganisms.pdf
│ ├── pexels-daria-klet-8479585.jpg
│ └── main.html
│ └── graph.yaml
├── LICENSE
└── README.md
/src/check.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | go fmt && go vet && staticcheck
3 |
--------------------------------------------------------------------------------
/aux/start_server.bat:
--------------------------------------------------------------------------------
1 | CALL linkitall.exe --overwrite -i "?" --release --serve --listen ":8101"
2 | PAUSE
3 |
--------------------------------------------------------------------------------
/docs/algo-config/images/node-sorting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charstorm/linkitall/HEAD/docs/algo-config/images/node-sorting.png
--------------------------------------------------------------------------------
/docs/algo-config/images/level-strategy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charstorm/linkitall/HEAD/docs/algo-config/images/level-strategy.png
--------------------------------------------------------------------------------
/examples/simple/resources/microorganisms.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charstorm/linkitall/HEAD/examples/simple/resources/microorganisms.pdf
--------------------------------------------------------------------------------
/examples/simple/resources/pexels-daria-klet-8479585.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/charstorm/linkitall/HEAD/examples/simple/resources/pexels-daria-klet-8479585.jpg
--------------------------------------------------------------------------------
/src/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/charstorm/linkitall/v2
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/alexflint/go-arg v1.4.3 // indirect
7 | github.com/alexflint/go-scalar v1.1.0 // indirect
8 | github.com/otiai10/copy v1.12.0 // indirect
9 | golang.org/x/sys v0.5.0 // indirect
10 | golang.org/x/text v0.12.0 // indirect
11 | gopkg.in/yaml.v2 v2.4.0 // indirect
12 | )
13 |
--------------------------------------------------------------------------------
/src/common_utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | func pushBack[T any](array *[]T, object T) {
4 | *array = append(*array, object)
5 | }
6 |
7 | // func assertEqual[T comparable](expected, actual T) {
8 | // if expected != actual {
9 | // panic(fmt.Sprintf("Expected %v, got %v", expected, actual))
10 | // }
11 | // }
12 |
13 | func getUnique[T comparable](array []T) []T {
14 | seen := make(map[T]bool)
15 | // input -> map
16 | for _, item := range array {
17 | seen[item] = true
18 | }
19 |
20 | uniqueItems := make([]T, 0, len(seen))
21 | // map -> output
22 | for item := range seen {
23 | uniqueItems = append(uniqueItems, item)
24 | }
25 |
26 | return uniqueItems
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023- Vinay Krishnan
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 |
--------------------------------------------------------------------------------
/aux/RELEASE_README.md:
--------------------------------------------------------------------------------
1 | # Linkitall
2 |
3 | This package contains the Linkitall release files (amd64) for Windows and Linux operating systems.
4 | There is no installation required.
5 |
6 | Steps for running the tool are given below.
7 |
8 | ## Windows
9 |
10 | 1. Extract the archive to a suitable location
11 | 2. Execute the batch script `start_server.bat` found inside the extracted directory.
12 | Just double clicking the script file should be enough in most cases.
13 | 3. Provide the path to the directory with graph file when asked (example is provided in the GitHub repository)
14 | 4. Allow permission for networking if asked
15 | 5. Go to `http://127.0.0.1:8101` in a browser to see the graph generated
16 |
17 | The tool will be running in server mode. It follows a simple `update-generate-refresh` cycle.
18 |
19 | 1. Edit/Update the graph file
20 | 2. Press enter in the script window to generate the new HTML for the graph
21 | 3. Refresh the web page
22 | 4. When development is complete, press `q` to quit the tool!
23 |
24 | ## Linux
25 |
26 | Use the following command to run the tool:
27 |
28 | ```bash
29 | linkitall --overwrite --release --serve --listen ":8101" -i path/to/graph/dir
30 | ```
31 |
32 | See the main `README.md` of the linitall repo to understand the meaning of these flags.
33 | For the usage see the `update-generate-refresh` part explained in the Windows section above.
34 |
35 | ## License
36 |
37 | [The license for the project](https://github.com/charstorm/linkitall/blob/main/LICENSE)
38 |
39 | External packages:
40 |
41 | * You can find the list of external Go packages [here](https://github.com/charstorm/linkitall/blob/main/src/go.mod).
42 | Their licenses can be found in their respective repositories.
43 | * The project uses the Javascript package `leader-line` to draw all the connections.
44 | License for it can be found [here](https://anseki.github.io/leader-line/).
45 |
46 |
--------------------------------------------------------------------------------
/src/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
2 | github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
3 | github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
4 | github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY=
8 | github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
11 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
12 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
13 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
14 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
15 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
16 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
17 | golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
18 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
21 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 |
--------------------------------------------------------------------------------
/src/html_generation.go:
--------------------------------------------------------------------------------
1 | // This file deals with the generation of the HTML document based on the processed data
2 | package main
3 |
4 | import (
5 | "html/template"
6 | "os"
7 | )
8 |
9 | // Info about the board (outer board used for holding all the nodes)
10 | type BoardConfigFields struct {
11 | Width int
12 | Height int
13 | }
14 |
15 | type ControlConfigFields struct {
16 | // In release mode, we use CDN for all the vendor files
17 | Release bool
18 | }
19 |
20 | // All the data required for generating HTML page from template is stored in this struct
21 | type TemplateData struct {
22 | // Input GDF Data
23 | GdfData *GdfDataStruct
24 | // With computed fields
25 | Nodes []NodeData
26 | // Board configuration
27 | BoardConfig BoardConfigFields
28 | // Controlling template generation
29 | ControlConfig ControlConfigFields
30 | }
31 |
32 | func computeBoardConfig(gdfData *GdfDataStruct, nodes []NodeData) BoardConfigFields {
33 | extraWidth := 10
34 | nodeBoxWidthPx := gdfData.DisplayConfig.NodeBoxWidthPx
35 | // Not configurable for now (but it shouldn't matter much anyway)
36 | nodeBoxHeightPx := 150
37 | maxWidth := 0
38 | maxHeight := 0
39 | // Initially compute max of left and top. Then add node width and height.
40 | for _, node := range nodes {
41 | if node.ElemFields.LeftPx > maxWidth {
42 | maxWidth = node.ElemFields.LeftPx
43 | }
44 | if node.ElemFields.TopPx > maxHeight {
45 | maxHeight = node.ElemFields.TopPx
46 | }
47 | }
48 |
49 | return BoardConfigFields{maxWidth + nodeBoxWidthPx + extraWidth, maxHeight + nodeBoxHeightPx}
50 | }
51 |
52 | // Constructor for TemplateData. There are some fields like BoardConfig that needs to be
53 | // calculated
54 | func newTemplateData(gdfData *GdfDataStruct,
55 | nodes []NodeData, controlConfig ControlConfigFields) TemplateData {
56 | boardConfig := computeBoardConfig(gdfData, nodes)
57 | return TemplateData{gdfData, nodes, boardConfig, controlConfig}
58 | }
59 |
60 | // The function responsible for generating the final HTML from template
61 | func fillTemplateWriteOutput(templateFile string, data TemplateData, outputFile string) error {
62 | tmpl, err := template.ParseFiles(templateFile)
63 | if err != nil {
64 | return err
65 | }
66 |
67 | writer, err := os.Create(outputFile)
68 | if err != nil {
69 | return err
70 | }
71 | defer writer.Close()
72 |
73 | err = tmpl.Execute(writer, data)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | return nil
79 | }
80 |
--------------------------------------------------------------------------------
/docs/algo-config/README.md:
--------------------------------------------------------------------------------
1 | ## algo-config
2 |
3 | These fields control the behavior of the graph generation algorithm related to
4 | the placements of nodes.
5 |
6 | It is difficult to explain these without an example graph. Consider the following
7 | set of nodes. The images below are based on these nodes.
8 |
9 | ```yaml
10 | - name: A
11 | - name: B
12 | - name: C
13 | depends-on:
14 | - A
15 | - name: D
16 | depends-on:
17 | - A
18 | - C
19 | - name: E
20 | depends-on:
21 | - B
22 | - D
23 | - name: F
24 | depends-on:
25 | - D
26 | - B
27 | ```
28 |
29 | ### node-sorting
30 |
31 | By default, the graph generation places the most fundamental nodes at the bottom
32 | (Node A and B in the graph above).
33 | The default behavior resembles the construction of a building.
34 | Complex ideas are built on the TOP of fundamental ideas.
35 |
36 | However, the common flow of ideas in a book or a webpage is from top to bottom.
37 | For that, we can use `node-sorting: descend`. Here the most fundamental nodes will be
38 | placed at the top. Compare these two graphs:
39 |
40 | 
41 |
42 | Node positions get flipped along the vertical axis.
43 | Also, notice that the arrows still keep a top to bottom flow.
44 |
45 | Note: This behavior of arrows may change in the future.
46 |
47 | ### arrow-direction
48 |
49 | This controls the direction of the arrows. There are two options:
50 | * child2parent (default)
51 | * parent2child
52 |
53 | ### level-strategy
54 |
55 | This controls how the levels are assigned to each node. There are two options:
56 | * bottom2top (default)
57 | * top2bottom
58 |
59 | With `bottom2top`, all the nodes without dependencies get the bottom row.
60 | Other nodes are progressively arranged on the top. The level of a node is 1
61 | more than the max level of all its dependencies. This is usually enough for
62 | most of the use-cases.
63 |
64 | However in some cases, we want to assign levels from the other end.
65 | All the nodes that are not used by any other node as a dependency is assigned
66 | the top row. In other words, all the nodes at the top will have 0 users.
67 | Every other node gets a level 1 below the minimum level of all its users.
68 | This is the strategy used for `top2bottom`.
69 |
70 | 
71 |
72 | It is highly encouraged to try the various strategies on a small graph before
73 | trying anything big.
74 |
--------------------------------------------------------------------------------
/src/linkitall_assets/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
74 |
75 |
80 | {{if .ControlConfig.Release}}
81 |
82 | {{else}}
83 |
84 |
85 | {{end}}
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/linkitall_assets/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | body {
8 | background-attachment: fixed;
9 | background-repeat: no-repeat;
10 | color: #ddd;
11 | font-family: Sans;
12 | background-color: hsl(205, 0%, 17%);
13 | padding: 20px;
14 | }
15 |
16 | .board {
17 | position: relative;
18 | margin: 10px;
19 | margin-left: auto;
20 | margin-right: auto;
21 | margin-top: 80px;
22 | }
23 |
24 | .node {
25 | position: absolute;
26 | z-index: 1;
27 | }
28 |
29 | a, a:link, a:visited, a:hover, a:active {
30 | text-decoration: none;
31 | color: inherit;
32 | }
33 |
34 | .node-content {
35 | position: relative;
36 | min-height: 100px;
37 | background-color: hsl(205, 0%, 13%);
38 | text-align: center;
39 | border: 2px solid hsl(50, 0%, 20%);
40 | border-radius: 10px;
41 | }
42 |
43 | .node-content-inner {
44 | position: absolute;
45 | margin: auto;
46 | top: 50%;
47 | transform: translateY(-50%);
48 | width: 100%;
49 | }
50 |
51 | .node .title {
52 | font-size: 1.2em;
53 | margin: 5px;
54 | color: hsl(50, 0%, 50%);
55 | }
56 |
57 | .node .title a {
58 | color: #eee;
59 | }
60 |
61 | .node .title a:hover {
62 | text-decoration: underline;
63 | color: #2af;
64 | }
65 |
66 | .node .subtitle {
67 | font-size: 1em;
68 | margin: 5px;
69 | color: hsl(50, 0%, 50%);
70 | }
71 |
72 | .link-panel {
73 | display: flex;
74 | width: 100%;
75 | position: static;
76 | min-height: 18px;
77 | }
78 |
79 | .dot-outer {
80 | flex-grow: 1;
81 | }
82 |
83 | .dot {
84 | width: 20px;
85 | margin-left: auto;
86 | margin-right: auto;
87 | text-align: center;
88 | background-color: #444;
89 | color: #eee;
90 | border-radius: 10px;
91 | }
92 |
93 | .link-source {
94 | /* its empty */
95 | }
96 |
97 | .highlighted-node {
98 | border: 2px solid #ccc;
99 | box-shadow: 0px 0px 20px #ccc;
100 | }
101 |
102 | #link-view-panel {
103 | z-index: 2;
104 | display: none;
105 | position: fixed;
106 | top: 0px;
107 | left: 0;
108 | right: 0;
109 | margin-left: auto;
110 | margin-right: auto;
111 | width: 0px;
112 | }
113 |
114 | #link-view-panel .close-button {
115 | margin-top: 10px;
116 | margin-bottom: 5px;
117 | min-width: 100px;
118 | font-size: 1em;
119 | transform: translateX(-50%);
120 | }
121 |
122 | #link-view-panel iframe {
123 | overflow: scroll;
124 | transform: translateX(-50%);
125 | background-color: #111;
126 | box-shadow: 0px 0px 40px #000;
127 | }
128 |
129 | #link-view-panel .div-frame {
130 | transform: translateX(-50%);
131 | background-color: #111e;
132 | box-shadow: 0px 0px 40px #000;
133 | overflow: auto;
134 | justify-content: center;
135 | }
136 |
137 | .div-frame img {
138 | display: block;
139 | margin-left: auto;
140 | margin-right: auto;
141 | }
142 |
143 | @keyframes zoomIn {
144 | 0% {
145 | transform: scale(0.8);
146 | }
147 | 100% {
148 | transform: scale(1);
149 | }
150 | }
151 |
152 | .display-load-effect {
153 | animation: zoomIn 0.5s ease;
154 | }
155 |
156 | ::-webkit-scrollbar {
157 | height: 12px;
158 | width: 12px;
159 | background-color: #111;
160 | }
161 |
162 | ::-webkit-scrollbar-thumb {
163 | background-color: #555;
164 | box-shadow: inset 0 0 8px #000;
165 | border-radius: 12px;
166 | }
167 |
168 | ::-webkit-scrollbar-corner {
169 | background: #111;
170 | }
171 |
172 | button {
173 | cursor: pointer;
174 | outline: 0;
175 | display: inline-block;
176 | text-align: center;
177 | background-color: #444;
178 | border: 1px solid transparent;
179 | padding: 5px;
180 | transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
181 | color: #BBB;
182 | border-color: #555;
183 | }
184 |
185 | button:hover {
186 | color: #CCC;
187 | background-color: #222;
188 | }
189 |
--------------------------------------------------------------------------------
/examples/simple/graph.yaml:
--------------------------------------------------------------------------------
1 | # THIS IS A SIMPLE EXAMPLE CREATED PURELY TO SHOWCASE THE FEATURES OF LINKITALL.
2 | # INFORMATION PROVIDED HERE IS NOT MEANT TO BE RELIABLE.
3 |
4 | # Build this using the following command:
5 | # -> cd path/to/linkitall/repo
6 | # -> linkitall -i examples/simple
7 |
8 | # These will be added to the section of the page.
9 | head-config:
10 | title: Tap Water
11 | description: What is in tap water?
12 | author: Vinay Krishnan
13 |
14 | algo-config:
15 | # Supported: bottom2top (default), top2bottom
16 | level-strategy: top2bottom
17 | # Supported: child2parent (default), parent2child
18 | arrow-direction: child2parent
19 | # Supported: ascend (default), descend
20 | node-sorting: ascend
21 |
22 | resources:
23 | # Page stored in resources locally. Nodes can 'linkto' this page.
24 | # Make sure to copy the resources dir along with the html file if you take it somewhere else.
25 | # Optionally, one can target a specific element id in the page using the target field.
26 | main: resources/main.html
27 | # It can be an external webpage as well
28 | disinfectants_wiki: https://en.wikipedia.org/wiki/Disinfectant
29 | # Local or web images work too
30 | algae_image: resources/pexels-daria-klet-8479585.jpg
31 | # Local or web pdf references also work. As target, use page= (eg: page=10)
32 | # (Note: #view=fit is not part of the filename. It is added to resize the pdf view)
33 | microorganisms_pdf: resources/microorganisms.pdf#view=fit
34 |
35 |
36 | nodes:
37 | - name: tap_water
38 | title: Tap Water
39 | linkto:
40 | # resource defined in section `resources`
41 | resource: main
42 | # the id of the target section
43 | target: tap-water
44 | depends-on:
45 | - pure_water
46 | - impurities
47 |
48 | - name: pure_water
49 | # If title is not provided, it will be guessed based on name
50 | linkto:
51 | resource: main
52 | target: pure-water
53 |
54 | - name: impurities
55 | subtitle: Chemicals, Gases, Organisms
56 | linkto:
57 | resource: main
58 | target: impurities
59 | depends-on:
60 | - dissolved_gases
61 | - disinfectants
62 | - natural_minerals
63 | - microorganisms
64 | - chemicals
65 |
66 | - name: disinfectants
67 | subtitle: Most commonly Chlorine
68 | linkto:
69 | resource: disinfectants_wiki
70 | depends-on:
71 | - chlorine
72 |
73 | - name: dissolved_gases
74 | linkto:
75 | resource: main
76 | target: dissolved-gases
77 |
78 | - name: natural_minerals
79 | linkto:
80 | resource: main
81 | target: natural-minerals
82 |
83 | - name: chlorine
84 | title: Chlorine
85 | subtitle: Most common chemical in water treatment
86 | linkto:
87 | resource: main
88 | target: chlorine
89 |
90 | - name: microorganisms
91 | title: Micro-organisms
92 | subtitle: Tiny, yet sometimes harmful
93 | linkto:
94 | resource: main
95 | target: microorganisms
96 | depends-on:
97 | - algae
98 | - bacteria
99 | - fungi
100 | - protozoa
101 | - viruses
102 |
103 | - name: chemicals
104 | title: Chemicals
105 | subtitle: From various sources
106 | linkto:
107 | resource: main
108 | target: chemicals
109 |
110 | - name: algae
111 | title: Algae
112 | linkto:
113 | # Using the image resource
114 | resource: algae_image
115 |
116 | - name: bacteria
117 | title: Bacteria
118 | linkto:
119 | resource: microorganisms_pdf
120 | target: page=2
121 |
122 | - name: fungi
123 | title: Fungi
124 | subtitle: Has 'fun' in name
125 | linkto:
126 | resource: microorganisms_pdf
127 | target: page=3
128 |
129 | - name: protozoa
130 | title: Protozoa
131 | subtitle: No idea what this is
132 | linkto:
133 | resource: microorganisms_pdf
134 | target: page=4
135 |
136 | - name: viruses
137 | title: Viruses
138 | linkto:
139 | resource: microorganisms_pdf
140 | target: page=5
141 |
--------------------------------------------------------------------------------
/src/linkitall_assets/main.js:
--------------------------------------------------------------------------------
1 | // This script is used by the generated HTML page containing the graph.
2 | //
3 | // Thanks to:
4 | // - https://anseki.github.io/leader-line/
5 | // Used for making connections.
6 | // Note: These entities are not associated with the project.
7 |
8 | let links = []
9 | let imgWidth = "60%"
10 | let _buildConfig = null
11 |
12 | // just renaming the function to a simpler one
13 | function id2el(idstr) {
14 | return document.getElementById(idstr)
15 | }
16 |
17 | function getBuildConfig() {
18 | if (_buildConfig != null) {
19 | return _buildConfig
20 | }
21 | const elem = id2el("buildconfig")
22 | _buildConfig = JSON.parse(elem.innerHTML)
23 | return _buildConfig
24 | }
25 |
26 | function getBaseUrl(url) {
27 | return url.split(/[?#]/)[0]
28 | }
29 |
30 | function removeAndAddClass(elem, className) {
31 | elem.classList.remove(className)
32 | elem.classList.add(className)
33 | }
34 |
35 | function isImageFile(filename) {
36 | let checkExt = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg']
37 | let lowerFilename = filename.toLowerCase()
38 | return checkExt.some(ext => lowerFilename.endsWith(ext))
39 | }
40 |
41 | function getLinkOptions(source, target, color) {
42 | let sourceTop = source.getBoundingClientRect().top
43 | let targetTop = target.getBoundingClientRect().top
44 |
45 | let startSocket = 'bottom'
46 | let endSocket = 'top'
47 | if (targetTop < sourceTop) {
48 | startSocket = 'top'
49 | endSocket = 'bottom'
50 | }
51 |
52 | let options = {
53 | startSocket,
54 | endSocket,
55 | color,
56 | size: 2
57 | }
58 |
59 | return options
60 | }
61 |
62 | // Find all the link-source dots and connect them to their target dot.
63 | function connectDots() {
64 | // Template used to color links and their dots
65 | const colorTemplate = "hsl({hue}, 40%, 50%)"
66 | // we will cycle over different values of hue
67 | let hue = 0
68 | const linkSources = document.getElementsByClassName("link-source")
69 |
70 | for (let idx=0; idx < linkSources.length; idx++) {
71 | let source = linkSources[idx]
72 | const sourceId = source.id
73 | if (!sourceId.startsWith("D_")) {
74 | console.log(`ERROR: source ${source} does not start with D_`)
75 | continue
76 | }
77 |
78 | const targetId = sourceId.replace(/^D_/, "U_")
79 | let target = id2el(targetId)
80 | if (target == null) {
81 | console.log(`ERROR: no target dot with id ${targetId}`)
82 | continue
83 | }
84 |
85 | // Color for this link
86 | let color = colorTemplate.replace("{hue}", hue.toString())
87 | hue = (hue + 67) % 360
88 | source.style.backgroundColor = color
89 | target.style.backgroundColor = color
90 |
91 | let buildConfig = getBuildConfig()
92 | if (buildConfig.ArrowDirection == "parent2child") {
93 | // Swap source and target in this case
94 | let temp = source
95 | source = target
96 | target = temp
97 | }
98 |
99 | let link = new LeaderLine(source, target)
100 | link.setOptions(getLinkOptions(source, target, color))
101 | links.push(link)
102 | }
103 | }
104 |
105 | function main() {
106 | connectDots()
107 | }
108 |
109 | document.addEventListener("DOMContentLoaded", main)
110 |
111 | // When clicking the link box, focus and show the target node.
112 | function showNode(nodeId) {
113 | const elem = id2el(nodeId)
114 | elem.scrollIntoView({behavior: "smooth", block: "center", inline: "center"})
115 | elem.classList.add("highlighted-node")
116 |
117 | // TODO: this looks so wrong. We need a *better* way to highlight a node.
118 | setTimeout(() => {
119 | elem.classList.remove("highlighted-node")
120 | }, 1500)
121 | }
122 |
123 | // Use state=true to enable link-view-panel and state=false to hide it.
124 | function setLinkViewPatelState(state) {
125 | let linkViewOuterElem = id2el("link-view-panel")
126 | if (state) {
127 | linkViewOuterElem.style.zIndex = "3"
128 | linkViewOuterElem.style.display = "block"
129 | document.body.style.overflow = 'hidden'
130 | } else if (id2el("link-view-panel").style.display != "none") {
131 | linkViewOuterElem.style.zIndex = "-1"
132 | linkViewOuterElem.style.display = "none"
133 | document.body.style.overflow = 'visible'
134 | }
135 | }
136 |
137 | var currentLinkViewUrl = ""
138 |
139 | function getWidthAndHeightForFrame() {
140 | let height = window.innerHeight - 100
141 | let width = Math.floor(window.innerWidth * 0.8)
142 | if (width < 800) {
143 | width = 800
144 | }
145 | return [width, height]
146 | }
147 |
148 | function withpx(val) {
149 | return `${val}px`
150 | }
151 |
152 | function openInIframe(url, targetParent) {
153 | let [width, height] = getWidthAndHeightForFrame()
154 |
155 | // Open link in an iframe and insert it into the inner div
156 | let iframe = document.createElement("iframe")
157 | iframe.src = url
158 | iframe.width = withpx(width)
159 | iframe.height = withpx(height)
160 | iframe.frameBorder="0"
161 | iframe.onload = () => {
162 | setLinkViewPatelState(true)
163 | iframe.focus()
164 | }
165 | targetParent.appendChild(iframe)
166 | }
167 |
168 | function openInDiv(url, targetParent) {
169 | let [width, height] = getWidthAndHeightForFrame()
170 |
171 | targetParent.innerHTML = `
172 |
173 |
174 |
175 | `
176 | let divFrame = id2el("div-frame")
177 | divFrame.style.width = withpx(width)
178 | divFrame.style.height = withpx(height)
179 |
180 | setLinkViewPatelState(true)
181 | removeAndAddClass(targetParent, "display-load-effect")
182 | }
183 |
184 | // Open panel for viewing the target url.
185 | // If we open the same link again, reuse the iframe.
186 | function openNodeLink(evt, url, aux) {
187 | evt.preventDefault()
188 |
189 | // In case of middle-click or ctrl-click, open link in a new tab
190 | if (aux || (evt.ctrlKey == true)) {
191 | window.open(url, "newTab")
192 | return
193 | }
194 |
195 | if (url == currentLinkViewUrl) {
196 | setLinkViewPatelState(true)
197 | return
198 | }
199 |
200 | // clear contents of the link-view-inner
201 | let inner = id2el("link-view-inner")
202 | inner.textContent = ""
203 |
204 | let baseUrl = getBaseUrl(url)
205 | if (isImageFile(baseUrl)) {
206 | // Images are open witout an iframe, with a simple div
207 | openInDiv(url, inner)
208 | } else {
209 | // Everything else opened with an iframe
210 | openInIframe(url, inner)
211 | }
212 | currentLinkViewUrl = url
213 | }
214 |
215 | function closeLinkViewPanel() {
216 | setLinkViewPatelState(false)
217 | }
218 |
219 |
220 | function zoomFrameImg(key) {
221 | let minWidth = 100
222 | let maxWidth = 4000
223 | let elem = id2el("frame-img")
224 | if (elem == null) {
225 | return
226 | }
227 | let width = elem.width
228 | if ((key === "[") && (width > minWidth)) {
229 | imgWidth = width * 0.9
230 | elem.width = imgWidth
231 | }
232 | if ((key === "]") && (width < maxWidth)) {
233 | imgWidth = width * 1 / 0.9
234 | elem.width = imgWidth
235 | }
236 | }
237 |
238 |
239 | // Handle escape keypress.
240 | // Close the link view panel when pressing escape.
241 | document.onkeydown = function(evt) {
242 | if(evt.key === "Escape") {
243 | closeLinkViewPanel()
244 | }
245 |
246 | if((evt.key === "[") || (evt.key === "]")) {
247 | zoomFrameImg(evt.key)
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/src/gdf_loading.go:
--------------------------------------------------------------------------------
1 | // This file handles the loading of Graph Definition File (GDF)
2 | // Contains loading of data to struct, checking input values, and filling optional fields.
3 | // Some of the checks may be redundant as similar checks may be present in modules that actually
4 | // use the data.
5 | package main
6 |
7 | import (
8 | "fmt"
9 | "io"
10 | "os"
11 | "regexp"
12 | "strings"
13 |
14 | "golang.org/x/text/cases"
15 | "golang.org/x/text/language"
16 | "gopkg.in/yaml.v2"
17 | )
18 |
19 | var name_pattern = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
20 | var importance_pattern = regexp.MustCompile(`^(lowest|lower|low|normal|high|higher|highest)$`)
21 |
22 | // Used in the of the final HTML
23 | type HeadConfigFields struct {
24 | Title string
25 | Description string
26 | Author string
27 | }
28 |
29 | // Configuration related to positioning
30 | type DisplayConfigFields struct {
31 | // Size of horizontal grid step
32 | HorizontalStepPx int `yaml:"horizontal-step-px,omitempty"`
33 | // Size of vertical grid step
34 | VerticalStepPx int `yaml:"vertical-step-px,omitempty"`
35 | // Width of the node box
36 | NodeBoxWidthPx int `yaml:"node-box-width-px,omitempty"`
37 | }
38 |
39 | type LinkToFields struct {
40 | // Resource name to be linked to
41 | ResourceName string `yaml:"resource"`
42 | // A target for the final resource (page/section/div-id) etc.
43 | Target string `yaml:"target"`
44 | }
45 |
46 | type ResourceConfigMap map[string]string
47 |
48 | // Defines the node definition by the user in the
49 | type NodeInputFields struct {
50 | // A unique name for the node (no spaces, all small letters)
51 | Name string
52 | // Title of the node (shown in big font)
53 | Title string
54 | // Subtitle (shown in smaller font or sometimes omitted)
55 | Subtitle string `yaml:"subtitle,omitempty"`
56 | // Importance to be assigned to this node. It is a 7 point scale:
57 | // lowest, lower, low, normal, high, higher, highest
58 | Importance string `yaml:"importance,omitempty"`
59 | // List of node names (current node depends on these nodes)
60 | DependsOn []string `yaml:"depends-on,omitempty"`
61 | // Link to the resource
62 | LinkTo LinkToFields `yaml:"linkto,omitempty"`
63 | }
64 |
65 | type AlgoConfigFields struct {
66 | LevelStrategy string `yaml:"level-strategy,omitempty"`
67 | ArrowDirection string `yaml:"arrow-direction,omitempty"`
68 | NodeSorting string `yaml:"node-sorting,omitempty"`
69 | }
70 |
71 | type GdfDataStruct struct {
72 | Nodes []NodeInputFields `yaml:"nodes"`
73 | HeadConfig HeadConfigFields `yaml:"head-config"`
74 | DisplayConfig DisplayConfigFields `yaml:"display-config,omitempty"`
75 | ResourceConfig ResourceConfigMap `yaml:"resources"`
76 | AlgoConfig AlgoConfigFields `yaml:"algo-config,omitempty"`
77 | }
78 |
79 | func validateAndUpdateAlgoConfig(algoConfig *AlgoConfigFields) error {
80 | if len(algoConfig.LevelStrategy) == 0 {
81 | algoConfig.LevelStrategy = "bottom2top"
82 | }
83 | if algoConfig.LevelStrategy != "bottom2top" && algoConfig.LevelStrategy != "top2bottom" {
84 | return fmt.Errorf("invalid level strategy: '%v'", algoConfig.LevelStrategy)
85 | }
86 |
87 | if len(algoConfig.ArrowDirection) == 0 {
88 | algoConfig.ArrowDirection = "child2parent"
89 | }
90 | if algoConfig.ArrowDirection != "child2parent" && algoConfig.ArrowDirection != "parent2child" {
91 | return fmt.Errorf("invalid arrow direction: '%v'", algoConfig.ArrowDirection)
92 | }
93 |
94 | if len(algoConfig.NodeSorting) == 0 {
95 | algoConfig.NodeSorting = "ascend"
96 | }
97 | if algoConfig.NodeSorting != "ascend" && algoConfig.NodeSorting != "descend" {
98 | return fmt.Errorf("invalid growth strategy: '%v'", algoConfig.NodeSorting)
99 | }
100 | return nil
101 | }
102 |
103 | func validateAndUpdateDisplayConfig(displayConfig *DisplayConfigFields) error {
104 | if displayConfig.HorizontalStepPx == 0 {
105 | displayConfig.HorizontalStepPx = 400
106 | }
107 | if displayConfig.VerticalStepPx == 0 {
108 | displayConfig.VerticalStepPx = 300
109 | }
110 | if displayConfig.NodeBoxWidthPx == 0 {
111 | displayConfig.NodeBoxWidthPx = 300
112 | }
113 | return nil
114 | }
115 |
116 | // Replace underscores with spaces and capitalize first letter of every word.
117 | func convertNameToTitle(name string) string {
118 | spacedStr := strings.ReplaceAll(name, "_", " ")
119 | // We have to do some extra-steps to do since strings.Title is deprecated. :eyeroll:
120 | caser := cases.Title(language.English)
121 | result := caser.String(spacedStr)
122 | return result
123 | }
124 |
125 | // Validate data related to nodes in GDF
126 | // This function changes blank ("") value for node.Importance to "normal".
127 | func validateAndUpdateNodes(nodes []NodeInputFields) error {
128 | // Number of nodes without any dependencies (level 0 nodes)
129 | numLevel0Nodes := 0
130 | // Unique nodes (names)
131 | uniqueNames := map[string]bool{}
132 | for idx := range nodes {
133 | node := &nodes[idx]
134 |
135 | // CHECK: node name must be [a-zA-Z0-9_]
136 | if !name_pattern.MatchString(node.Name) {
137 | return fmt.Errorf("invalid node name (only letters, numbers, _) '%v'", node.Name)
138 | }
139 |
140 | // CHECK: node name must be unique
141 | if _, ok := uniqueNames[node.Name]; ok {
142 | return fmt.Errorf("node name repeated '%v'", node.Name)
143 | }
144 | uniqueNames[node.Name] = true
145 |
146 | if node.Importance == "" {
147 | node.Importance = "normal"
148 | }
149 | // CHECK: importance must be one of the 7 options
150 | if !importance_pattern.MatchString(node.Importance) {
151 | return fmt.Errorf("unknown importance pattern for node '%v': '%v'",
152 | node.Name, node.Importance)
153 | }
154 |
155 | if len(node.DependsOn) == 0 {
156 | // These nodes do not depend on any nodes
157 | numLevel0Nodes += 1
158 | }
159 |
160 | // If the title is not given, fill it using the name.
161 | if len(node.Title) == 0 {
162 | node.Title = convertNameToTitle(node.Name)
163 | }
164 | }
165 |
166 | if numLevel0Nodes == 0 {
167 | return fmt.Errorf("there must be atleast 1 node without any dependency")
168 | }
169 |
170 | for _, node := range nodes {
171 | for _, dep := range node.DependsOn {
172 | // CHECK: dependency must be one of the node names
173 | if _, ok := uniqueNames[dep]; !ok {
174 | return fmt.Errorf("unknown dependency for node '%v': '%v'", node.Name, dep)
175 | }
176 | }
177 | }
178 |
179 | return nil
180 | }
181 |
182 | // Goes over each source in resources and makes sure the input is proper.
183 | // Also iterates over the nodes and makes sure all the resources are available.
184 | // TODO: check if the specified resource file actually exists!
185 | func validateAndUpdateResources(resources ResourceConfigMap, nodes []NodeInputFields) error {
186 | // Check all nodes are using resources actually present in the GDF
187 | for idx := range nodes {
188 | node := &nodes[idx]
189 | if len(node.LinkTo.ResourceName) == 0 {
190 | continue
191 | }
192 | _, ok := resources[node.LinkTo.ResourceName]
193 | if !ok {
194 | return fmt.Errorf("error in node %s: linkto resource %s not found",
195 | node.Name, node.LinkTo.ResourceName)
196 | }
197 | }
198 |
199 | return nil
200 | }
201 |
202 | // Validate graph data loaded from YAML
203 | // Input must not be nil.
204 | func validateAndUpdateGraphData(data *GdfDataStruct) error {
205 | err := validateAndUpdateNodes(data.Nodes)
206 | if err != nil {
207 | return err
208 | }
209 |
210 | err = validateAndUpdateDisplayConfig(&data.DisplayConfig)
211 | if err != nil {
212 | return err
213 | }
214 |
215 | err = validateAndUpdateAlgoConfig(&data.AlgoConfig)
216 | if err != nil {
217 | return err
218 | }
219 |
220 | err = validateAndUpdateResources(data.ResourceConfig, data.Nodes)
221 | if err != nil {
222 | return err
223 | }
224 |
225 | return nil
226 | }
227 |
228 | // Load Graph Definition File
229 | //
230 | // Inputs:
231 | // filename - input filename (YAML file for GDF)
232 | //
233 | // Returns: (data, readable, error)
234 | // data - loaded data (if everything goes fine)
235 | // readable - true if file is readable
236 | // error - error if any
237 | func loadGdf(filename string) (*GdfDataStruct, bool, error) {
238 | file, err := os.Open(filename)
239 | if err != nil {
240 | return nil, false, err
241 | }
242 | defer file.Close()
243 |
244 | fileData, err := io.ReadAll(file)
245 | if err != nil {
246 | return nil, false, err
247 | }
248 |
249 | var data GdfDataStruct
250 | err = yaml.UnmarshalStrict(fileData, &data)
251 | if err != nil {
252 | return nil, true, err
253 | }
254 |
255 | err = validateAndUpdateGraphData(&data)
256 | if err != nil {
257 | return nil, true, err
258 | }
259 |
260 | return &data, true, nil
261 | }
262 |
--------------------------------------------------------------------------------
/src/main.go:
--------------------------------------------------------------------------------
1 | // This file handles the conversion of Graph in YAML format to HTML
2 |
3 | // Important uncommon shortforms used:
4 | // GDF - Graph Definition File (usually in YAML)
5 |
6 | // TODO: Add an output directory option for this tool. As of now, input directory is the target
7 | // directory where we will keep all the files. When used in serve mode, we serve files from the
8 | // input directory.
9 |
10 | package main
11 |
12 | import (
13 | // "html/template"
14 | "bufio"
15 | "fmt"
16 | "log"
17 | "net/http"
18 | "os"
19 | "path/filepath"
20 | "strings"
21 | "time"
22 |
23 | argparse "github.com/alexflint/go-arg"
24 | copylib "github.com/otiai10/copy"
25 | )
26 |
27 | // Some fields (GraphFile, OutFile) are basepaths (just the filename without dir).
28 | // For these, full path is attached by the getInputsForProcessing() function.
29 | type CliArgs struct {
30 | ServerMode bool `arg:"-s,--serve" help:"run in edit-update-serve mode"`
31 | Release bool `arg:"-r,--release" help:"run in release mode"`
32 | ServerAddr string `arg:"-l,--listen" default:":8101" help:"listen address in serve mode"`
33 | InputDir string `arg:"-i,--indir,required" help:"path to the input directory"`
34 | GraphFile string `arg:"-g,--graph" default:"graph.yaml" help:"input graph base filename"`
35 | OutFile string `arg:"-o,--out" default:"index.html" help:"output html base filename"`
36 | Overwrite bool `arg:"--overwrite" help:"overwrite asset files"`
37 | }
38 |
39 | var bufferedStdin *bufio.Reader = bufio.NewReader(os.Stdin)
40 |
41 | // Return path to the assets directory inside indir
42 | func getPathToAssetDir(indir string) string {
43 | return filepath.Join(indir, "linkitall_assets")
44 | }
45 |
46 | // Similar to the above, but for vendor directory
47 | func getPathToVendorDir(indir string) string {
48 | return filepath.Join(indir, "linkitall_vendor")
49 | }
50 |
51 | // Check if the give `path` is accessible.
52 | // kind can be "file" or "dir"
53 | func isPathAccessible(path string, kind string) bool {
54 | stat, err := os.Stat(path)
55 | if err != nil {
56 | return false
57 | }
58 |
59 | result := false
60 | if kind == "dir" {
61 | result = stat.IsDir()
62 | } else if kind == "file" {
63 | file, err := os.Open(path)
64 | if err == nil {
65 | result = true
66 | file.Close()
67 | }
68 | }
69 | return result
70 | }
71 |
72 | // Check if file can be written
73 | func canFileWrite(path string) bool {
74 | file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE, 0644)
75 | if err != nil {
76 | return false
77 | }
78 | file.Close()
79 | return true
80 | }
81 |
82 | // Parse arguments and perform steps to prepare input for processing.
83 | // If --indir is specified "?", get the input path from the user via stdin.
84 | // Final InputDir path is converted to absolute path.
85 | // Check for existence of indir and graph file.
86 | func getInputsForProcessing() (CliArgs, error) {
87 | var args CliArgs
88 | if len(os.Args) == 1 {
89 | fmt.Printf("No args. Use --help\n")
90 | os.Exit(1)
91 | }
92 | argparse.MustParse(&args)
93 |
94 | if args.InputDir == "?" {
95 | fmt.Printf("Enter input directory => ")
96 | line, err := bufferedStdin.ReadString('\n')
97 | if err != nil {
98 | return args, err
99 | }
100 | line = strings.TrimSpace(line)
101 | args.InputDir = line
102 | }
103 |
104 | if !isPathAccessible(args.InputDir, "dir") {
105 | return args, fmt.Errorf("input dir not accessible: %s", args.InputDir)
106 | }
107 |
108 | absInputDir, err := filepath.Abs(args.InputDir)
109 | if err != nil {
110 | return args, err
111 | }
112 |
113 | args.InputDir = absInputDir
114 | args.GraphFile = filepath.Join(args.InputDir, args.GraphFile)
115 | if !isPathAccessible(args.GraphFile, "file") {
116 | return args, fmt.Errorf("unable to find graph file: %s", args.GraphFile)
117 | }
118 | // Fill full path to input and output
119 | args.OutFile = filepath.Join(args.InputDir, args.OutFile)
120 | if !canFileWrite(args.OutFile) {
121 | return args, fmt.Errorf("unable to open file for writing: %s", args.OutFile)
122 | }
123 |
124 | return args, nil
125 | }
126 |
127 | // Return path to the parent dir where the executable is.
128 | func getExecutableDir() (string, error) {
129 | execFile, err := os.Executable()
130 | if err != nil {
131 | return "", err
132 | }
133 |
134 | execDir := filepath.Dir(execFile)
135 | absExecDir, err := filepath.Abs(execDir)
136 | if err != nil {
137 | return "", err
138 | }
139 |
140 | return absExecDir, nil
141 | }
142 |
143 | // Copy all the assets files to the target directory where the output will be generated.
144 | // The asset files (source) are located in the same directory of the executable.
145 | func copyAssetsAndVendorFilesToDir(targetDir string, overwrite bool, release bool) error {
146 | execDir, err := getExecutableDir()
147 | if err != nil {
148 | return err
149 | }
150 |
151 | assetPath := getPathToAssetDir(execDir)
152 | targetAssetPath := getPathToAssetDir(targetDir)
153 | if !overwrite && isPathAccessible(targetAssetPath, "dir") {
154 | log.Printf("Asset dir %s already exists. Skipping copying assets\n", targetAssetPath)
155 | } else {
156 | log.Printf("Copy %s -> %s\n", assetPath, targetAssetPath)
157 | copylib.Copy(assetPath, targetAssetPath)
158 | }
159 |
160 | vendorPath := getPathToVendorDir(execDir)
161 | targetVendorPath := getPathToVendorDir(targetDir)
162 | if release {
163 | log.Printf("In release mode. Not copying vendor dir")
164 | } else if !overwrite && isPathAccessible(targetVendorPath, "dir") {
165 | log.Printf("Vendor dir %s already exists. Skipping copying vendor\n", targetVendorPath)
166 | } else {
167 | log.Printf("Copy %s -> %s\n", vendorPath, targetVendorPath)
168 | copylib.Copy(vendorPath, targetVendorPath)
169 | }
170 |
171 | return nil
172 | }
173 |
174 | // ** This is the core function which does all the processing **
175 | // Process Graph Data File (GDF) and writes the HTML output.
176 | // The `indir` is also the target dir. Output is generated at the same location.
177 | // Copy the required asset dir to the `indir` before calling this function.
178 | func processGraphWriteOutput(args *CliArgs) error {
179 | log.Printf("Reading graph: %s\n", args.GraphFile)
180 | gdfData, readable, err := loadGdf(args.GraphFile)
181 | if !readable {
182 | log.Fatalf("graph file %s not readable: %s\n", args.GraphFile, err)
183 | }
184 |
185 | if err != nil {
186 | return err
187 | }
188 |
189 | log.Printf("Preparing nodes\n")
190 | nodes, err := createComputeAndFillNodeDataList(gdfData)
191 | if err != nil {
192 | return err
193 | }
194 | log.Printf("Number of nodes: %d\n", len(nodes))
195 |
196 | log.Printf("Generating template data\n")
197 | controlConfig := ControlConfigFields{
198 | Release: args.Release,
199 | }
200 | templateData := newTemplateData(gdfData, nodes, controlConfig)
201 |
202 | targetAssetDir := getPathToAssetDir(args.InputDir)
203 | templateFile := filepath.Join(targetAssetDir, "template.html")
204 |
205 | log.Printf("Filling template and writing output\n")
206 | err = fillTemplateWriteOutput(templateFile, templateData, args.OutFile)
207 | if err != nil {
208 | return err
209 | }
210 |
211 | log.Printf("Done\n")
212 | return nil
213 | }
214 |
215 | // Process the graph file. Print error if any.
216 | func processAndLogError(args *CliArgs) {
217 | err := processGraphWriteOutput(args)
218 |
219 | if err != nil {
220 | log.Printf("Error: %s", err)
221 | }
222 | }
223 |
224 | // In server mode, we run a http server on the target directory.
225 | // We also run a read-update cycle to update the output file.
226 | func runInServerMode(args *CliArgs) {
227 | // Run processing once before starting server
228 | processAndLogError(args)
229 |
230 | // Start server on the target dir
231 | fileServer := http.FileServer(http.Dir(args.InputDir))
232 | go func() {
233 | log.Printf("Starting server for dir %s. Listening at %s\n",
234 | args.InputDir, args.ServerAddr)
235 | http.ListenAndServe(args.ServerAddr, fileServer)
236 | }()
237 |
238 | time.Sleep(time.Second)
239 |
240 | // Run read-update cycle
241 | for {
242 | fmt.Printf("\nq: quit, enter: update output => ")
243 | line, err := bufferedStdin.ReadString('\n')
244 | if err != nil {
245 | log.Fatalf("unable to read from stdin. %s", err)
246 | }
247 |
248 | line = strings.TrimSpace(line)
249 | if line == "q" {
250 | break
251 | } else if len(line) > 0 {
252 | log.Printf("Warning: Ignoring input: '%s'", line)
253 | continue
254 | }
255 | processAndLogError(args)
256 | }
257 | }
258 |
259 | func main() {
260 | args, err := getInputsForProcessing()
261 | if err != nil {
262 | log.Fatalf("unable to read args. %s", err)
263 | }
264 |
265 | // Check if the graph file exists. It is the input file. User only specifies the dir.
266 |
267 | // It doesn't matter whether we are running in server mode or not. We always copy the asset
268 | // files to the target dir (input dir in this case).
269 | err = copyAssetsAndVendorFilesToDir(args.InputDir, args.Overwrite, args.Release)
270 | if err != nil {
271 | log.Fatalf("unable to copy asset files to %s", args.InputDir)
272 | }
273 |
274 | if args.ServerMode {
275 | runInServerMode(&args)
276 | } else {
277 | err = processGraphWriteOutput(&args)
278 | }
279 |
280 | if err != nil {
281 | log.Fatalf("error while processing %s", err)
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | Linkitall lets you create a clear, visual map showing how different ideas are related. This map, structured as a dependency graph, shows the hierarchy and connections between concepts. To use it, you simply create a YAML file describing the graph structure, and Linkitall then generates an HTML file with clickable nodes and links. It's especially useful for making educational or reference content, offering a graphical view of topics.
4 |
5 | Linkitall also helps deepen understanding by exploring dependencies. As we ask "why" or "how," we follow these links, uncovering complex relationships that build our knowledge. This process improves both understanding and critical thinking.
6 |
7 | An example: [Trigonometric Relations](https://charstorm.github.io/class-11-12-india/class11/maths/trigonometry/relations/)
8 |
9 | ## Usage
10 |
11 | Download the latest release zipfile and follow the instructions in the README.md inside.
12 |
13 | If the release files fail due to some reason, please build the tool from the source as explained below.
14 |
15 | ## Build
16 |
17 | The steps for building the tool from the source are given here.
18 |
19 | Clone the source files locally:
20 |
21 | ```bash
22 | git clone https://github.com/charstorm/linkitall.git
23 | ```
24 |
25 | The source files are located in `linkitall/src` directory. The project is written in Golang and is tested on version `go1.20.7`.
26 |
27 | CD to the `src` directory, build the tool, and check the result:
28 |
29 | ```bash
30 | cd linkitall/src
31 | go build
32 | ./linkitall --help
33 | ```
34 |
35 | To make the tool available globally, it is required to add the directory path to the PATH environment variable.
36 |
37 | ```bash
38 | export PATH="$PATH:/path/to/linkitall/src/"
39 | ```
40 |
41 | ## Graph Generation
42 |
43 | The tool takes the path to a directory containing the graph file `graph.yaml` and its dependent resources as the input. See `examples/simple` for a quick reference.
44 | More details about the graph file are explained in the section "Graph File",
45 |
46 | To run the graph generation, execute the following command (assuming `targetdir` is the directory of graph file and its resources):
47 |
48 | ```bash
49 | linkitall -i targetdir
50 | ```
51 |
52 | If executed successfully, it will generate an `index.html` file at `targetdir`. Additionally, asset files (CSS, JS, etc) required for the generated HTML will be copied to the `targetdir` with name `linkitall_assets`. These are initially located in the same directory of the `linkitall` tool. However, it is a one-time action. Subsequent invocation of the tool will skip this copy-assets step. Same is true for the vendor files (3rd party libraries) used by the project. They are stored in `linkitall_vendor` directory.
53 |
54 | One can open the generated HTML file in a browser and see the result.
55 |
56 | ### CLI
57 |
58 | ```
59 | Usage: linkitall [--serve] [--release] [--listen LISTEN] --indir INDIR [--graph GRAPH] [--out OUT] [--overwrite]
60 |
61 | Options:
62 | --serve, -s run in edit-update-serve mode
63 | --release, -r run in release mode
64 | --listen LISTEN, -l LISTEN
65 | listen address in serve mode [default: :8101]
66 | --indir INDIR, -i INDIR
67 | path to the input directory
68 | --graph GRAPH, -g GRAPH
69 | input graph base filename [default: graph.yaml]
70 | --out OUT, -o OUT output html base filename [default: index.html]
71 | --overwrite overwrite asset files
72 | --help, -h display this help and exit
73 | ```
74 |
75 | 1. `serve` - to run in server mode. See below.
76 | 2. `release` - run in release mode. This includes:
77 | - use CDN for links, instead of local vendor files.
78 | 3. `listen` - the address to listen to (eg: ":8101") in the server mode.
79 | 4. `indir` - input (or target) directory containing the graph file.
80 | 5. `graph` - base-name of the graph file (eg: "main.yaml") inside `indir`.
81 | 6. `out` - base-name of the output file to be created inside `indir`.
82 |
83 |
84 | ### Server Mode
85 |
86 | The default behavior of the tool is to run the generation process only once. This is not ideal for development. For that, we have added a server mode, which can be enabled by the -s flag.
87 |
88 | ```bash
89 | linkitall -s -i targetdir
90 | ```
91 |
92 | This will start an HTTP development server at default port 8101. One can see the results by visiting http://127.0.0.1:8101 .
93 |
94 | The tool will wait for user input to update the generated graph. Enter will trigger a graph generation, q will quit the tool.
95 |
96 | With this development process for the graph will be as follows:
97 |
98 | 1. Make changes to the graph file
99 |
100 | 2. Press Enter to trigger a graph generation
101 |
102 | 1. If there are errors, fix them and continue
103 |
104 | 3. Refresh the webpage to see the updated graph
105 |
106 | ## Graph File
107 |
108 | The Graph Definition File (GDF) is a YAML file with different sections.
109 | The default filename expected for the file is `graph.yaml`.
110 | Different sections of the graph file are explained below.
111 |
112 | ### head-config
113 | These fields will be forwarded to the `head` section of the output html file.
114 | Example:
115 | ```yaml
116 | head-config:
117 | title: Awesome Graph
118 | description: A long description
119 | author: Someone
120 | ```
121 |
122 | ### display-config
123 | These fields control the size and spacing of nodes in the graph.
124 | Example:
125 | ```yaml
126 | display-config:
127 | # Horizontal spacing
128 | horizontal-step-px: 400
129 | # Vertical spacing
130 | vertical-step-px: 300
131 | # Width of each node
132 | node-box-width-px: 300
133 | # There is no configuration for the node height from the graph file.
134 | ```
135 | The numerical values shown in the configuration above are the default values.
136 |
137 | ### resources
138 | These are the resources used in the graph.
139 | Resources can be images, pdf-files, html pages, etc.
140 | When clicked on that node, the corresponding link will be opened.
141 | Each node can "linkto" a resource.
142 | Multiple nodes can share the same resource.
143 | Example:
144 | ```yaml
145 | # The keys will be used in the "linkto" field of each node
146 | resources:
147 | # An internal link
148 | main: resources/main.html
149 | # An external link
150 | disinfectants_wiki: https://en.wikipedia.org/wiki/Disinfectant
151 | # Local or web images work too
152 | algae_image: resources/pexels-daria-klet-8479585.jpg
153 | # Local or web pdf references also work.
154 | # (Note: #view=fit is not part of the filename. It is added to resize the pdf view)
155 | microorganisms_pdf: resources/microorganisms.pdf#view=fit
156 | ```
157 | An explanation of using these references will be provided below.
158 |
159 | ### nodes
160 | This is a list of data dictionaries for each node in the graph.
161 | Example:
162 | ```yaml
163 | nodes:
164 | # Expects lowercase, without space, must be unique
165 | - name: tap_water
166 | # This the the text that will be shown on the node
167 | title: Tap Water
168 | # Text that will be shown below title
169 | subtitle: Node about tap water
170 | # Resource information for this node
171 | linkto:
172 | # Resource defined in section resources
173 | resource: main
174 | # The id of the target section in the resource page (optional)
175 | target: tap-water
176 | depends-on:
177 | # List of dependencies (their name, not title)
178 | - pure_water
179 | - impurities
180 |
181 | - name: pure_water
182 | # If title is not provided, it will be guessed based on name
183 | linkto:
184 | resource: main
185 | target: pure-water
186 | ```
187 |
188 | ### algo-config
189 | These fields control the node placement, direction, etc of the graph generation
190 | algorithm. Example:
191 | ```yaml
192 | algo-config:
193 | # Supported: bottom2top (default), top2bottom
194 | level-strategy: top2bottom
195 | # Supported: child2parent (default), parent2child
196 | arrow-direction: child2parent
197 | # Supported: ascend (default), descend
198 | node-sorting: ascend
199 | ```
200 |
201 | [More details on algo-config](docs/algo-config/README.md)
202 |
203 | ## User Interface
204 |
205 | The graph generated is a basic HTML web-page. But we have added a few features to make it
206 | easy to use.
207 |
208 | 1. A resource connected to a node (if available) can be accessed by left-clicking the
209 | node's title. Middle-click or Ctrl-click will open the same resource in a new tab.
210 | 2. Clicking on the connection box (those small circles with a +) will move the view to
211 | the target node. The target node will be highlighted in this case.
212 | 3. When an image resource is viewed in the same page of the graph, keys "[" and "]"
213 | can be used to control the size/zoom of the image.
214 |
215 | ## External Examples
216 |
217 | We are currently building graphs for topics covered in class 11 and 12 (plus-one and plus-two).
218 | You will find the example graphs in this repository:
219 | [class-11-12-india](https://github.com/charstorm/class-11-12-india) (See readme.md).
220 |
221 | ## Status
222 |
223 | ❗The project is still in beta phase.
224 |
225 | ## License
226 |
227 | All files in this repo (except those in `src/linkitall_vendor/`) follow the MIT License.
228 | See LICENSE for details. The files in the vendor directory have their own licenses.
229 |
--------------------------------------------------------------------------------
/examples/simple/resources/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Tap Water
8 |
34 |
35 |
36 |
Tap Water
37 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
38 |
Pure Water
39 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
40 |
Impurities
41 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
42 |
Dissolved gases
43 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
44 |
Disinfectants
45 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
46 |
Microorganisms
47 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
48 |
Chemicals
49 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
50 |
Natural Minerals
51 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
52 |
Agricultural runoff
53 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
54 |
Pharmaceuticals
55 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
56 |
Other impurities
57 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
58 |
Organic Compounds
59 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
60 |
Sediments
61 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
62 |
Chlorine
63 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
64 |
Methane
65 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
66 |
Carbon Dioxide
67 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
68 |
Hydrogen Sulfide
69 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
70 |
Bacteria
71 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
72 |
Protozoa
73 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
74 |
Viruses
75 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
76 |
Fungi
77 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
78 |
Algae
79 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.