├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _example ├── settings.yaml └── sites │ └── default │ ├── content │ ├── .empty │ └── index.md │ ├── site.yaml │ ├── templates │ ├── .empty │ └── index.tpl │ └── webroot │ ├── .empty │ ├── apple-touch-icon-precomposed.png │ ├── css │ ├── hyde.css │ ├── luminos.css │ ├── poole.css │ └── syntax.css │ ├── favicon.ico │ ├── images │ └── logo.svg │ └── js │ └── main.js ├── blobs.go ├── command-init-unpack.go ├── command-init.go ├── command-run.go ├── command-version.go ├── host └── main.go ├── main.go ├── page └── main.go ├── server.go └── watcher ├── main.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | dist/ 3 | bin/ 4 | luminos 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | 3 | ENV VERSION 0.9.3 4 | 5 | RUN apt-get update 6 | 7 | RUN apt-get install -y curl 8 | 9 | ENV LUMINOS_URL https://github.com/xiam/luminos/releases/download/v$VERSION/luminos_linux_amd64.gz 10 | 11 | RUN curl --silent -L ${LUMINOS_URL} | gzip -d > /bin/luminos 12 | 13 | RUN chmod +x /bin/luminos 14 | 15 | EXPOSE 9000 16 | 17 | ENTRYPOINT [ "/bin/luminos" ] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOX_OSARCH ?= "darwin/amd64 linux/amd64 linux/arm freebsd/386 freebsd/amd64 linux/386 windows/386" 2 | GOX_OUTPUT_DIR ?= bin 3 | GH_ACCESS_TOKEN ?= Missing access token. 4 | MESSAGE ?= Latest release. 5 | 6 | binaries: clean 7 | @mkdir -p $(GOX_OUTPUT_DIR) && \ 8 | gox -osarch=$(GOX_OSARCH) -output "$(GOX_OUTPUT_DIR)/{{.Dir}}_{{.OS}}_{{.Arch}}" && \ 9 | gzip bin/luminos_darwin_* && \ 10 | gzip bin/luminos_freebsd_* && \ 11 | gzip bin/luminos_linux_* && \ 12 | zip -r bin/luminos_windows_386.zip bin/luminos_windows_386.exe 13 | 14 | require-version: 15 | @if [[ -z "$$VERSION" ]]; then echo "Missing \$$VERSION"; exit 1; fi 16 | 17 | release: binaries require-version 18 | @RESP=$$(curl --silent --data '{ \ 19 | "tag_name": "v$(VERSION)", \ 20 | "name": "v$(VERSION)", \ 21 | "body": "$(MESSAGE)", \ 22 | "target_commitish": "$(git rev-parse --abbrev-ref HEAD)", \ 23 | "draft": false, \ 24 | "prerelease": false \ 25 | }' "https://api.github.com/repos/xiam/luminos/releases?access_token=$(GH_ACCESS_TOKEN)") && \ 26 | \ 27 | UPLOAD_URL_TEMPLATE=$$(echo $$RESP | python -mjson.tool | grep upload_url | awk '{print $$2}' | sed s/,$$//g | sed s/'"'//g) && \ 28 | if [[ -z "$$UPLOAD_URL_TEMPLATE" ]]; then echo $$RESP; exit 1; fi && \ 29 | \ 30 | for ASSET in $$(ls -1 bin/); do \ 31 | UPLOAD_URL=$$(echo $$UPLOAD_URL_TEMPLATE | sed s/"{?name,label}"/"?access_token=$(GH_ACCESS_TOKEN)\&name=$$ASSET"/g) && \ 32 | MIME_TYPE=$$(file --mime-type bin/$$ASSET | awk '{print $$2}') && \ 33 | curl --silent -H "Content-Type: $$MIME_TYPE" --data-binary @bin/$$ASSET $$UPLOAD_URL > /dev/null && \ 34 | echo "-> $$ASSET OK." \ 35 | ; done && \ 36 | $(MAKE) docker-push 37 | 38 | clean: 39 | @rm -rf $(GOX_OUTPUT_DIR) 40 | 41 | docker: 42 | docker build -t menteslibres/luminos . 43 | 44 | docker-push: docker require-version 45 | docker tag menteslibres/luminos menteslibres/luminos:$(VERSION) 46 | docker push menteslibres/luminos:$(VERSION) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luminos, markdown server 2 | 3 | [Luminos][5] is a tiny HTTP/FastCGI server written in [Go][2] that converts 4 | [markdown][3] files into HTML on the fly. 5 | 6 | ## Getting Luminos 7 | 8 | In order to download and install Luminos, a [working Go 9 | environment](https://golang.org/doc/install) is required. 10 | 11 | If you already have Go, you may install Luminos by issuing the following 12 | command: 13 | 14 | ```sh 15 | go get menteslibres.net/luminos 16 | ``` 17 | 18 | ## Usage 19 | 20 | ``` 21 | rev@localhost > luminos 22 | 23 | Luminos Markdown Server (0.9) - https://menteslibres.net/luminos 24 | by J. Carlos Nieto 25 | 26 | Usage: luminos 27 | 28 | Available commands for luminos: 29 | 30 | help Shows information about the given command. 31 | init Creates a new Luminos site scaffold in the given PATH. 32 | run Runs a luminos server. 33 | version Prints software version. 34 | 35 | Use "luminos help " to view more information about a command. 36 | ``` 37 | 38 | Use `luminos init` to create an empty site: 39 | 40 | ```sh 41 | cd ~/projects 42 | luminos init test-site 43 | ``` 44 | 45 | then you can ask `luminos run` to serve it: 46 | 47 | ```sh 48 | cd test-site 49 | luminos run 50 | ``` 51 | 52 | If you want to use Luminos with Apache or NGINX see the [Getting 53 | started](https://menteslibres.net/luminos/getting-started) page. 54 | 55 | ## Documentation 56 | 57 | See the [project's page][5] for documentation, tips and tricks. 58 | 59 | ## Licenses and acknowledgements 60 | 61 | Luminos is released under the MIT License. 62 | 63 | > Copyright (c) 2012-2013 José Carlos Nieto, https://menteslibres.net/luminos 64 | > 65 | > Permission is hereby granted, free of charge, to any person obtaining 66 | > a copy of this software and associated documentation files (the 67 | > "Software"), to deal in the Software without restriction, including 68 | > without limitation the rights to use, copy, modify, merge, publish, 69 | > distribute, sublicense, and/or sell copies of the Software, and to 70 | > permit persons to whom the Software is furnished to do so, subject to 71 | > the following conditions: 72 | > 73 | > The above copyright notice and this permission notice shall be 74 | > included in all copies or substantial portions of the Software. 75 | > 76 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 77 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 78 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 79 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 80 | > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 81 | > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 82 | > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 83 | 84 | The [Hyde theme](https://github.com/poole/hyde) was created by [Mark 85 | Otto](http://jekyllrb.com/) for [Jekyll](http://jekyllrb.com/) and released 86 | under the MIT License. 87 | 88 | > Copyright (c) 2013 Mark Otto. 89 | > 90 | > Permission is hereby granted, free of charge, to any person obtaining a copy of 91 | > this software and associated documentation files (the "Software"), to deal in 92 | > the Software without restriction, including without limitation the rights to 93 | > use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 94 | > of the Software, and to permit persons to whom the Software is furnished to do 95 | > so, subject to the following conditions: 96 | > 97 | > The above copyright notice and this permission notice shall be included in all 98 | > copies or substantial portions of the Software. 99 | > 100 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 101 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 102 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 103 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 104 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 105 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 106 | > SOFTWARE. 107 | 108 | [1]: http://werc.cat-v.org 109 | [2]: http://golang.org 110 | [3]: http://daringfireball.net/projects/markdown/ 111 | [4]: https://github.com/xiam/luminos 112 | [5]: https://menteslibres.net/luminos 113 | -------------------------------------------------------------------------------- /_example/settings.yaml: -------------------------------------------------------------------------------- 1 | # Configuration file for Luminos. https://menteslibres.net/luminos 2 | # 3 | # 4 | # You are viewing a YAML formatted file, read more about the format at 5 | # http://www.yaml.org. 6 | # 7 | # Note: Use two space indentations instead of the \t character. 8 | # 9 | 10 | # SERVER CONFIGURATION 11 | # Note: Changing server settings requires restarting luminos. 12 | server: 13 | 14 | # The IPv4 or IPv6 address to bind to. 15 | bind: "0.0.0.0" 16 | 17 | # Port to listen on. 18 | port: 9000 19 | 20 | # Use "fastcgi" to start a FastCGI server (if you're planning to use an 21 | # external webserver) or "standalone" to start a HTTP server instead. 22 | type: "standalone" 23 | 24 | # VIRTUAL HOSTS CONFIGURATION 25 | # Changing virtual hosts does not require a restart. 26 | hosts: 27 | 28 | # Default route. 29 | default: "./sites/default" 30 | 31 | # Uncomment the following line to route client requests for "foo.example.org" 32 | # to the "/path/to/foo.example.org/docs" directory. 33 | # foo.example.org: "/path/to/foo.example.org/docs" 34 | 35 | # Uncomment the following line to route client requests for 36 | # "foo.example.org/bar" to the "/path/to/foo.example.org/docs/foo" directory. 37 | # foo.example.org/docs: "./sites/default" 38 | 39 | # Uncomment the following line to route client requests for "bar.example.org" 40 | # to the "/path/to/bar.example.org/docs" directory. 41 | # bar.example.org: "/path/to/bar.example.org/docs" 42 | 43 | -------------------------------------------------------------------------------- /_example/sites/default/content/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiam/luminos/6697ddd9788dc6457e105cd452ae69ef79c89e81/_example/sites/default/content/.empty -------------------------------------------------------------------------------- /_example/sites/default/content/index.md: -------------------------------------------------------------------------------- 1 | # It works! 2 | 3 | This is [Luminos][2] a markdown server written in [Go][1]. You can find some 4 | tips and tricks at the [getting started][5] page. 5 | 6 | [Luminos][2] is an Open Source project, feel free to [browse and hack][2] the 7 | source and, if you find this project useful, please consider [making a 8 | donation][4] to the [author][6]. 9 | 10 | Thanks for using [Luminos][3]! 11 | 12 | ## A few markdown examples 13 | 14 | [Markdown](http://daringfireball.net/projects/markdown/) is a very comfortable 15 | format for writing documents in plain text format. 16 | 17 | Here are some examples on how your markdown code would be translated into HTML 18 | by [Luminos][3]. 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 35 | 36 | 37 | 40 | 43 | 44 | 45 | 48 | 51 | 52 | 53 | 56 | 59 | 60 | 61 | 64 | 67 | 68 | 69 | 72 | 75 | 76 | 77 | 80 | 83 | 84 | 85 | 88 | 91 | 92 | 93 | 96 | 99 | 100 | 101 | 104 | 107 | 108 | 109 | 114 | 121 | 122 | 123 | 128 | 135 | 136 | 137 | 143 | 163 | 164 | 165 | 174 | 182 | 183 | 184 | 189 | 193 | 194 | 195 |
Markdown codeResult
30 | **Bold text** 31 | 33 | Bold text 34 |
38 | *Italics* 39 | 41 | Italics 42 |
46 | ~~Striked-through~~ 47 | 49 | Striked-through 50 |
54 | # First level header 55 | 57 |

First level header

58 |
62 | ## Second level header 63 | 65 |

Second level header

66 |
70 | ### Third level header 71 | 73 |

Third level header

74 |
78 | #### Fourth level header 79 | 81 |

Fourth level header

82 |
86 | ##### Fifth level header 87 | 89 |
Fifth level header
90 |
94 | [The Go Programming Language](http://golang.org) 95 | 97 | The Go Programming Language 98 |
102 | ![A gopher](http://bit.ly/SLqdv6) 103 | 105 | A gopher! 106 |
110 |
* List item 1
111 | * List item 2
112 | * List item 3
113 |
115 |
    116 |
  • List item 1
  • 117 |
  • List item 2
  • 118 |
  • List item 3
  • 119 |
120 |
124 |
1. List item 1
125 | 2. List item 2
126 | 3. List item 3
127 |
129 |
    130 |
  1. List item 1
  2. 131 |
  3. List item 2
  4. 132 |
  5. List item 3
  6. 133 |
134 |
138 |
Name    | Age
139 | --------|------
140 | Bob     | 27
141 | Alice   | 23
142 |
144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 |
NameAge
Bob27
Alice23
162 |
166 |
```go
167 | import "foo"
168 | 
169 | func main() {
170 |   foo.Bar()
171 | }
172 | ```
173 |
175 |
import "foo"
176 | 
177 | func main() {
178 |   foo.Bar()
179 | }
180 | 
181 |
185 |
```latex
186 | \LaTeX
187 | ```
188 |
190 |
\LaTeX
191 | 
192 |
196 | 197 | [1]: http://golang.org 198 | [2]: https://github.com/xiam/luminos 199 | [3]: https://menteslibres.net/luminos 200 | [4]: https://menteslibres.net/xiam/donate 201 | [5]: https://menteslibres.net/luminos/getting-started 202 | [6]: https://menteslibres.net/xiam 203 | -------------------------------------------------------------------------------- /_example/sites/default/site.yaml: -------------------------------------------------------------------------------- 1 | # Configuration file for Luminos. http://luminos.menteslibres.org 2 | # 3 | # You are viewing a YAML formatted file, read more about the format 4 | # at http://www.yaml.org. 5 | # 6 | # Note: Use two space indentations instead of the \t character. 7 | # 8 | # 9 | 10 | page: 11 | # Name of the site. 12 | brand: "Luminos" 13 | 14 | # Some properties for . 15 | head: 16 | # Title that appears on the tag. 17 | title: "Luminos markdown server." 18 | 19 | # Some properties for <body>. 20 | body: 21 | # Page subtitle. 22 | title: "Markdown server" 23 | 24 | # Top navigation bar. 25 | menu: 26 | - { text: "Getting started with Luminos", url: "https://menteslibres.net/luminos/getting-started" } 27 | 28 | # Top-right navigation helper. 29 | menu_pull: 30 | - { text: "Home", url: "/" } 31 | 32 | copyright: "© 2012-2014. J. Carlos Nieto." 33 | -------------------------------------------------------------------------------- /_example/sites/default/templates/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiam/luminos/6697ddd9788dc6457e105cd452ae69ef79c89e81/_example/sites/default/templates/.empty -------------------------------------------------------------------------------- /_example/sites/default/templates/index.tpl: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en-us"> 3 | 4 | <head> 5 | <link href="http://gmpg.org/xfn/11" rel="profile"> 6 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 7 | <meta http-equiv="content-type" content="text/html; charset=utf-8"> 8 | 9 | <!-- Enable responsiveness on mobile devices--> 10 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1"> 11 | 12 | <title> 13 | {{ if .IsHome }} 14 | {{ setting "page/head/title" }} 15 | {{ else }} 16 | {{ if .Title }} 17 | {{ .Title }} {{ if setting "page/head/title" }} · {{ setting "page/head/title" }} {{ end }} 18 | {{ else }} 19 | {{ setting "page/head/title" }} 20 | {{ end }} 21 | {{ end }} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 119 | 120 |
121 | 122 | {{ if not .IsHome }} 123 | {{ if .BreadCrumb }} 124 | 129 | {{ end }} 130 | {{ end }} 131 | 132 | {{ if .Content }} 133 | 134 | {{ .ContentHeader }} 135 | 136 | {{ .Content }} 137 | 138 | {{ .ContentFooter }} 139 | 140 | {{ else }} 141 | 142 | {{ if .CurrentPage }} 143 |

{{ .CurrentPage.Text }}

144 | {{ end }} 145 | 146 |
    147 | {{ range .SideMenu }} 148 |
  • 149 | {{ .Text }} 150 |
  • 151 | {{ end }} 152 |
153 | 154 | {{end}} 155 | 156 | {{ if setting "page/body/copyright" }} 157 |

{{ setting "page/body/copyright" | html }}

158 | {{ end }} 159 | 160 |
161 | 162 | {{ if setting "page/body/scripts/footer" }} 163 | 166 | {{ end }} 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /_example/sites/default/webroot/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiam/luminos/6697ddd9788dc6457e105cd452ae69ef79c89e81/_example/sites/default/webroot/.empty -------------------------------------------------------------------------------- /_example/sites/default/webroot/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiam/luminos/6697ddd9788dc6457e105cd452ae69ef79c89e81/_example/sites/default/webroot/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /_example/sites/default/webroot/css/hyde.css: -------------------------------------------------------------------------------- 1 | /* 2 | * __ __ 3 | * /\ \ /\ \ 4 | * \ \ \___ __ __ \_\ \ __ 5 | * \ \ _ `\/\ \/\ \ /'_` \ /'__`\ 6 | * \ \ \ \ \ \ \_\ \/\ \_\ \/\ __/ 7 | * \ \_\ \_\/`____ \ \___,_\ \____\ 8 | * \/_/\/_/`/___/> \/__,_ /\/____/ 9 | * /\___/ 10 | * \/__/ 11 | * 12 | * Designed, built, and released under MIT license by @mdo. Learn more at 13 | * https://github.com/poole/hyde. 14 | */ 15 | 16 | 17 | /* 18 | * Contents 19 | * 20 | * Global resets 21 | * Sidebar 22 | * Container 23 | * Reverse layout 24 | * Themes 25 | */ 26 | 27 | 28 | /* 29 | * Global resets 30 | * 31 | * Update the foundational and global aspects of the page. 32 | */ 33 | 34 | html { 35 | font-family: "PT Sans", Helvetica, Arial, sans-serif; 36 | } 37 | @media (min-width: 48em) { 38 | html { 39 | font-size: 16px; 40 | } 41 | } 42 | @media (min-width: 58em) { 43 | html { 44 | font-size: 20px; 45 | } 46 | } 47 | 48 | 49 | /* 50 | * Sidebar 51 | * 52 | * Flexible banner for housing site name, intro, and "footer" content. Starts 53 | * out above content in mobile and later moves to the side with wider viewports. 54 | */ 55 | 56 | .sidebar { 57 | text-align: center; 58 | padding: 2rem 1rem; 59 | color: rgba(255,255,255,.5); 60 | background-color: #202020; 61 | } 62 | @media (min-width: 48em) { 63 | .sidebar { 64 | position: fixed; 65 | top: 0; 66 | left: 0; 67 | bottom: 0; 68 | width: 18rem; 69 | text-align: left; 70 | } 71 | } 72 | 73 | /* Sidebar links */ 74 | .sidebar a { 75 | color: #fff; 76 | } 77 | 78 | /* About section */ 79 | .sidebar-about h1 { 80 | color: #fff; 81 | margin-top: 0; 82 | font-family: "Abril Fatface", serif; 83 | font-size: 3.25rem; 84 | } 85 | 86 | /* Sidebar nav */ 87 | .sidebar-nav { 88 | margin-bottom: 1rem; 89 | } 90 | .sidebar-nav-item { 91 | display: block; 92 | line-height: 1.75; 93 | } 94 | a.sidebar-nav-item:hover, 95 | a.sidebar-nav-item:focus { 96 | text-decoration: underline; 97 | } 98 | .sidebar-nav-item.active { 99 | font-weight: bold; 100 | } 101 | 102 | /* Sticky sidebar 103 | * 104 | * Add the `sidebar-sticky` class to the sidebar's container to affix it the 105 | * contents to the bottom of the sidebar in tablets and up. 106 | */ 107 | 108 | @media (min-width: 48em) { 109 | .sidebar-sticky { 110 | position: absolute; 111 | right: 1rem; 112 | bottom: 1rem; 113 | left: 1rem; 114 | } 115 | } 116 | 117 | 118 | /* Container 119 | * 120 | * Align the contents of the site above the proper threshold with some margin-fu 121 | * with a 25%-wide `.sidebar`. 122 | */ 123 | 124 | .content { 125 | padding-top: 4rem; 126 | padding-bottom: 4rem; 127 | } 128 | 129 | @media (min-width: 48em) { 130 | .content { 131 | max-width: 38rem; 132 | margin-left: 20rem; 133 | margin-right: 2rem; 134 | } 135 | } 136 | 137 | @media (min-width: 64em) { 138 | .content { 139 | margin-left: 22rem; 140 | margin-right: 4rem; 141 | } 142 | } 143 | 144 | 145 | /* 146 | * Reverse layout 147 | * 148 | * Flip the orientation of the page by placing the `.sidebar` on the right. 149 | */ 150 | 151 | @media (min-width: 48em) { 152 | .layout-reverse .sidebar { 153 | left: auto; 154 | right: 0; 155 | } 156 | .layout-reverse .content { 157 | margin-left: 2rem; 158 | margin-right: 20rem; 159 | } 160 | } 161 | 162 | @media (min-width: 64em) { 163 | .layout-reverse .content { 164 | margin-left: 4rem; 165 | margin-right: 22rem; 166 | } 167 | } 168 | 169 | 170 | 171 | /* 172 | * Themes 173 | * 174 | * As of v1.1, Hyde includes optional themes to color the sidebar and links 175 | * within blog posts. To use, add the class of your choosing to the `body`. 176 | */ 177 | 178 | /* Base16 (http://chriskempson.github.io/base16/#default) */ 179 | 180 | /* Red */ 181 | .theme-base-08 .sidebar { 182 | background-color: #ac4142; 183 | } 184 | .theme-base-08 .content a, 185 | .theme-base-08 .related-posts li a:hover { 186 | color: #ac4142; 187 | } 188 | 189 | /* Orange */ 190 | .theme-base-09 .sidebar { 191 | background-color: #d28445; 192 | } 193 | .theme-base-09 .content a, 194 | .theme-base-09 .related-posts li a:hover { 195 | color: #d28445; 196 | } 197 | 198 | /* Yellow */ 199 | .theme-base-0a .sidebar { 200 | background-color: #f4bf75; 201 | } 202 | .theme-base-0a .content a, 203 | .theme-base-0a .related-posts li a:hover { 204 | color: #f4bf75; 205 | } 206 | 207 | /* Green */ 208 | .theme-base-0b .sidebar { 209 | background-color: #90a959; 210 | } 211 | .theme-base-0b .content a, 212 | .theme-base-0b .related-posts li a:hover { 213 | color: #90a959; 214 | } 215 | 216 | /* Cyan */ 217 | .theme-base-0c .sidebar { 218 | background-color: #75b5aa; 219 | } 220 | .theme-base-0c .content a, 221 | .theme-base-0c .related-posts li a:hover { 222 | color: #75b5aa; 223 | } 224 | 225 | /* Blue */ 226 | .theme-base-0d .sidebar { 227 | background-color: #6a9fb5; 228 | } 229 | .theme-base-0d .content a, 230 | .theme-base-0d .related-posts li a:hover { 231 | color: #6a9fb5; 232 | } 233 | 234 | /* Magenta */ 235 | .theme-base-0e .sidebar { 236 | background-color: #aa759f; 237 | } 238 | .theme-base-0e .content a, 239 | .theme-base-0e .related-posts li a:hover { 240 | color: #aa759f; 241 | } 242 | 243 | /* Brown */ 244 | .theme-base-0f .sidebar { 245 | background-color: #8f5536; 246 | } 247 | .theme-base-0f .content a, 248 | .theme-base-0f .related-posts li a:hover { 249 | color: #8f5536; 250 | } 251 | -------------------------------------------------------------------------------- /_example/sites/default/webroot/css/luminos.css: -------------------------------------------------------------------------------- 1 | /* I just happen to love this font. */ 2 | code { 3 | font-family: 'Source Code Pro'; 4 | } 5 | 6 | /* Removing background from highlightjs. */ 7 | .hljs { 8 | background: none; 9 | } 10 | 11 | /* Snippets from Bootstrap. */ 12 | /* https://github.com/twbs/bootstrap */ 13 | /* 14 | * Code and documentation copyright 2011-2014 Twitter, Inc. Code released under 15 | * the MIT license. Docs released under Creative Commons. 16 | * */ 17 | .breadcrumb { 18 | padding: 8px 15px; 19 | margin-bottom: 20px; 20 | list-style: none; 21 | background-color: #f5f5f5; 22 | border-radius: 4px; 23 | } 24 | .breadcrumb > li { 25 | display: inline-block; 26 | } 27 | .breadcrumb > li + li:before { 28 | padding: 0 5px; 29 | color: #ccc; 30 | content: "/\00a0"; 31 | } 32 | .breadcrumb > .active, 33 | .breadcrumb > .active a, 34 | .breadcrumb > .active a:hover, 35 | .breadcrumb > .active a:focus 36 | { 37 | cursor: default; 38 | color: #777; 39 | } 40 | 41 | .nav { 42 | font-size: small; 43 | padding-left: 0; 44 | margin-bottom: 0; 45 | list-style: none; 46 | } 47 | .nav > li { 48 | position: relative; 49 | display: block; 50 | } 51 | .nav > li > a { 52 | position: relative; 53 | display: block; 54 | padding: 10px 15px; 55 | } 56 | .nav > li > a:hover, 57 | .nav > li > a:focus { 58 | text-decoration: none; 59 | background-color: #333; 60 | } 61 | .nav > li.disabled > a { 62 | color: #777; 63 | } 64 | .nav > li.disabled > a:hover, 65 | .nav > li.disabled > a:focus { 66 | color: #777; 67 | text-decoration: none; 68 | cursor: not-allowed; 69 | background-color: transparent; 70 | } 71 | .nav .open > a, 72 | .nav .open > a:hover, 73 | .nav .open > a:focus { 74 | background-color: #eee; 75 | border-color: #337ab7; 76 | } 77 | 78 | .nav:before, 79 | .nav:after, 80 | .navbar:before, 81 | .navbar:after { 82 | display: table; 83 | content: " "; 84 | } 85 | .nav:after, 86 | .navbar:after { 87 | clear: both; 88 | } 89 | .navbar { 90 | position: relative; 91 | min-height: 50px; 92 | margin-bottom: 20px; 93 | border: 1px solid transparent; 94 | } 95 | .navbar-nav { 96 | margin: 7.5px -15px; 97 | } 98 | .navbar-nav > li > a { 99 | padding-top: 10px; 100 | padding-bottom: 10px; 101 | line-height: 20px; 102 | } 103 | @media (max-width: 767px) { 104 | .navbar-nav .open .dropdown-menu { 105 | position: static; 106 | float: none; 107 | width: auto; 108 | margin-top: 0; 109 | background-color: transparent; 110 | border: 0; 111 | -webkit-box-shadow: none; 112 | box-shadow: none; 113 | } 114 | .navbar-nav .open .dropdown-menu > li > a, 115 | .navbar-nav .open .dropdown-menu .dropdown-header { 116 | padding: 5px 15px 5px 25px; 117 | } 118 | .navbar-nav .open .dropdown-menu > li > a { 119 | line-height: 20px; 120 | } 121 | .navbar-nav .open .dropdown-menu > li > a:hover, 122 | .navbar-nav .open .dropdown-menu > li > a:focus { 123 | background-image: none; 124 | } 125 | } 126 | @media (min-width: 768px) { 127 | .navbar-nav { 128 | margin: 0; 129 | } 130 | .navbar-nav > li { 131 | float: left; 132 | } 133 | .navbar-nav > li > a { 134 | padding-top: 15px; 135 | padding-bottom: 15px; 136 | } 137 | } 138 | @media (min-width: 768px) { 139 | .navbar-collapse { 140 | width: auto; 141 | border-top: 0; 142 | -webkit-box-shadow: none; 143 | box-shadow: none; 144 | } 145 | .navbar-collapse.collapse { 146 | display: block !important; 147 | height: auto !important; 148 | padding-bottom: 0; 149 | overflow: visible !important; 150 | visibility: visible !important; 151 | } 152 | .navbar-collapse.in { 153 | overflow-y: visible; 154 | } 155 | .navbar-fixed-top .navbar-collapse, 156 | .navbar-static-top .navbar-collapse, 157 | .navbar-fixed-bottom .navbar-collapse { 158 | padding-right: 0; 159 | padding-left: 0; 160 | } 161 | } 162 | .navbar-collapse { 163 | padding-right: 15px; 164 | padding-left: 15px; 165 | overflow-x: visible; 166 | -webkit-overflow-scrolling: touch; 167 | border-top: 1px solid transparent; 168 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); 169 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); 170 | } 171 | @media (min-width: 768px) { 172 | .navbar-collapse { 173 | width: auto; 174 | border-top: 0; 175 | -webkit-box-shadow: none; 176 | box-shadow: none; 177 | } 178 | .navbar-collapse.collapse { 179 | display: block !important; 180 | height: auto !important; 181 | padding-bottom: 0; 182 | overflow: visible !important; 183 | visibility: visible !important; 184 | } 185 | .navbar-collapse.in { 186 | overflow-y: visible; 187 | } 188 | .navbar-fixed-top .navbar-collapse, 189 | .navbar-static-top .navbar-collapse, 190 | .navbar-fixed-bottom .navbar-collapse { 191 | padding-right: 0; 192 | padding-left: 0; 193 | } 194 | } 195 | .navbar-collapse { 196 | border-color: #e7e7e7; 197 | } 198 | .navbar-nav > li > a { 199 | color: #777; 200 | } 201 | .navbar-nav > li > a:hover, 202 | .navbar-nav > li > a:focus { 203 | color: #fff; 204 | background-color: transparent; 205 | } 206 | .navbar-nav > .active > a, 207 | .navbar-nav > .active > a:hover, 208 | .navbar-nav > .active > a:focus { 209 | color: #fff; 210 | background-color: #191919; 211 | } 212 | 213 | .nav-tabs { 214 | border-bottom: 1px solid #333; 215 | } 216 | .nav-tabs > li { 217 | float: left; 218 | margin-bottom: -1px; 219 | } 220 | .nav-tabs > li > a { 221 | margin-right: 2px; 222 | line-height: 1.42857143; 223 | border: 1px solid transparent; 224 | border-radius: 4px 4px 0 0; 225 | } 226 | .nav-tabs li > a { 227 | color: #555; 228 | } 229 | .nav-tabs > li > a:hover { 230 | color: #ddd; 231 | border-color: #202020 #202020 #333; 232 | } 233 | .nav-tabs > li.active > a, 234 | .nav-tabs > li.active > a:hover, 235 | .nav-tabs > li.active > a:focus { 236 | color: #fff; 237 | cursor: default; 238 | background-color: #202020; 239 | border: 1px solid #333; 240 | border-bottom-color: transparent; 241 | } 242 | 243 | .logo { 244 | text-align: center; 245 | padding-top: 1rem; 246 | } 247 | .logo img { 248 | display: inline; 249 | margin: 0px; 250 | } 251 | a img { 252 | border: none; 253 | } 254 | .sidebar-about h1 { 255 | margin-top: 0px; 256 | } 257 | -------------------------------------------------------------------------------- /_example/sites/default/webroot/css/poole.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ___ 3 | * /\_ \ 4 | * _____ ___ ___\//\ \ __ 5 | * /\ '__`\ / __`\ / __`\\ \ \ /'__`\ 6 | * \ \ \_\ \/\ \_\ \/\ \_\ \\_\ \_/\ __/ 7 | * \ \ ,__/\ \____/\ \____//\____\ \____\ 8 | * \ \ \/ \/___/ \/___/ \/____/\/____/ 9 | * \ \_\ 10 | * \/_/ 11 | * 12 | * Designed, built, and released under MIT license by @mdo. Learn more at 13 | * https://github.com/poole/poole. 14 | */ 15 | 16 | 17 | /* 18 | * Contents 19 | * 20 | * Body resets 21 | * Custom type 22 | * Messages 23 | * Container 24 | * Masthead 25 | * Posts and pages 26 | * Pagination 27 | * Reverse layout 28 | * Themes 29 | */ 30 | 31 | 32 | /* 33 | * Body resets 34 | * 35 | * Update the foundational and global aspects of the page. 36 | */ 37 | 38 | * { 39 | -webkit-box-sizing: border-box; 40 | -moz-box-sizing: border-box; 41 | box-sizing: border-box; 42 | } 43 | 44 | html, 45 | body { 46 | margin: 0; 47 | padding: 0; 48 | } 49 | 50 | html { 51 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 52 | font-size: 16px; 53 | line-height: 1.5; 54 | } 55 | @media (min-width: 38em) { 56 | html { 57 | font-size: 20px; 58 | } 59 | } 60 | 61 | body { 62 | color: #515151; 63 | background-color: #fff; 64 | -webkit-text-size-adjust: 100%; 65 | -ms-text-size-adjust: 100%; 66 | } 67 | 68 | /* No `:visited` state is required by default (browsers will use `a`) */ 69 | a { 70 | color: #268bd2; 71 | text-decoration: none; 72 | } 73 | a strong { 74 | color: inherit; 75 | } 76 | /* `:focus` is linked to `:hover` for basic accessibility */ 77 | a:hover, 78 | a:focus { 79 | text-decoration: underline; 80 | } 81 | 82 | /* Headings */ 83 | h1, h2, h3, h4, h5, h6 { 84 | margin-bottom: .5rem; 85 | font-weight: bold; 86 | line-height: 1.25; 87 | color: #313131; 88 | text-rendering: optimizeLegibility; 89 | } 90 | h1 { 91 | font-size: 2rem; 92 | } 93 | h2 { 94 | margin-top: 1rem; 95 | font-size: 1.5rem; 96 | } 97 | h3 { 98 | margin-top: 1.5rem; 99 | font-size: 1.25rem; 100 | } 101 | h4, h5, h6 { 102 | margin-top: 1rem; 103 | font-size: 1rem; 104 | } 105 | 106 | /* Body text */ 107 | p { 108 | margin-top: 0; 109 | margin-bottom: 1rem; 110 | } 111 | 112 | strong { 113 | color: #303030; 114 | } 115 | 116 | 117 | /* Lists */ 118 | ul, ol, dl { 119 | margin-top: 0; 120 | margin-bottom: 1rem; 121 | } 122 | 123 | dt { 124 | font-weight: bold; 125 | } 126 | dd { 127 | margin-bottom: .5rem; 128 | } 129 | 130 | /* Misc */ 131 | hr { 132 | position: relative; 133 | margin: 1.5rem 0; 134 | border: 0; 135 | border-top: 1px solid #eee; 136 | border-bottom: 1px solid #fff; 137 | } 138 | 139 | abbr { 140 | font-size: 85%; 141 | font-weight: bold; 142 | color: #555; 143 | text-transform: uppercase; 144 | } 145 | abbr[title] { 146 | cursor: help; 147 | border-bottom: 1px dotted #e5e5e5; 148 | } 149 | 150 | /* Code */ 151 | code, 152 | pre { 153 | font-family: Menlo, Monaco, "Courier New", monospace; 154 | } 155 | code { 156 | padding: .25em .5em; 157 | font-size: 85%; 158 | color: #bf616a; 159 | background-color: #f9f9f9; 160 | border-radius: 3px; 161 | } 162 | pre { 163 | display: block; 164 | margin-top: 0; 165 | margin-bottom: 1rem; 166 | padding: 1rem; 167 | font-size: .8rem; 168 | line-height: 1.4; 169 | white-space: pre; 170 | white-space: pre-wrap; 171 | word-break: break-all; 172 | word-wrap: break-word; 173 | background-color: #f9f9f9; 174 | } 175 | pre code { 176 | padding: 0; 177 | font-size: 100%; 178 | color: inherit; 179 | background-color: transparent; 180 | } 181 | 182 | /* Pygments via Jekyll */ 183 | .highlight { 184 | margin-bottom: 1rem; 185 | border-radius: 4px; 186 | } 187 | .highlight pre { 188 | margin-bottom: 0; 189 | } 190 | 191 | /* Gist via GitHub Pages */ 192 | .gist .gist-file { 193 | font-family: Menlo, Monaco, "Courier New", monospace !important; 194 | } 195 | .gist .markdown-body { 196 | padding: 15px; 197 | } 198 | .gist pre { 199 | padding: 0; 200 | background-color: transparent; 201 | } 202 | .gist .gist-file .gist-data { 203 | font-size: .8rem !important; 204 | line-height: 1.4; 205 | } 206 | .gist code { 207 | padding: 0; 208 | color: inherit; 209 | background-color: transparent; 210 | border-radius: 0; 211 | } 212 | 213 | /* Quotes */ 214 | blockquote { 215 | padding: .5rem 1rem; 216 | margin: .8rem 0; 217 | color: #7a7a7a; 218 | border-left: .25rem solid #e5e5e5; 219 | } 220 | blockquote p:last-child { 221 | margin-bottom: 0; 222 | } 223 | @media (min-width: 30em) { 224 | blockquote { 225 | padding-right: 5rem; 226 | padding-left: 1.25rem; 227 | } 228 | } 229 | 230 | img { 231 | display: block; 232 | max-width: 100%; 233 | margin: 0 0 1rem; 234 | border-radius: 5px; 235 | } 236 | 237 | /* Tables */ 238 | table { 239 | margin-bottom: 1rem; 240 | width: 100%; 241 | border: 1px solid #e5e5e5; 242 | border-collapse: collapse; 243 | } 244 | td, 245 | th { 246 | padding: .25rem .5rem; 247 | border: 1px solid #e5e5e5; 248 | } 249 | tbody tr:nth-child(odd) td, 250 | tbody tr:nth-child(odd) th { 251 | background-color: #f9f9f9; 252 | } 253 | 254 | 255 | /* 256 | * Custom type 257 | * 258 | * Extend paragraphs with `.lead` for larger introductory text. 259 | */ 260 | 261 | .lead { 262 | font-size: 1.25rem; 263 | font-weight: 300; 264 | } 265 | 266 | 267 | /* 268 | * Messages 269 | * 270 | * Show alert messages to users. You may add it to single elements like a `

`, 271 | * or to a parent if there are multiple elements to show. 272 | */ 273 | 274 | .message { 275 | margin-bottom: 1rem; 276 | padding: 1rem; 277 | color: #717171; 278 | background-color: #f9f9f9; 279 | } 280 | 281 | 282 | /* 283 | * Container 284 | * 285 | * Center the page content. 286 | */ 287 | 288 | .container { 289 | max-width: 38rem; 290 | padding-left: 1rem; 291 | padding-right: 1rem; 292 | margin-left: auto; 293 | margin-right: auto; 294 | } 295 | 296 | 297 | /* 298 | * Masthead 299 | * 300 | * Super small header above the content for site name and short description. 301 | */ 302 | 303 | .masthead { 304 | padding-top: 1rem; 305 | padding-bottom: 1rem; 306 | margin-bottom: 3rem; 307 | } 308 | .masthead-title { 309 | margin-top: 0; 310 | margin-bottom: 0; 311 | color: #505050; 312 | } 313 | .masthead-title a { 314 | color: #505050; 315 | } 316 | .masthead-title small { 317 | font-size: 75%; 318 | font-weight: 400; 319 | color: #c0c0c0; 320 | letter-spacing: 0; 321 | } 322 | 323 | 324 | /* 325 | * Posts and pages 326 | * 327 | * Each post is wrapped in `.post` and is used on default and post layouts. Each 328 | * page is wrapped in `.page` and is only used on the page layout. 329 | */ 330 | 331 | .page, 332 | .post { 333 | margin-bottom: 4em; 334 | } 335 | 336 | /* Blog post or page title */ 337 | .page-title, 338 | .post-title, 339 | .post-title a { 340 | color: #303030; 341 | } 342 | .page-title, 343 | .post-title { 344 | margin-top: 0; 345 | } 346 | 347 | /* Meta data line below post title */ 348 | .post-date { 349 | display: block; 350 | margin-top: -.5rem; 351 | margin-bottom: 1rem; 352 | color: #9a9a9a; 353 | } 354 | 355 | /* Related posts */ 356 | .related { 357 | padding-top: 2rem; 358 | padding-bottom: 2rem; 359 | border-top: 1px solid #eee; 360 | } 361 | .related-posts { 362 | padding-left: 0; 363 | list-style: none; 364 | } 365 | .related-posts h3 { 366 | margin-top: 0; 367 | } 368 | .related-posts li small { 369 | font-size: 75%; 370 | color: #999; 371 | } 372 | .related-posts li a:hover { 373 | color: #268bd2; 374 | text-decoration: none; 375 | } 376 | .related-posts li a:hover small { 377 | color: inherit; 378 | } 379 | 380 | 381 | /* 382 | * Pagination 383 | * 384 | * Super lightweight (HTML-wise) blog pagination. `span`s are provide for when 385 | * there are no more previous or next posts to show. 386 | */ 387 | 388 | .pagination { 389 | overflow: hidden; /* clearfix */ 390 | margin-left: -1rem; 391 | margin-right: -1rem; 392 | font-family: "PT Sans", Helvetica, Arial, sans-serif; 393 | color: #ccc; 394 | text-align: center; 395 | } 396 | 397 | /* Pagination items can be `span`s or `a`s */ 398 | .pagination-item { 399 | display: block; 400 | padding: 1rem; 401 | border: 1px solid #eee; 402 | } 403 | .pagination-item:first-child { 404 | margin-bottom: -1px; 405 | } 406 | 407 | /* Only provide a hover state for linked pagination items */ 408 | a.pagination-item:hover { 409 | background-color: #f5f5f5; 410 | } 411 | 412 | @media (min-width: 30em) { 413 | .pagination { 414 | margin: 3rem 0; 415 | } 416 | .pagination-item { 417 | float: left; 418 | width: 50%; 419 | } 420 | .pagination-item:first-child { 421 | margin-bottom: 0; 422 | border-top-left-radius: 4px; 423 | border-bottom-left-radius: 4px; 424 | } 425 | .pagination-item:last-child { 426 | margin-left: -1px; 427 | border-top-right-radius: 4px; 428 | border-bottom-right-radius: 4px; 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /_example/sites/default/webroot/css/syntax.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffc; } 2 | .highlight .c { color: #999; } /* Comment */ 3 | .highlight .err { color: #a00; background-color: #faa } /* Error */ 4 | .highlight .k { color: #069; } /* Keyword */ 5 | .highlight .o { color: #555 } /* Operator */ 6 | .highlight .cm { color: #09f; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #099 } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999; } /* Comment.Single */ 9 | .highlight .cs { color: #999; } /* Comment.Special */ 10 | .highlight .gd { background-color: #fcc; border: 1px solid #c00 } /* Generic.Deleted */ 11 | .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #f00 } /* Generic.Error */ 13 | .highlight .gh { color: #030; } /* Generic.Heading */ 14 | .highlight .gi { background-color: #cfc; border: 1px solid #0c0 } /* Generic.Inserted */ 15 | .highlight .go { color: #aaa } /* Generic.Output */ 16 | .highlight .gp { color: #009; } /* Generic.Prompt */ 17 | .highlight .gs { } /* Generic.Strong */ 18 | .highlight .gu { color: #030; } /* Generic.Subheading */ 19 | .highlight .gt { color: #9c6 } /* Generic.Traceback */ 20 | .highlight .kc { color: #069; } /* Keyword.Constant */ 21 | .highlight .kd { color: #069; } /* Keyword.Declaration */ 22 | .highlight .kn { color: #069; } /* Keyword.Namespace */ 23 | .highlight .kp { color: #069 } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #069; } /* Keyword.Reserved */ 25 | .highlight .kt { color: #078; } /* Keyword.Type */ 26 | .highlight .m { color: #f60 } /* Literal.Number */ 27 | .highlight .s { color: #d44950 } /* Literal.String */ 28 | .highlight .na { color: #4f9fcf } /* Name.Attribute */ 29 | .highlight .nb { color: #366 } /* Name.Builtin */ 30 | .highlight .nc { color: #0a8; } /* Name.Class */ 31 | .highlight .no { color: #360 } /* Name.Constant */ 32 | .highlight .nd { color: #99f } /* Name.Decorator */ 33 | .highlight .ni { color: #999; } /* Name.Entity */ 34 | .highlight .ne { color: #c00; } /* Name.Exception */ 35 | .highlight .nf { color: #c0f } /* Name.Function */ 36 | .highlight .nl { color: #99f } /* Name.Label */ 37 | .highlight .nn { color: #0cf; } /* Name.Namespace */ 38 | .highlight .nt { color: #2f6f9f; } /* Name.Tag */ 39 | .highlight .nv { color: #033 } /* Name.Variable */ 40 | .highlight .ow { color: #000; } /* Operator.Word */ 41 | .highlight .w { color: #bbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #f60 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #f60 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #f60 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #f60 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #c30 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #c30 } /* Literal.String.Char */ 48 | .highlight .sd { color: #c30; font-style: italic } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #c30 } /* Literal.String.Double */ 50 | .highlight .se { color: #c30; } /* Literal.String.Escape */ 51 | .highlight .sh { color: #c30 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #a00 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #c30 } /* Literal.String.Other */ 54 | .highlight .sr { color: #3aa } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #c30 } /* Literal.String.Single */ 56 | .highlight .ss { color: #fc3 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #366 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #033 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #033 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #033 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #f60 } /* Literal.Number.Integer.Long */ 62 | 63 | .css .o, 64 | .css .o + .nt, 65 | .css .nt + .nt { color: #999; } 66 | -------------------------------------------------------------------------------- /_example/sites/default/webroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiam/luminos/6697ddd9788dc6457e105cd452ae69ef79c89e81/_example/sites/default/webroot/favicon.ico -------------------------------------------------------------------------------- /_example/sites/default/webroot/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /_example/sites/default/webroot/js/main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var getParent = function(start, fn) { 4 | while (start) { 5 | if (fn(start)) { 6 | return start; 7 | }; 8 | start = start.parentNode; 9 | }; 10 | return null; 11 | }; 12 | 13 | var normalizeLink = function(s) { 14 | if (s != null) { 15 | s = s.replace(/^\/+/g, ''); 16 | s = s.replace(/\/+$/g, ''); 17 | return s; 18 | }; 19 | return ""; 20 | }; 21 | 22 | // setActiveLinks loops over relevant links and sets the active class if 23 | // required. 24 | var setActiveLinks = function() { 25 | var anchors = document.body.querySelectorAll('a'); 26 | 27 | for (var i = 0; i < anchors.length; i++) { 28 | var anchor = anchors[i]; 29 | if (anchor) { 30 | if (normalizeLink(anchor.getAttribute('href')) == normalizeLink(document.location.pathname)) { 31 | // Look for a suitable parent. 32 | var li = getParent(anchor, function(el) { 33 | if (el.tagName == 'LI') { 34 | return true; 35 | }; 36 | return false; 37 | }); 38 | if (li) { 39 | li.className += ' active'; 40 | } else { 41 | anchor.className += ' active'; 42 | }; 43 | }; 44 | }; 45 | }; 46 | }; 47 | 48 | // load sets the task to be run at page loaded event. 49 | var load = function() { 50 | setActiveLinks(); 51 | }; 52 | 53 | window.onload = load; 54 | })(); 55 | 56 | 57 | /* 58 | $(document.body).ready( 59 | function() { 60 | // Code (marking code blocks for prettyPrint) 61 | var code = $('code'); 62 | 63 | for (var i = 0; i < code.length; i++) { 64 | var el = $(code[i]) 65 | var className = el.attr('class'); 66 | if (className) { 67 | el.addClass('language-'+className); 68 | } 69 | }; 70 | 71 | // An exception, LaTeX blocks. 72 | var code = $('code.latex'); 73 | 74 | for (var i = 0; i < code.length; i++) { 75 | var el = $(code[i]) 76 | var img = $('', { 'src': '//menteslibres.net/api/latex/png?t='+encodeURIComponent(el.html()) }); 77 | img.insertBefore(el); 78 | el.hide(); 79 | }; 80 | 81 | // Starting prettyPrint. 82 | hljs.initHighlightingOnLoad(); 83 | 84 | // Tables without class 85 | 86 | $('table').each( 87 | function(i, el) { 88 | if (!$(el).attr('class')) { 89 | $(el).addClass('table'); 90 | }; 91 | } 92 | ); 93 | 94 | // Navigation 95 | var links = $('ul.menu li').removeClass('active'); 96 | 97 | for (var i = 0; i < links.length; i++) { 98 | var a = $(links[i]).find('a'); 99 | if (a.attr('href') == document.location.pathname) { 100 | $(links[i]).addClass('active'); 101 | }; 102 | }; 103 | 104 | } 105 | ); 106 | */ 107 | -------------------------------------------------------------------------------- /command-init-unpack.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "archive/tar" 26 | "bytes" 27 | "compress/bzip2" 28 | "fmt" 29 | "io" 30 | "os" 31 | ) 32 | 33 | // Attempts to extract a compressed project into the given destination. 34 | func unpackExampleProject(root string) (err error) { 35 | var stat os.FileInfo 36 | 37 | // Validating destination. 38 | if stat, err = os.Stat(root); err != nil { 39 | return err 40 | } 41 | 42 | if stat.IsDir() == false { 43 | return fmt.Errorf("Expecting a directory.") 44 | } 45 | 46 | // Creating a tarbz2 reader. 47 | tbz := tar.NewReader(bzip2.NewReader(bytes.NewBuffer(compressedProject))) 48 | 49 | // Extracting tarred files. 50 | for { 51 | 52 | hdr, err := tbz.Next() 53 | 54 | if err != nil { 55 | if err == io.EOF { 56 | break 57 | } 58 | panic(err.Error()) 59 | } 60 | 61 | // See http://en.wikipedia.org/wiki/Tar_(computing) 62 | filePath := root + pathSeparator + hdr.Name 63 | 64 | switch hdr.Typeflag { 65 | case '0': 66 | // Normal file 67 | fp, err := os.Create(filePath) 68 | 69 | if err != nil { 70 | return err 71 | } 72 | 73 | io.Copy(fp, tbz) 74 | 75 | err = os.Chmod(filePath, os.FileMode(hdr.Mode)) 76 | 77 | if err != nil { 78 | return err 79 | } 80 | 81 | fp.Close() 82 | case '5': 83 | // Directory 84 | os.MkdirAll(filePath, os.FileMode(hdr.Mode)) 85 | default: 86 | // fmt.Printf("--> %s, %d, %c\n", hdr.Name, hdr.Mode, hdr.Typeflag) 87 | panic(fmt.Sprintf("Unhandled tar type: %c in file: %s", hdr.Typeflag, hdr.Name)) 88 | } 89 | } 90 | 91 | return nil 92 | 93 | } 94 | -------------------------------------------------------------------------------- /command-init.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "flag" 26 | "fmt" 27 | "os" 28 | 29 | "menteslibres.net/gosexy/cli" 30 | ) 31 | 32 | // initCommand is the structure that provides instructions for the "luminos 33 | // init" subcommand. 34 | type initCommand struct { 35 | } 36 | 37 | // Execute creates a new luminos site scaffold in the given PATH. 38 | func (c *initCommand) Execute() (err error) { 39 | var stat os.FileInfo 40 | 41 | // Default PATH if the current working directory. 42 | dest := "." 43 | 44 | // If a PATH was given, use it instead of the default PATH. 45 | if flag.NArg() > 1 { 46 | dest = flag.Arg(1) 47 | } 48 | 49 | // Verifying PATH. 50 | if stat, _ = os.Stat(dest); stat == nil { 51 | // Directory does not exists, try to create it. 52 | // TODO: Use system's default mask. 53 | if err = os.MkdirAll(dest, os.ModeDir|0755); err != nil { 54 | return err 55 | } 56 | } else { 57 | // Path exists, is it a directory? 58 | if stat.IsDir() == false { 59 | // Nope, the we can't use it. 60 | return fmt.Errorf("Cannot create directory, file %s already exists!", dest) 61 | } 62 | } 63 | 64 | // If the PATH was already initialized, then it must contain a LOCKFILE. 65 | lockFile := dest + pathSeparator + ".luminos" 66 | 67 | if stat, _ = os.Stat(lockFile); stat != nil { 68 | // If the LOCKFILE exists we cannot continue. 69 | if dest == "." { 70 | // Use "the current directory" to avoid confusing non-technical users 71 | // with a dot instead of a full PATH. 72 | return fmt.Errorf("A Luminos project already exists at the current directory.") 73 | } 74 | return fmt.Errorf("A luminos project already exists in %s.", dest) 75 | } 76 | 77 | // We may extract the example project now. 78 | if err = unpackExampleProject(dest); err != nil { 79 | return err 80 | } 81 | 82 | // And then create the LOCKFILE. 83 | var lfp *os.File 84 | if lfp, err = os.Create(lockFile); err != nil { 85 | return err 86 | } 87 | lfp.Close() 88 | 89 | // All done! Let's tell the user we've finished. 90 | if dest == "." { 91 | fmt.Printf("New project created at the current directory.\n") 92 | } else { 93 | fmt.Printf("New project created at %s.\n", dest) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func init() { 100 | // Describing the "init" subcommand. 101 | cli.Register("init", cli.Entry{ 102 | Description: "Creates a new Luminos site scaffold in the given PATH.", 103 | Usage: "init [PATH]", 104 | Command: &initCommand{}, 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /command-run.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "flag" 26 | "fmt" 27 | //"github.com/howeyc/fsnotify" 28 | "log" 29 | "net" 30 | "net/http" 31 | "net/http/fcgi" 32 | "os" 33 | 34 | "menteslibres.net/gosexy/cli" 35 | "menteslibres.net/gosexy/to" 36 | "menteslibres.net/gosexy/yaml" 37 | ) 38 | 39 | // Default values 40 | const ( 41 | envSettingsFile = "./settings.yaml" 42 | envServerDomain = "unix" 43 | envServerProtocol = "tcp" 44 | ) 45 | 46 | // Global software settings. 47 | var settings *yaml.Yaml 48 | 49 | // Command line settings. 50 | var flagSettings = flag.String("c", envSettingsFile, "Path to the settings.yaml file.") 51 | 52 | // runCommand is the structure that provides instructions for the "luminos 53 | // run" subcommand. 54 | type runCommand struct { 55 | } 56 | 57 | // Execute runs a luminos server using a settings file. 58 | func (c *runCommand) Execute() (err error) { 59 | var stat os.FileInfo 60 | 61 | // If no settings file was specified, use the default. 62 | if *flagSettings == "" { 63 | *flagSettings = envSettingsFile 64 | } 65 | 66 | // Attempt to stat the settings file. 67 | stat, err = os.Stat(*flagSettings) 68 | 69 | // It must not return an error. 70 | if err != nil { 71 | return fmt.Errorf("Error while opening %s: %q", *flagSettings, err) 72 | } 73 | 74 | // We must have a value in stat. 75 | if stat == nil { 76 | return fmt.Errorf("Could not load settings file: %s.", *flagSettings) 77 | } 78 | 79 | // And the file must not be a directory. 80 | if stat.IsDir() { 81 | return fmt.Errorf("Could not open %s: it's a directory!", *flagSettings) 82 | } 83 | 84 | // Now that we're positively sure that we have a valid file, let's try to 85 | // read settings from it. 86 | if settings, err = loadSettings(*flagSettings); err != nil { 87 | return fmt.Errorf("Error while reading settings file %s: %q", *flagSettings, err) 88 | } 89 | 90 | // Starting settings watcher. 91 | if err = settingsWatcher(); err == nil { 92 | watch.Watch(*flagSettings) 93 | } 94 | 95 | // Reading setttings. 96 | serverType := to.String(settings.Get("server", "type")) 97 | 98 | domain := envServerDomain 99 | address := to.String(settings.Get("server", "socket")) 100 | 101 | if address == "" { 102 | domain = envServerProtocol 103 | address = fmt.Sprintf("%s:%d", to.String(settings.Get("server", "bind")), to.Int64(settings.Get("server", "port"))) 104 | } 105 | 106 | // Creating a network listener. 107 | var listener net.Listener 108 | 109 | if listener, err = net.Listen(domain, address); err != nil { 110 | return fmt.Errorf("Could not create network listener: %q", err) 111 | } 112 | 113 | // Listener must be closed when the function exits. 114 | defer listener.Close() 115 | 116 | // Attempt to start a server. 117 | switch serverType { 118 | case "fastcgi": 119 | if err == nil { 120 | log.Printf("Starting FastCGI server. Listening at %s.\n", address) 121 | fcgi.Serve(listener, &server{}) 122 | } else { 123 | return fmt.Errorf("Failed to start FastCGI server: %q", err) 124 | } 125 | case "standalone": 126 | if err == nil { 127 | log.Printf("Starting HTTP server. Listening at %s.\n", address) 128 | http.Serve(listener, &server{}) 129 | } else { 130 | return fmt.Errorf("Failed to start HTTP server: %q", err) 131 | } 132 | default: 133 | return fmt.Errorf("Unknown server type: %s", serverType) 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func init() { 140 | // Describing the "run" subcommand. 141 | 142 | cli.Register("run", cli.Entry{ 143 | Name: "run", 144 | Description: "Runs a luminos server.", 145 | Arguments: []string{"c"}, 146 | Command: &runCommand{}, 147 | }) 148 | 149 | } 150 | -------------------------------------------------------------------------------- /command-version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | 27 | "menteslibres.net/gosexy/cli" 28 | ) 29 | 30 | // versionCommand is the structure that provides instructions for the "luminos 31 | // version" subcommand. 32 | type versionCommand struct { 33 | } 34 | 35 | // Execute prints the software version to the standard output. 36 | func (c *versionCommand) Execute() error { 37 | fmt.Printf("Luminos version: %s\n", Version) 38 | return nil 39 | } 40 | 41 | func init() { 42 | // Describing the "version" subcommand. 43 | cli.Register("version", cli.Entry{ 44 | Name: "version", 45 | Description: "Prints software version.", 46 | Command: &versionCommand{}, 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /host/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package host 23 | 24 | import ( 25 | "fmt" 26 | //"github.com/howeyc/fsnotify" 27 | "bytes" 28 | "errors" 29 | "html/template" 30 | "io/ioutil" 31 | "log" 32 | "net/http" 33 | "os" 34 | "path" 35 | "regexp" 36 | "strings" 37 | "time" 38 | 39 | "github.com/russross/blackfriday" 40 | "menteslibres.net/gosexy/to" 41 | "menteslibres.net/gosexy/yaml" 42 | "menteslibres.net/luminos/page" 43 | "menteslibres.net/luminos/watcher" 44 | ) 45 | 46 | const ( 47 | pathSeparator = string(os.PathSeparator) 48 | settingsFile = "site.yaml" 49 | ) 50 | 51 | var ( 52 | // Used to guess when dealing with an external URL. 53 | isExternalLinkPattern = regexp.MustCompile(`^[a-zA-Z0-9]+:\/\/`) 54 | ) 55 | 56 | // Host is the struct that represents virtual hosts. 57 | type Host struct { 58 | // Host name 59 | Name string 60 | // Main directory 61 | DocumentRoot string 62 | // Main path 63 | Path string 64 | // Settings 65 | Settings *yaml.Yaml 66 | // Templates (not fully functional yet) 67 | Templates map[string]*template.Template 68 | // Function map for templates. 69 | template.FuncMap 70 | // Standard request. 71 | *http.Request 72 | // Standard response writer. 73 | http.ResponseWriter 74 | // Function map 75 | funcMap template.FuncMap 76 | // File watcher 77 | //Watcher *fsnotify.Watcher 78 | Watcher *watcher.Watcher 79 | // Template root 80 | TemplateRoot string 81 | } 82 | 83 | // Expected extensions. Elements on the left have precedence. 84 | var extensions = []string{ 85 | ".md", 86 | ".html", 87 | ".txt", 88 | ".md.tpl", 89 | } 90 | 91 | // fixDeprecatedSyntax fixes old template syntax. 92 | func fixDeprecatedSyntax(s string) string { 93 | 94 | s = strings.Replace(s, "{{ link", "{{ anchor", -1) 95 | s = strings.Replace(s, "{{link", "{{anchor", -1) 96 | s = strings.Replace(s, ".link", ".URL", -1) 97 | s = strings.Replace(s, ".url", ".URL", -1) 98 | s = strings.Replace(s, ".text", ".Text", -1) 99 | s = strings.Replace(s, "jstext", "js", -1) 100 | s = strings.Replace(s, "htmltext", "html", -1) 101 | 102 | return s 103 | } 104 | 105 | func (host *Host) getContentPath() (string, error) { 106 | var directories []string 107 | 108 | contentdir := to.String(host.Settings.Get("content", "markdown")) 109 | if contentdir == "" { 110 | directories = []string{ 111 | "content", 112 | "markdown", 113 | } 114 | } else { 115 | directories = []string{contentdir} 116 | } 117 | 118 | for _, directory := range directories { 119 | path := host.DocumentRoot + pathSeparator + directory 120 | if _, err := os.Stat(path); err == nil { 121 | return path, nil 122 | } 123 | } 124 | 125 | return "", errors.New(`Content directory was not found.`) 126 | } 127 | 128 | // readFile attempts to read a file from disk and returns its contents. 129 | func readFile(file string) (string, error) { 130 | var buf []byte 131 | var err error 132 | 133 | if buf, err = ioutil.ReadFile(file); err != nil { 134 | return "", fmt.Errorf("Could not read file %s: %s", file, err.Error()) 135 | } 136 | 137 | return string(buf), nil 138 | } 139 | 140 | // Close removes the watcher that is currently associated with the host. 141 | func (host *Host) Close() { 142 | host.Watcher.Close() 143 | } 144 | 145 | // asset returns a relative URL. 146 | func (host *Host) asset(assetURL string) string { 147 | if !host.isExternalLink(assetURL) { 148 | assetURL = strings.TrimLeft(assetURL, "/") 149 | p := strings.Trim(host.Path, "/") 150 | if p == "" { 151 | return "/" + assetURL 152 | } 153 | return "/" + p + "/" + assetURL 154 | } 155 | return assetURL 156 | } 157 | 158 | // url returns an absolute URL. 159 | func (host *Host) url(url string) string { 160 | if host.isExternalLink(url) == false { 161 | return "//" + host.Request.Host + "/" + strings.TrimLeft(url, "/") 162 | } 163 | return url 164 | } 165 | 166 | // isExternalLink returns true if the given URL is outside this host. 167 | func (host *Host) isExternalLink(url string) bool { 168 | return isExternalLinkPattern.MatchString(url) 169 | } 170 | 171 | // setting function returns a setting value. 172 | func (host *Host) setting(path string) interface{} { 173 | route := strings.Split(path, "/") 174 | args := make([]interface{}, len(route)) 175 | for i := range route { 176 | args[i] = route[i] 177 | } 178 | setting := host.Settings.Get(args...) 179 | return fixSetting(setting) 180 | } 181 | 182 | // fixSetting returns additional keys to make certain maps act like anchors. 183 | func fixSetting(setting interface{}) interface{} { 184 | 185 | if m, ok := setting.(map[interface{}]interface{}); ok { 186 | for k, v := range m { 187 | switch k { 188 | case "text": 189 | m["Text"] = v 190 | case "link", "url": 191 | m["URL"] = v 192 | } 193 | } 194 | } 195 | 196 | return setting 197 | } 198 | 199 | // settings is a function that returns an array of settings. 200 | func (host *Host) settings(path string) []interface{} { 201 | route := strings.Split(path, "/") 202 | args := make([]interface{}, len(route)) 203 | for i := range route { 204 | args[i] = route[i] 205 | } 206 | val := host.Settings.Get(args...) 207 | if val == nil { 208 | return nil 209 | } 210 | 211 | ival := val.([]interface{}) 212 | 213 | for i := range ival { 214 | ival[i] = fixSetting(ival[i]) 215 | } 216 | 217 | return ival 218 | } 219 | 220 | // javascriptText is a function for funcMap that writes text as Javascript. 221 | func javascriptText(text string) template.JS { 222 | return template.JS(text) 223 | } 224 | 225 | // htmlText is a function for funcMap that writes text as plain HTML. 226 | func htmlText(text string) template.HTML { 227 | return template.HTML(text) 228 | } 229 | 230 | // Function for funcMap that writes links. 231 | func (host *Host) anchor(url, text string) template.HTML { 232 | if host.isExternalLink(url) { 233 | return template.HTML(fmt.Sprintf(`%s`, host.asset(url), text)) 234 | } 235 | return template.HTML(fmt.Sprintf(`%s`, host.asset(url), text)) 236 | } 237 | 238 | // guessFile checks for files names and returns a guessed name. 239 | func guessFile(file string, descend bool) (string, os.FileInfo) { 240 | stat, err := os.Stat(file) 241 | 242 | file = strings.TrimRight(file, pathSeparator) 243 | 244 | if descend { 245 | if err == nil { 246 | if stat.IsDir() { 247 | f, s := guessFile(file+pathSeparator+"index", true) 248 | if s != nil { 249 | return f, s 250 | } 251 | } 252 | return file, stat 253 | } 254 | for _, extension := range extensions { 255 | f, s := guessFile(file+extension, false) 256 | if s != nil { 257 | return f, s 258 | } 259 | } 260 | } 261 | 262 | if err == nil { 263 | return file, stat 264 | } 265 | 266 | return "", nil 267 | } 268 | 269 | // readFile opens a file and reads its contents, if the file has the .md 270 | // extension the contents are parsed and HTML is returned. 271 | func (host *Host) readFile(file string) ([]byte, error) { 272 | var buf []byte 273 | var err error 274 | 275 | if buf, err = ioutil.ReadFile(file); err != nil { 276 | return nil, err 277 | } 278 | 279 | if strings.HasSuffix(file, ".tpl") { 280 | var out bytes.Buffer 281 | tpl, err := template.New("").Funcs(host.funcMap).Parse(string(buf)) 282 | if err != nil { 283 | return nil, err 284 | } 285 | if err := tpl.Execute(&out, nil); err != nil { 286 | return nil, err 287 | } 288 | file = file[:len(file)-4] 289 | buf = out.Bytes() 290 | } 291 | 292 | if strings.HasSuffix(file, ".md") { 293 | buf = blackfriday.MarkdownCommon(buf) 294 | } 295 | 296 | return buf, nil 297 | } 298 | 299 | func chunk(value string) string { 300 | if value == "" { 301 | return "-" 302 | } 303 | return value 304 | } 305 | 306 | // ServeHTTP reads a request and creates an appropriate response. 307 | func (host *Host) ServeHTTP(w http.ResponseWriter, req *http.Request) { 308 | var localFile string 309 | 310 | // TODO: Fix this non-critical race condition. We need to save some 311 | // variables in a per request basis, in particular the hostname. It may not 312 | // always match the host name we gave to it (i.e: the "default" hostname). A 313 | // per-request context would be useful. 314 | host.Request = req 315 | 316 | // Settings default status as not found. 317 | status := http.StatusNotFound 318 | 319 | // Default size is no size (-1). 320 | size := -1 321 | 322 | // Requested path 323 | reqpath := strings.TrimRight(req.URL.Path, "/") 324 | 325 | // Stripping path 326 | index := len(host.Path) 327 | 328 | // If the hosts contains a path and the request begins with the same path, it 329 | // is ignored for the matches. 330 | if reqpath[0:index] == host.Path { 331 | reqpath = reqpath[index:] 332 | } 333 | 334 | reqpath = strings.TrimRight(reqpath, "/") 335 | 336 | // Trying to match a file on webroot/ 337 | webrootdir := to.String(host.Settings.Get("content", "webroot")) 338 | 339 | if webrootdir == "" { 340 | webrootdir = "webroot" 341 | } 342 | 343 | // Absolute local webroot. 344 | webroot := host.DocumentRoot + pathSeparator + webrootdir 345 | 346 | // Attempt to match a request with a file in webroot/. 347 | localFile = webroot + pathSeparator + reqpath 348 | 349 | stat, err := os.Stat(localFile) 350 | 351 | if err == nil { 352 | // File exists 353 | if stat.IsDir() == false { 354 | // Exists and it's not a directory, let's serve it. 355 | status = http.StatusOK // Changing status. 356 | http.ServeFile(w, req, localFile) 357 | size = int(stat.Size()) 358 | } 359 | } 360 | 361 | // Was the status already changed? 362 | if status == http.StatusNotFound { 363 | 364 | // Absolute document root. 365 | var docroot string 366 | if docroot, err = host.getContentPath(); err != nil { 367 | http.Error(w, err.Error(), http.StatusInternalServerError) 368 | return 369 | } 370 | 371 | // Defining a filename to look for. 372 | testFile := docroot + pathSeparator + reqpath 373 | 374 | localFile, stat = guessFile(testFile, true) 375 | 376 | if stat != nil { 377 | 378 | if reqpath != "" { 379 | // Let's not accept paths ending in "/". 380 | if stat.IsDir() == false { 381 | if strings.HasSuffix(req.URL.Path, "/") == true { 382 | http.Redirect(w, req, "/"+host.Path+"/"+reqpath, 301) 383 | w.Write([]byte(http.StatusText(301))) 384 | return 385 | } 386 | } else { 387 | if strings.HasSuffix(req.URL.Path, "/") == false { 388 | http.Redirect(w, req, req.URL.Path+"/", 301) 389 | w.Write([]byte(http.StatusText(301))) 390 | return 391 | } 392 | } 393 | } 394 | 395 | // Creating a page. 396 | p := &page.Page{} 397 | 398 | p.FilePath = localFile 399 | p.BasePath = req.URL.Path 400 | 401 | relPath := localFile[len(docroot):] 402 | 403 | if stat.IsDir() == false { 404 | p.FileDir = path.Dir(localFile) 405 | p.BasePath = path.Dir(relPath) 406 | } else { 407 | p.FileDir = localFile 408 | p.BasePath = relPath 409 | } 410 | 411 | // Reading contents. 412 | content, err := host.readFile(localFile) 413 | 414 | if err == nil { 415 | p.Content = template.HTML(content) 416 | } 417 | 418 | p.FileDir = strings.TrimRight(p.FileDir, pathSeparator) + pathSeparator 419 | p.BasePath = strings.TrimRight(p.BasePath, pathSeparator) + pathSeparator 420 | 421 | // werc-like header and footer. 422 | hfile, hstat := guessFile(p.FileDir+"_header", true) 423 | 424 | if hstat != nil { 425 | hcontent, herr := host.readFile(hfile) 426 | if herr == nil { 427 | p.ContentHeader = template.HTML(hcontent) 428 | } 429 | } 430 | 431 | if strings.Trim(host.Path, pathSeparator) == strings.Trim(req.URL.Path, pathSeparator) { 432 | p.IsHome = true 433 | } 434 | 435 | // werc-like header and footer. 436 | ffile, fstat := guessFile(p.FileDir+"_footer", true) 437 | 438 | if fstat != nil { 439 | fcontent, ferr := host.readFile(ffile) 440 | if ferr == nil { 441 | p.ContentFooter = template.HTML(fcontent) 442 | } 443 | } 444 | 445 | p.CreateBreadCrumb() 446 | p.CreateMenu() 447 | p.CreateSideMenu() 448 | 449 | p.ProcessContent() 450 | 451 | // Applying template. 452 | if err = host.Templates["index.tpl"].Execute(w, p); err != nil { 453 | http.Error(w, err.Error(), http.StatusInternalServerError) 454 | status = http.StatusInternalServerError 455 | } else { 456 | status = http.StatusOK 457 | } 458 | 459 | } 460 | } 461 | 462 | if status == http.StatusNotFound { 463 | http.Error(w, "Not found", http.StatusNotFound) 464 | } 465 | 466 | // Log line. 467 | logLine := []string{ 468 | chunk(req.RemoteAddr), 469 | chunk(""), 470 | chunk(""), 471 | chunk("[" + time.Now().Format("02/Jan/2006:15:04:05 -0700") + "]"), 472 | chunk("\"" + fmt.Sprintf("%s %s %s", req.Method, req.RequestURI, req.Proto) + "\""), 473 | chunk(fmt.Sprintf("%d", status)), 474 | chunk(fmt.Sprintf("%d", size)), 475 | } 476 | 477 | fmt.Println(strings.Join(logLine, " ")) 478 | } 479 | 480 | func (host *Host) loadTemplate(file string) error { 481 | var err error 482 | 483 | // Reading template file. 484 | var text string 485 | if text, err = readFile(file); err != nil { 486 | return err 487 | } 488 | 489 | // Fixing template. 490 | text = fixDeprecatedSyntax(text) 491 | 492 | // Allocating name. 493 | name := path.Base(file) 494 | parsed := template.New(name).Funcs(host.funcMap) 495 | 496 | if _, err = parsed.Parse(text); err != nil { 497 | return err 498 | } 499 | 500 | host.Templates[name] = parsed 501 | 502 | if host.Watcher != nil { 503 | host.Watcher.RemoveWatch(file) 504 | host.Watcher.Watch(file) 505 | } 506 | 507 | return nil 508 | } 509 | 510 | // loadTemplates loads templates with .tpl extension from the templates 511 | // directory. At this moment only index.tpl is expected. 512 | func (host *Host) loadTemplates() error { 513 | var err error 514 | var fp *os.File 515 | 516 | tpldir := to.String(host.Settings.Get("content", "templates")) 517 | 518 | if tpldir == "" { 519 | tpldir = "templates" 520 | } 521 | 522 | tplroot := host.DocumentRoot + pathSeparator + tpldir 523 | 524 | if fp, err = os.Open(tplroot); err != nil { 525 | return fmt.Errorf("Error trying to open %s: %q", tplroot, err) 526 | } 527 | 528 | defer fp.Close() 529 | 530 | host.TemplateRoot = tplroot 531 | 532 | var files []os.FileInfo 533 | if files, err = fp.Readdir(-1); err != nil { 534 | return fmt.Errorf("Error reading directory %s: %q", tplroot, err) 535 | } 536 | 537 | for _, fp := range files { 538 | 539 | if strings.HasSuffix(fp.Name(), ".tpl") == true { 540 | 541 | file := host.TemplateRoot + pathSeparator + fp.Name() 542 | 543 | err := host.loadTemplate(file) 544 | 545 | if err != nil { 546 | log.Printf("%s: Template error in file %s: %q\n", host.Name, file, err) 547 | } 548 | 549 | } 550 | } 551 | 552 | if _, ok := host.Templates["index.tpl"]; ok == false { 553 | return fmt.Errorf("Template %s could not be found.", "index.tpl") 554 | } 555 | 556 | return nil 557 | 558 | } 559 | 560 | func (host *Host) fileWatcher() error { 561 | 562 | var err error 563 | 564 | /* 565 | // File watcher. 566 | host.Watcher, err = fsnotify.NewWatcher() 567 | 568 | if err == nil { 569 | 570 | go func() { 571 | 572 | for { 573 | 574 | select { 575 | 576 | case ev := <-host.Watcher.Event: 577 | 578 | fmt.Printf("%s: got ev: %v\n", host.Name, ev) 579 | 580 | if ev == nil { 581 | return 582 | } 583 | 584 | if ev.IsModify() { 585 | // Is settings file? 586 | if ev.Name == host.DocumentRoot+pathSeparator+settingsFile { 587 | log.Printf("%s: Reloading host settings %s...\n", host.Name, ev.Name) 588 | err := host.loadSettings() 589 | 590 | if err != nil { 591 | log.Printf("%s: Could not reload host settings: %s\n", host.Name, host.DocumentRoot+pathSeparator+settingsFile) 592 | } 593 | } 594 | 595 | // Is a template? 596 | if strings.HasPrefix(ev.Name, host.TemplateRoot) == true { 597 | 598 | if strings.HasSuffix(ev.Name, ".tpl") == true { 599 | log.Printf("%s: Reloading template %s\n", host.Name, ev.Name) 600 | host.loadTemplate(ev.Name) 601 | 602 | if err != nil { 603 | log.Printf("%s: Could not reload template %s: %q\n", host.Name, ev.Name, err) 604 | } 605 | 606 | } 607 | } 608 | 609 | } else if ev.IsDelete() { 610 | // Attemping to re-add watcher. 611 | host.Watcher.RemoveWatch(ev.Name) 612 | host.Watcher.Watch(ev.Name) 613 | } 614 | 615 | } 616 | } 617 | 618 | }() 619 | 620 | } 621 | */ 622 | 623 | // (Stupid) file modification watcher. 624 | host.Watcher, err = watcher.New() 625 | 626 | if err == nil { 627 | 628 | go func() { 629 | 630 | for { 631 | select { 632 | case ev := <-host.Watcher.Event: 633 | 634 | if ev.IsModify() { 635 | // Is settings file? 636 | if ev.Name == host.DocumentRoot+pathSeparator+settingsFile { 637 | log.Printf("%s: Reloading host settings %s...\n", host.Name, ev.Name) 638 | err := host.loadSettings() 639 | 640 | if err != nil { 641 | log.Printf("%s: Could not reload host settings: %s\n", host.Name, host.DocumentRoot+pathSeparator+settingsFile) 642 | } 643 | } 644 | 645 | // Is a template? 646 | if strings.HasPrefix(ev.Name, host.TemplateRoot) == true { 647 | if strings.HasSuffix(ev.Name, ".tpl") == true { 648 | log.Printf("%s: Reloading template %s\n", host.Name, ev.Name) 649 | host.loadTemplate(ev.Name) 650 | if err != nil { 651 | log.Printf("%s: Could not reload template %s: %q\n", host.Name, ev.Name, err) 652 | } 653 | } 654 | } 655 | } 656 | } 657 | } 658 | }() 659 | } 660 | 661 | return err 662 | 663 | } 664 | 665 | // loadSettings loads settings for the host. 666 | func (host *Host) loadSettings() error { 667 | 668 | var settings *yaml.Yaml 669 | 670 | file := host.DocumentRoot + pathSeparator + settingsFile 671 | 672 | _, err := os.Stat(file) 673 | 674 | if err == nil { 675 | settings, err = yaml.Open(file) 676 | if err != nil { 677 | return fmt.Errorf(`Could not parse settings file (%s): %q`, file, err) 678 | } 679 | } else { 680 | return fmt.Errorf(`Error trying to open settings file (%s): %q.`, file, err) 681 | } 682 | 683 | if host.Watcher != nil { 684 | host.Watcher.RemoveWatch(file) 685 | host.Watcher.Watch(file) 686 | } 687 | 688 | host.Settings = settings 689 | 690 | return nil 691 | } 692 | 693 | // New creates and returns a host. 694 | func New(name string, root string) (*Host, error) { 695 | 696 | _, err := os.Stat(root) 697 | 698 | if err != nil { 699 | log.Printf("Error reading directory %s: %q\n", root, err) 700 | log.Printf("Checkout an example directory at https://github.com/xiam/luminos/tree/master/default\n") 701 | 702 | return nil, err 703 | } 704 | 705 | route := "/" 706 | name = strings.TrimRight(name, "/") 707 | 708 | index := strings.Index(name, "/") 709 | if index > -1 { 710 | route = name[index:] 711 | } 712 | 713 | host := &Host{ 714 | Name: strings.TrimRight(name, "/"), 715 | Path: strings.TrimRight(route, "/"), 716 | DocumentRoot: root, 717 | Templates: make(map[string]*template.Template), 718 | } 719 | 720 | host.funcMap = template.FuncMap{ 721 | "url": func(s string) string { return host.url(s) }, 722 | "anchor": func(a, b string) template.HTML { return host.anchor(a, b) }, 723 | "asset": func(s string) string { return host.asset(s) }, 724 | "include": func(f string) string { 725 | s, err := readFile(host.DocumentRoot + "/" + f) 726 | if err != nil { 727 | log.Printf("readFile: %q", err) 728 | } 729 | return s 730 | }, 731 | "setting": func(s string) interface{} { return host.setting(s) }, 732 | "settings": func(s string) []interface{} { return host.settings(s) }, 733 | "js": javascriptText, 734 | "html": htmlText, 735 | } 736 | 737 | // Watcher 738 | host.fileWatcher() 739 | 740 | // Loading host settings 741 | if err = host.loadSettings(); err != nil { 742 | log.Printf("Could not start host: %s\n", name) 743 | return nil, err 744 | } 745 | 746 | // Loading templates. 747 | if err = host.loadTemplates(); err != nil { 748 | log.Printf("Could not start host: %s\n", name) 749 | return nil, err 750 | } 751 | 752 | log.Printf("Routing: %s -> %s\n", name, root) 753 | 754 | return host, nil 755 | 756 | } 757 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package main // package "menteslibres.net/luminos" 23 | 24 | import ( 25 | "log" 26 | "os" 27 | 28 | "menteslibres.net/gosexy/cli" 29 | ) 30 | 31 | // Handy path separator. 32 | const pathSeparator = string(os.PathSeparator) 33 | 34 | // Version holds the software version. 35 | const Version = "0.9" 36 | 37 | func main() { 38 | // Software properties. 39 | cli.Name = "Luminos Markdown Server" 40 | cli.Homepage = "https://menteslibres.net/luminos" 41 | cli.Author = "J. Carlos Nieto" 42 | cli.Version = Version 43 | cli.AuthorEmail = "jose.carlos@menteslibres.net" 44 | 45 | // Shows banner 46 | cli.Banner() 47 | 48 | // Dispatches the command. 49 | if err := cli.Dispatch(); err != nil { 50 | log.Fatal("Could not start Luminos: ", err) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /page/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package page 23 | 24 | import ( 25 | "fmt" 26 | "html/template" 27 | "log" 28 | "os" 29 | "path" 30 | "regexp" 31 | "sort" 32 | "strconv" 33 | "strings" 34 | 35 | "github.com/extemporalgenome/slug" 36 | ) 37 | 38 | var titlePattern = regexp.MustCompile(`(.+)`) 39 | 40 | type anchor struct { 41 | Text string 42 | URL string 43 | children []anchor 44 | } 45 | 46 | var homeAnchor = anchor{Text: "Home", URL: "/"} 47 | 48 | var ( 49 | titleReplacePattern = regexp.MustCompile(`[-_]`) 50 | ) 51 | 52 | // Page struct holds information on the current document being served. 53 | type Page struct { 54 | 55 | // Title of the page. This is guessed from the current document. (It looks 56 | // for the first H1, H2, ..., H6 tag). 57 | Title string 58 | 59 | // The HTML source of the current document. 60 | Content template.HTML 61 | 62 | // The HTML source of the _header.md or _header.html file on the current 63 | // document's directory. 64 | ContentHeader template.HTML 65 | 66 | // The HTML source of the _footer.md or _footer.html file on the current 67 | // document's directory. 68 | ContentFooter template.HTML 69 | 70 | // Titles holds links to all page subtitles. 71 | Titles map[int][]anchor 72 | 73 | // An array that contains names and links of all the items on the document's 74 | // root. Names that begin with a dot or an underscore are ignored from the 75 | // listing. 76 | Menu []anchor 77 | 78 | // An array that contains names and links of all the items on the current 79 | // document's directory. Names that begin with a dot or an underscore are 80 | // ignored from the listing. 81 | SideMenu []anchor 82 | 83 | // An array of anchors that contain names and URLs of the current document's 84 | // path. 85 | BreadCrumb []anchor 86 | 87 | // Contains the name and URL of the current page. 88 | CurrentPage anchor 89 | 90 | // Absolute path of the current document. 91 | FilePath string 92 | 93 | // Absolute parent directory of the current document. 94 | FileDir string 95 | 96 | // Relative path of the current document. 97 | BasePath string 98 | 99 | // Relative parent directory of the current document. 100 | BaseDir string 101 | 102 | // True if the current document is / (home). 103 | IsHome bool 104 | } 105 | 106 | const ( 107 | pathSeparator = string(os.PathSeparator) 108 | ) 109 | 110 | // List of known extensions. 111 | var knownExtensions = []string{".html", ".md", ""} 112 | 113 | // fileList struct is a sorted list of files. 114 | type fileList []os.FileInfo 115 | 116 | func (f fileList) Len() int { 117 | return len(f) 118 | } 119 | 120 | func (f fileList) Less(i, j int) bool { 121 | return f[i].Name() < f[j].Name() 122 | } 123 | 124 | func (f fileList) Swap(i, j int) { 125 | f[i], f[j] = f[j], f[i] 126 | } 127 | 128 | // removeKnownExtension strips out known extensions from a given file name. 129 | func removeKnownExtension(s string) string { 130 | fileExt := path.Ext(s) 131 | 132 | for _, ext := range knownExtensions { 133 | if ext != "" { 134 | if fileExt == ext { 135 | return s[:len(s)-len(ext)] 136 | } 137 | } 138 | } 139 | 140 | return s 141 | } 142 | 143 | // filterList returns files in a directory passed through a filter. 144 | func filterList(directory string, filter func(os.FileInfo) bool) fileList { 145 | var list fileList 146 | var err error 147 | 148 | // Attempt to open directory. 149 | var fp *os.File 150 | if fp, err = os.Open(directory); err != nil { 151 | panic(err) 152 | } 153 | 154 | defer fp.Close() 155 | 156 | // Listing directory contents. 157 | var dirContents []os.FileInfo 158 | if dirContents, err = fp.Readdir(-1); err != nil { 159 | panic(err) 160 | } 161 | 162 | // Looping over directory contents. 163 | for _, file := range dirContents { 164 | if filter(file) == true { 165 | list = append(list, file) 166 | } 167 | } 168 | 169 | // Sorting file list. 170 | sort.Sort(struct{ fileList }{list}) 171 | 172 | return list 173 | } 174 | 175 | // dummyFilter is a filter for filterList. Returns all files except for those 176 | // that begin with "." or "_". 177 | func dummyFilter(f os.FileInfo) bool { 178 | if !strings.HasPrefix(f.Name(), ".") && !strings.HasPrefix(f.Name(), "_") { 179 | return true 180 | } 181 | return false 182 | } 183 | 184 | // directoryFilter is a filter for filterList. Returns all directories except 185 | // those that begin with "." or "_". 186 | func directoryFilter(f os.FileInfo) bool { 187 | if !strings.HasPrefix(f.Name(), ".") && !strings.HasPrefix(f.Name(), "_") { 188 | return f.IsDir() 189 | } 190 | return false 191 | } 192 | 193 | // fileFilter is a filter for filterList. Returns all files except for those 194 | // that begin with "." or "_". 195 | func fileFilter(f os.FileInfo) bool { 196 | if !strings.HasPrefix(f.Name(), ".") && !strings.HasPrefix(f.Name(), "_") { 197 | return (f.IsDir() == false) 198 | } 199 | return false 200 | } 201 | 202 | // createTitle expects a filename and returns a stylized human title. 203 | func createTitle(s string) string { 204 | s = removeKnownExtension(s) 205 | s = titleReplacePattern.ReplaceAllString(s, " ") 206 | return strings.Title(s[:1]) + s[1:] 207 | } 208 | 209 | // CreateLink returns a link to another page. 210 | func (p *Page) CreateLink(file os.FileInfo, prefix string) anchor { 211 | item := anchor{} 212 | 213 | if file.IsDir() == true { 214 | item.URL = prefix + file.Name() 215 | } else { 216 | item.URL = prefix + removeKnownExtension(file.Name()) 217 | } 218 | 219 | item.URL = path.Clean(item.URL) 220 | 221 | item.Text = createTitle(file.Name()) 222 | 223 | return item 224 | } 225 | 226 | // CreateMenu scans files and directories and builds a list of children links. 227 | func (p *Page) CreateMenu() { 228 | var item anchor 229 | p.Menu = []anchor{} 230 | 231 | files := filterList(p.FileDir, directoryFilter) 232 | 233 | for _, file := range files { 234 | item = p.CreateLink(file, p.BasePath) 235 | children := filterList(p.FileDir+pathSeparator+file.Name(), directoryFilter) 236 | if len(children) > 0 { 237 | item.children = make([]anchor, 0, len(children)) 238 | for _, child := range children { 239 | childItem := p.CreateLink(child, p.BasePath+file.Name()) 240 | item.children = append(item.children, childItem) 241 | } 242 | } 243 | p.Menu = append(p.Menu, item) 244 | } 245 | } 246 | 247 | // CreateBreadCrumb populates Page.BreadCrumb with links. 248 | func (p *Page) CreateBreadCrumb() { 249 | 250 | chunks := strings.Split(strings.Trim(p.BasePath, "/"), "/") 251 | 252 | p.BreadCrumb = make([]anchor, 0, len(chunks)+1) 253 | 254 | p.BreadCrumb = append(p.BreadCrumb, homeAnchor) 255 | 256 | prefix := "" 257 | 258 | for _, chunk := range chunks { 259 | if chunk != "" { 260 | 261 | item := anchor{ 262 | URL: prefix + "/" + chunk, 263 | Text: createTitle(chunk), 264 | } 265 | 266 | prefix = prefix + pathSeparator + chunk 267 | p.BreadCrumb = append(p.BreadCrumb, item) 268 | } 269 | } 270 | 271 | p.CurrentPage = p.BreadCrumb[len(p.BreadCrumb)-1] 272 | } 273 | 274 | // CreateSideMenu populates Page.SideMenu with files on the current document's 275 | // directory. 276 | func (p *Page) CreateSideMenu() { 277 | var item anchor 278 | 279 | files := filterList(p.FileDir, dummyFilter) 280 | 281 | p.SideMenu = make([]anchor, 0, len(files)) 282 | 283 | for _, file := range files { 284 | item = p.CreateLink(file, p.BasePath) 285 | if strings.ToLower(item.Text) != "index" { 286 | p.SideMenu = append(p.SideMenu, item) 287 | } 288 | } 289 | 290 | if strings.Trim(p.BasePath, "/") == "" { 291 | return 292 | } 293 | 294 | if len(p.SideMenu) == 0 { 295 | 296 | // Attempt to index parent directory. 297 | files = filterList(p.FileDir+pathSeparator+"..", dummyFilter) 298 | 299 | for _, file := range files { 300 | item = p.CreateLink(file, p.BasePath+".."+pathSeparator) 301 | if strings.ToLower(item.Text) != "index" { 302 | p.SideMenu = append(p.SideMenu, item) 303 | } 304 | } 305 | 306 | } 307 | } 308 | 309 | func (p *Page) ProcessContent() { 310 | content := string(p.Content) 311 | titles := titlePattern.FindAllStringSubmatch(content, -1) 312 | for _, title := range titles { 313 | if p.Titles == nil { 314 | p.Titles = make(map[int][]anchor) 315 | } 316 | if len(title) == 3 { 317 | if level, _ := strconv.Atoi(title[1]); level > 0 { 318 | ll := level - 1 319 | text := title[2] 320 | 321 | id := slug.Slug(text) 322 | 323 | if id == "" { 324 | id = fmt.Sprintf("%05d", level) 325 | } 326 | 327 | if p.Titles[ll] == nil { 328 | p.Titles[ll] = []anchor{} 329 | } 330 | 331 | r := fmt.Sprintf(`%s`, level, id, id, text, level) 332 | p.Titles[ll] = append(p.Titles[ll], anchor{Text: text, URL: "#" + id}) 333 | 334 | content = strings.Replace(content, title[0], r, 1) 335 | } 336 | } 337 | } 338 | p.Content = template.HTML(content) 339 | } 340 | 341 | func (p *Page) GetTitlesFromLevel(ll int) []anchor { 342 | if p.Titles == nil || p.Titles[ll] == nil { 343 | return []anchor{} 344 | } 345 | return p.Titles[ll] 346 | } 347 | 348 | func (p *Page) URLMatch(s string) bool { 349 | re, err := regexp.Compile(s) 350 | if err != nil { 351 | log.Printf("URLMatch: %q", err) 352 | return false 353 | } 354 | return re.MatchString(p.CurrentPage.URL) 355 | } 356 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | package main 23 | 24 | import ( 25 | "errors" 26 | "fmt" 27 | //"github.com/howeyc/fsnotify" 28 | "log" 29 | "net/http" 30 | "os" 31 | "strings" 32 | 33 | "menteslibres.net/gosexy/to" 34 | "menteslibres.net/gosexy/yaml" 35 | "menteslibres.net/luminos/host" 36 | "menteslibres.net/luminos/watcher" 37 | ) 38 | 39 | // Map of hosts. 40 | var hosts map[string]*host.Host 41 | 42 | // File watcher. 43 | var watch *watcher.Watcher 44 | 45 | type server struct { 46 | } 47 | 48 | func init() { 49 | // Allocating map. 50 | hosts = make(map[string]*host.Host) 51 | } 52 | 53 | // Finds the appropriate hosts for a request. 54 | func route(req *http.Request) *host.Host { 55 | 56 | // Request's hostname. 57 | name := req.Host 58 | 59 | // Removing the port part of the host. 60 | if strings.Contains(name, ":") { 61 | name = name[0:strings.Index(name, ":")] 62 | } 63 | 64 | // Host and path. 65 | path := name + req.URL.Path 66 | 67 | // Searching for the host that best matches this request. 68 | match := "" 69 | 70 | for key := range hosts { 71 | lkey := len(key) 72 | if key[0] == '/' { 73 | if lkey <= len(req.URL.Path) { 74 | if req.URL.Path[0:lkey] == key { 75 | match = key 76 | } 77 | } 78 | } 79 | if lkey <= len(path) { 80 | if path[0:lkey] == key { 81 | match = key 82 | } 83 | } 84 | } 85 | 86 | // No host matched, let's use the default host. 87 | if match == "" { 88 | log.Printf("Path %v could not match any route, falling back to the default.\n", path) 89 | match = "default" 90 | } 91 | 92 | // Let's verify and return the host. 93 | if _, ok := hosts[match]; !ok { 94 | // Host was not found. 95 | log.Printf("Request for unknown host: %s\n", req.Host) 96 | return nil 97 | } 98 | 99 | return hosts[match] 100 | } 101 | 102 | // Routes a request and lets the host handle it. 103 | func (s server) ServeHTTP(wri http.ResponseWriter, req *http.Request) { 104 | r := route(req) 105 | if r != nil { 106 | r.ServeHTTP(wri, req) 107 | } else { 108 | log.Printf("Failed to serve host %s.\n", req.Host) 109 | http.Error(wri, "Not found", http.StatusNotFound) 110 | } 111 | } 112 | 113 | // Loads settings 114 | func loadSettings(file string) (*yaml.Yaml, error) { 115 | 116 | var entries map[interface{}]interface{} 117 | var ok bool 118 | 119 | // Trying to read settings from file. 120 | y, err := yaml.Open(file) 121 | 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | // Loading and verifying host entries 127 | if entries, ok = y.Get("hosts").(map[interface{}]interface{}); ok == false { 128 | return nil, errors.New("Missing \"hosts\" entry.") 129 | } 130 | 131 | h := map[string]*host.Host{} 132 | 133 | // Populating host entries. 134 | for key := range entries { 135 | name := to.String(key) 136 | path := to.String(entries[name]) 137 | 138 | info, err := os.Stat(path) 139 | if err != nil { 140 | return nil, fmt.Errorf("Failed to validate host %s: %q.", name, err) 141 | } 142 | if info.IsDir() == false { 143 | return nil, fmt.Errorf("Host %s does not point to a directory.", name) 144 | } 145 | 146 | h[name], err = host.New(name, path) 147 | 148 | if err != nil { 149 | return nil, fmt.Errorf("Failed to initialize host %s: %q.", name, err) 150 | } 151 | } 152 | 153 | for name := range hosts { 154 | hosts[name].Close() 155 | } 156 | 157 | hosts = h 158 | 159 | if _, ok := hosts["default"]; ok == false { 160 | log.Printf("Warning: default host was not provided.\n") 161 | } 162 | 163 | return y, nil 164 | } 165 | 166 | func settingsWatcher() error { 167 | 168 | var err error 169 | 170 | /* 171 | // Watching settings file for changes. 172 | // Was not properly returning events on OSX. 173 | // https://github.com/howeyc/fsnotify/issues/34 174 | 175 | watcher, err := fsnotify.NewWatcher() 176 | 177 | if err == nil { 178 | defer watcher.Close() 179 | 180 | go func() { 181 | for { 182 | select { 183 | case ev := <-watcher.Event: 184 | if ev == nil { 185 | return 186 | } 187 | if ev.IsModify() { 188 | log.Printf("Trying to reload settings file %s...\n", ev.Name) 189 | y, err := loadSettings(ev.Name) 190 | if err != nil { 191 | log.Printf("Error loading settings file %s: %q\n", ev.Name, err) 192 | } else { 193 | settings = y 194 | } 195 | } else if ev.IsDelete() { 196 | watcher.RemoveWatch(ev.Name) 197 | watcher.Watch(ev.Name) 198 | } 199 | case err := <-watcher.Error: 200 | log.Printf("Watcher error: %q\n", err) 201 | } 202 | } 203 | }() 204 | 205 | watcher.Watch(settingsFile) 206 | } 207 | */ 208 | 209 | // (Stupid) time based file modification watcher. 210 | watch, err = watcher.New() 211 | 212 | if err == nil { 213 | go func() { 214 | defer watch.Close() 215 | for { 216 | select { 217 | case ev := <-watch.Event: 218 | if ev.IsModify() { 219 | y, err := loadSettings(ev.Name) 220 | if err != nil { 221 | log.Printf("Error loading settings file %s: %q\n", ev.Name, err) 222 | } else { 223 | log.Printf("Reloading settings file %s.\n", ev.Name) 224 | settings = y 225 | } 226 | } 227 | } 228 | } 229 | }() 230 | 231 | } 232 | 233 | return err 234 | } 235 | -------------------------------------------------------------------------------- /watcher/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2014 José Carlos Nieto, https://menteslibres.net/xiam 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining 4 | // a copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so, subject to 9 | // the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be 12 | // included in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | // Package watcher is a stupid time-based file modification watcher, I expect 23 | // to use fsnotify once it becomes consistent among all platforms. You should 24 | // not use it. 25 | package watcher 26 | 27 | import ( 28 | "os" 29 | "time" 30 | ) 31 | 32 | // Event is the struct that watches a file. 33 | type Event struct { 34 | Name string 35 | // We don't need other event besides modification. 36 | isModify bool 37 | } 38 | 39 | // Watcher is the struct that handles a the list of file to watch. 40 | type Watcher struct { 41 | Files map[string]*WatcherFile 42 | Event chan (*Event) 43 | t time.Duration 44 | watching bool 45 | } 46 | 47 | // WatcherFile is the struct that handles the last known file properties. 48 | type WatcherFile struct { 49 | Filemtime time.Time 50 | } 51 | 52 | // IsModify returns true if the event was a file modification, then it resets 53 | // the modified flag. 54 | func (ev *Event) IsModify() bool { 55 | if ev.isModify == true { 56 | ev.isModify = false 57 | return true 58 | } 59 | return false 60 | } 61 | 62 | // RemoveWatch deletes a file from the watching list. 63 | func (w *Watcher) RemoveWatch(file string) error { 64 | delete(w.Files, file) 65 | return nil 66 | } 67 | 68 | // Watch adds a file to the watching list. 69 | func (w *Watcher) Watch(file string) error { 70 | stat, err := os.Stat(file) 71 | if err != nil { 72 | return err 73 | } 74 | wf := &WatcherFile{ 75 | Filemtime: stat.ModTime(), 76 | } 77 | w.Files[file] = wf 78 | return nil 79 | } 80 | 81 | // Check compares the last known state of a file with the current state and 82 | // updates modification flags, if required. 83 | func (w *Watcher) check() { 84 | for name, f := range w.Files { 85 | stat, err := os.Stat(name) 86 | if err == nil { 87 | mtime := stat.ModTime() 88 | if mtime != f.Filemtime { 89 | ev := &Event{ 90 | Name: name, 91 | isModify: true, 92 | } 93 | f.Filemtime = mtime 94 | w.Event <- ev 95 | } 96 | } 97 | } 98 | } 99 | 100 | // Close makes a watcher sleep. 101 | func (w *Watcher) Close() { 102 | w.watching = false 103 | } 104 | 105 | // New allocates, returns a file watcher and starts the watching loop. 106 | func New() (*Watcher, error) { 107 | w := &Watcher{} 108 | w.t = time.Millisecond * 500 109 | w.Event = make(chan *Event) 110 | w.watching = true 111 | w.Files = make(map[string]*WatcherFile) 112 | 113 | go func() { 114 | for w.watching { 115 | w.check() 116 | time.Sleep(w.t) 117 | } 118 | }() 119 | 120 | return w, nil 121 | } 122 | -------------------------------------------------------------------------------- /watcher/main_test.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestWatch(t *testing.T) { 10 | w, _ := New() 11 | w.Watch("main.go") 12 | 13 | go func() { 14 | for { 15 | select { 16 | case ev := <-w.Event: 17 | if ev.IsModify() { 18 | fmt.Printf("File %s was modified.\n", ev.Name) 19 | } 20 | } 21 | } 22 | }() 23 | 24 | time.Sleep(120 * time.Second) 25 | 26 | w.Close() 27 | } 28 | --------------------------------------------------------------------------------