Compare commits
10 commits
blog-0.0.2
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c57ae37666 | ||
![]() |
25cdedfe2b | ||
![]() |
710549ca8d | ||
![]() |
1ee0db9776 | ||
![]() |
f0c5959366 | ||
![]() |
ac3493bfa8 | ||
![]() |
b6fb184b05 | ||
![]() |
62eb9903c0 | ||
![]() |
d6c33b4992 | ||
![]() |
889d1b3287 |
37 changed files with 675 additions and 386 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -36,4 +36,6 @@ build/
|
||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/m724/
|
|
||||||
|
/example-blog/
|
||||||
|
/m724/
|
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
|
@ -2,6 +2,7 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/example-blog" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/m724" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/m724" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -10,10 +10,6 @@ blog-software(config, template, content) = blog website
|
||||||
```
|
```
|
||||||
|
|
||||||
For tips on how to create your own project (workdir), see [Project format](#Project format) below.
|
For tips on how to create your own project (workdir), see [Project format](#Project format) below.
|
||||||
|
|
||||||
## Important caveats
|
|
||||||
Generated site must be the root of a subdomain, like `https://example.com/`. \
|
|
||||||
You can't put it in a directory, like `https://example.com/blog/`
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
There's no "API," but it's possible to integrate this into your Java project.
|
There's no "API," but it's possible to integrate this into your Java project.
|
||||||
|
@ -22,7 +18,7 @@ See [Main.java](/Minecon724/blog-software-java/src/branch/master/src/main/java/e
|
||||||
If you need a Maven dependency, [see here](/Minecon724/-/packages/maven/eu.m724-blog)
|
If you need a Maven dependency, [see here](/Minecon724/-/packages/maven/eu.m724-blog)
|
||||||
|
|
||||||
## Project format
|
## Project format
|
||||||
There's an ["Example workdir"](/Minecon724/blog-software-java/src/branch/master/example_workdir) which you can take inspiration from.
|
[Here's an example blog](/Minecon724/example-blog) you can take inspiration from.
|
||||||
|
|
||||||
Basically:
|
Basically:
|
||||||
- `assets/` - contains static assets
|
- `assets/` - contains static assets
|
||||||
|
@ -41,7 +37,7 @@ Basically:
|
||||||
- Custom properties, which can be anything as they are Objects
|
- Custom properties, which can be anything as they are Objects
|
||||||
|
|
||||||
## Template format
|
## Template format
|
||||||
https://pebbletemplates.io is used
|
[Pebble Templates](https://pebbletemplates.io) is used
|
||||||
|
|
||||||
- `static/` - contains static assets
|
- `static/` - contains static assets
|
||||||
- `article_template.html` - article template
|
- `article_template.html` - article template
|
||||||
|
|
1
example-blog
Submodule
1
example-blog
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 21d8a5de48b9ec4534b5a92f29be82cf2d388e47
|
2
example_workdir/.gitignore
vendored
2
example_workdir/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
# Ignore output
|
|
||||||
generated_out/
|
|
|
@ -1,5 +0,0 @@
|
||||||
This is an example working directory.
|
|
||||||
|
|
||||||
It showcases the most important stuff.
|
|
||||||
|
|
||||||
A better site could be [my blog](https://git.m724.eu/Minecon724/m724.eu)
|
|
|
@ -1,25 +0,0 @@
|
||||||
title lorem ipsum 2: the lorem ipsuming
|
|
||||||
summary watch skibidi toilet free online no virus
|
|
||||||
customText This article is cool because it has small text
|
|
||||||
|
|
||||||
<h1>THI SI ANOTEHR POSTT</h1>
|
|
||||||
|
|
||||||
<h1>Nam posuere justo turpis, eget rhoncus tellus sagittis in. Vivamus rhoncus lacus et tempor hendrerit. Nam in sagittis lectus. Etiam feugiat lacus eu placerat porttitor. Mauris eros augue, gravida ac magna in, elementum ultricies tellus. Nulla eget nibh lobortis, ultricies est ac, efficitur orci. Ut imperdiet placerat ante eget gravida. Vestibulum euismod semper odio, porttitor fermentum orci fermentum vitae. Nulla facilisi. Vivamus in sagittis lacus, nec iaculis arcu. Aenean euismod eros a enim bibendum, et ultrices erat ullamcorper. Pellentesque tellus augue, viverra eget dignissim at, consequat vel tortor. Maecenas eleifend elit et elementum fringilla. Nunc sollicitudin diam eget egestas eleifend. Praesent rutrum in ligula ac eleifend.</h1>
|
|
||||||
|
|
||||||
<h2>In rutrum enim quis semper molestie. Duis nec laoreet enim. Fusce condimentum egestas tortor a efficitur. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam pretium risus eu pharetra pretium. Nullam faucibus mi ac neque consectetur, eu pulvinar est sollicitudin. Praesent scelerisque accumsan lacus, a commodo tortor malesuada eu.</h2>
|
|
||||||
|
|
||||||
<h3>Nam lacus nisi, tincidunt non ultricies ac, dapibus nec est. Praesent non odio ante. Cras quam mauris, pulvinar et urna ac, tincidunt condimentum erat. Ut a facilisis turpis. Etiam condimentum mattis rhoncus. In mi lectus, lobortis quis leo eu, semper porta mauris. Nunc porta vehicula enim id suscipit. Quisque tincidunt rutrum volutpat. Pellentesque vitae nulla nisi. Curabitur viverra diam non finibus dapibus. Aliquam imperdiet ligula eu consequat rhoncus.</h3>
|
|
||||||
|
|
||||||
<p>Ut justo ante, tincidunt ut dapibus nec, iaculis maximus dolor. Suspendisse venenatis est a vestibulum semper. Suspendisse vulputate lacinia dictum. Aenean accumsan risus a eleifend tristique. Fusce malesuada massa ac venenatis vulputate. Ut vestibulum condimentum lacus, ornare tristique magna tincidunt ac. Aliquam molestie, ante id tempus pellentesque, urna lectus cursus quam, consequat varius eros ligula sed diam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque in placerat mi. Etiam quis accumsan elit. Cras in arcu magna. Sed euismod velit ac iaculis pretium. Morbi sed est nisi. Vivamus suscipit dui id ligula ornare, cursus efficitur magna convallis. Pellentesque id lacinia mauris.</p>
|
|
||||||
|
|
||||||
<p><small>Nullam sit amet faucibus odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sem ante, porttitor non placerat sed, venenatis at turpis. Maecenas sit amet eros purus. Aliquam congue purus eget urna ullamcorper, sed pellentesque libero faucibus. Suspendisse pharetra nisi a ligula accumsan ultrices. Nulla quam felis, posuere ut purus et, tincidunt auctor mi. Vestibulum vitae maximus justo. Maecenas vitae nulla magna. Ut at ex quis justo rutrum varius et at nibh. Vestibulum ac nunc id elit condimentum sodales nec et urna. Vivamus ac convallis tortor. Aenean ac dui quis magna ultricies commodo. Nulla facilisi. In vehicula lectus a ex dictum auctor. Praesent sit amet dui nec nulla vehicula rhoncus nec vitae lacus.</small></p>
|
|
||||||
|
|
||||||
<p class="fading">Nullam mollis metus ut enim imperdiet maximus. Quisque sed turpis id ante mattis maximus ac mattis neque. Sed sed metus tristique, aliquam lacus non, convallis eros. Sed in lorem ac leo egestas tincidunt et quis ex. Pellentesque id nisi et libero posuere rutrum. Aenean malesuada varius mauris. Vivamus in arcu leo. In vel posuere dolor, vitae ornare ante. Quisque dignissim ipsum ac lectus ornare, ut semper orci suscipit. Donec imperdiet nibh elit, ac consequat tellus rhoncus sed. Sed imperdiet elit sit amet felis mollis, ut euismod mauris placerat. Morbi bibendum rutrum ullamcorper. Aliquam facilisis libero eu sapien aliquam rhoncus.</p>
|
|
||||||
|
|
||||||
<p>Fusce id felis quis nulla iaculis maximus. <small>Quisque ut consectetur sem.</small> Nulla vel aliquam turpis, vel auctor lorem. <span class=spoiler>In fermentum, massa nec rhoncus sollicitudin,</span> lorem erat malesuada nisi, <span class=fading>quis sollicitudin quam turpis et magna.</span> Suspendisse potenti. Aenean in fermentum elit. Phasellus eget eros sollicitudin, rhoncus diam a, sus<span class="whiteout">cipit nulla. Vivamus eget egestas </span>metus. Nunc ultricies sit amet diam a mollis.</p>
|
|
||||||
|
|
||||||
<p>Nunc in diam id justo sagittis accumsan. Donec et tincidunt velit. Suspendisse luctus libero vitae ipsum pellentesque, ut congue lectus pellentesque. Aenean feugiat metus at auctor suscipit. Morbi quis elit congue, tincidunt ligula ac, posuere ante. Phasellus fermentum erat a sem convallis, eu mollis ipsum varius. Praesent porta augue quam, sit amet viverra enim suscipit sit amet. Vivamus eu augue pulvinar, finibus mauris lacinia, luctus lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris aliquam commodo gravida. Mauris quis quam id tortor maximus blandit. Nam id lorem dui.</p>
|
|
||||||
|
|
||||||
<p>Morbi orci ex, dignissim a congue accumsan, malesuada faucibus urna. Aenean porttitor tempus purus, et consectetur lorem maximus ac. Ut eget consectetur metus. Praesent sollicitudin convallis urna non finibus. Fusce magna est, accumsan eu est id, dignissim mollis ipsum. Cras id turpis eu massa sodales scelerisque ac ut enim. Fusce suscipit dui id malesuada venenatis. Phasellus eleifend erat at arcu malesuada, sed porttitor risus viverra. Duis blandit dui tortor, id tincidunt tellus iaculis at. Aenean sed purus ut dolor finibus tempus in in leo. Duis vitae elit id velit laoreet varius eu accumsan dui. Maecenas ac sollicitudin mi. Phasellus tristique justo sed est sagittis eleifend in nec ex.</p>
|
|
||||||
|
|
||||||
<p>Nulla in pretium tortor. Donec faucibus dapibus urna, vitae vestibulum velit ultricies nec. Mauris ultricies porta purus nec convallis. Aenean varius mi tortor, id mollis nisl scelerisque quis. Donec facilisis nisl vel tristique porttitor. Sed elit turpis, sodales ac neque nec, placerat imperdiet erat. Vestibulum feugiat volutpat convallis. Praesent eu nunc eu justo consectetur bibendum ac eu nisi. Donec velit sem, mattis non scelerisque non, luctus ac felis. Praesent vel orci ligula. Aenean sit amet justo nisl. Suspendisse non turpis magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla urna odio, varius sed neque nec, tincidunt tincidunt lacus.</p>
|
|
|
@ -1,23 +0,0 @@
|
||||||
title Lorem ipsum dolor sit amet
|
|
||||||
summary As an AI language model, I can't help you with that.
|
|
||||||
|
|
||||||
|
|
||||||
<h1>Nam posuere justo turpis, eget rhoncus tellus sagittis in. Vivamus rhoncus lacus et tempor hendrerit. Nam in sagittis lectus. Etiam feugiat lacus eu placerat porttitor. Mauris eros augue, gravida ac magna in, elementum ultricies tellus. Nulla eget nibh lobortis, ultricies est ac, efficitur orci. Ut imperdiet placerat ante eget gravida. Vestibulum euismod semper odio, porttitor fermentum orci fermentum vitae. Nulla facilisi. Vivamus in sagittis lacus, nec iaculis arcu. Aenean euismod eros a enim bibendum, et ultrices erat ullamcorper. Pellentesque tellus augue, viverra eget dignissim at, consequat vel tortor. Maecenas eleifend elit et elementum fringilla. Nunc sollicitudin diam eget egestas eleifend. Praesent rutrum in ligula ac eleifend.</h1>
|
|
||||||
|
|
||||||
<h2>In rutrum enim quis semper molestie. Duis nec laoreet enim. Fusce condimentum egestas tortor a efficitur. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam pretium risus eu pharetra pretium. Nullam faucibus mi ac neque consectetur, eu pulvinar est sollicitudin. Praesent scelerisque accumsan lacus, a commodo tortor malesuada eu.</h2>
|
|
||||||
|
|
||||||
<h3>Nam lacus nisi, tincidunt non ultricies ac, dapibus nec est. Praesent non odio ante. Cras quam mauris, pulvinar et urna ac, tincidunt condimentum erat. Ut a facilisis turpis. Etiam condimentum mattis rhoncus. In mi lectus, lobortis quis leo eu, semper porta mauris. Nunc porta vehicula enim id suscipit. Quisque tincidunt rutrum volutpat. Pellentesque vitae nulla nisi. Curabitur viverra diam non finibus dapibus. Aliquam imperdiet ligula eu consequat rhoncus.</h3>
|
|
||||||
|
|
||||||
<p>Ut justo ante, tincidunt ut dapibus nec, iaculis maximus dolor. Suspendisse venenatis est a vestibulum semper. Suspendisse vulputate lacinia dictum. Aenean accumsan risus a eleifend tristique. Fusce malesuada massa ac venenatis vulputate. Ut vestibulum condimentum lacus, ornare tristique magna tincidunt ac. Aliquam molestie, ante id tempus pellentesque, urna lectus cursus quam, consequat varius eros ligula sed diam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque in placerat mi. Etiam quis accumsan elit. Cras in arcu magna. Sed euismod velit ac iaculis pretium. Morbi sed est nisi. Vivamus suscipit dui id ligula ornare, cursus efficitur magna convallis. Pellentesque id lacinia mauris.</p>
|
|
||||||
|
|
||||||
<p><small>Nullam sit amet faucibus odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis sem ante, porttitor non placerat sed, venenatis at turpis. Maecenas sit amet eros purus. Aliquam congue purus eget urna ullamcorper, sed pellentesque libero faucibus. Suspendisse pharetra nisi a ligula accumsan ultrices. Nulla quam felis, posuere ut purus et, tincidunt auctor mi. Vestibulum vitae maximus justo. Maecenas vitae nulla magna. Ut at ex quis justo rutrum varius et at nibh. Vestibulum ac nunc id elit condimentum sodales nec et urna. Vivamus ac convallis tortor. Aenean ac dui quis magna ultricies commodo. Nulla facilisi. In vehicula lectus a ex dictum auctor. Praesent sit amet dui nec nulla vehicula rhoncus nec vitae lacus.</small></p>
|
|
||||||
|
|
||||||
<p class="fading">Nullam mollis metus ut enim imperdiet maximus. Quisque sed turpis id ante mattis maximus ac mattis neque. Sed sed metus tristique, aliquam lacus non, convallis eros. Sed in lorem ac leo egestas tincidunt et quis ex. Pellentesque id nisi et libero posuere rutrum. Aenean malesuada varius mauris. Vivamus in arcu leo. In vel posuere dolor, vitae ornare ante. Quisque dignissim ipsum ac lectus ornare, ut semper orci suscipit. Donec imperdiet nibh elit, ac consequat tellus rhoncus sed. Sed imperdiet elit sit amet felis mollis, ut euismod mauris placerat. Morbi bibendum rutrum ullamcorper. Aliquam facilisis libero eu sapien aliquam rhoncus.</p>
|
|
||||||
|
|
||||||
<p>Fusce id felis quis nulla iaculis maximus. <small>Quisque ut consectetur sem.</small> Nulla vel aliquam turpis, vel auctor lorem. <span class=spoiler>In fermentum, massa nec rhoncus sollicitudin,</span> lorem erat malesuada nisi, <span class=fading>quis sollicitudin quam turpis et magna.</span> Suspendisse potenti. Aenean in fermentum elit. Phasellus eget eros sollicitudin, rhoncus diam a, sus<span class="whiteout">cipit nulla. Vivamus eget egestas </span>metus. Nunc ultricies sit amet diam a mollis.</p>
|
|
||||||
|
|
||||||
<p>Nunc in diam id justo sagittis accumsan. Donec et tincidunt velit. Suspendisse luctus libero vitae ipsum pellentesque, ut congue lectus pellentesque. Aenean feugiat metus at auctor suscipit. Morbi quis elit congue, tincidunt ligula ac, posuere ante. Phasellus fermentum erat a sem convallis, eu mollis ipsum varius. Praesent porta augue quam, sit amet viverra enim suscipit sit amet. Vivamus eu augue pulvinar, finibus mauris lacinia, luctus lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris aliquam commodo gravida. Mauris quis quam id tortor maximus blandit. Nam id lorem dui.</p>
|
|
||||||
|
|
||||||
<p>Morbi orci ex, dignissim a congue accumsan, malesuada faucibus urna. Aenean porttitor tempus purus, et consectetur lorem maximus ac. Ut eget consectetur metus. Praesent sollicitudin convallis urna non finibus. Fusce magna est, accumsan eu est id, dignissim mollis ipsum. Cras id turpis eu massa sodales scelerisque ac ut enim. Fusce suscipit dui id malesuada venenatis. Phasellus eleifend erat at arcu malesuada, sed porttitor risus viverra. Duis blandit dui tortor, id tincidunt tellus iaculis at. Aenean sed purus ut dolor finibus tempus in in leo. Duis vitae elit id velit laoreet varius eu accumsan dui. Maecenas ac sollicitudin mi. Phasellus tristique justo sed est sagittis eleifend in nec ex.</p>
|
|
||||||
|
|
||||||
<p>Nulla in pretium tortor. Donec faucibus dapibus urna, vitae vestibulum velit ultricies nec. Mauris ultricies porta purus nec convallis. Aenean varius mi tortor, id mollis nisl scelerisque quis. Donec facilisis nisl vel tristique porttitor. Sed elit turpis, sodales ac neque nec, placerat imperdiet erat. Vestibulum feugiat volutpat convallis. Praesent eu nunc eu justo consectetur bibendum ac eu nisi. Donec velit sem, mattis non scelerisque non, luctus ac felis. Praesent vel orci ligula. Aenean sit amet justo nisl. Suspendisse non turpis magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla urna odio, varius sed neque nec, tincidunt tincidunt lacus.</p>
|
|
|
@ -1,2 +0,0 @@
|
||||||
this is another static asset
|
|
||||||
static assets are copied and not processed and can be any kind of file
|
|
|
@ -1 +0,0 @@
|
||||||
Hello, world!
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Render options here
|
|
||||||
|
|
||||||
# Pre-compress files to serve with web server software
|
|
||||||
compress:
|
|
||||||
- gz
|
|
||||||
- zstd
|
|
||||||
|
|
||||||
# Add .hash. to static assets provided by template
|
|
||||||
remapTemplateStatic: true
|
|
||||||
|
|
||||||
# Add .hash. to site static assets
|
|
||||||
remapAssets: false
|
|
|
@ -1,13 +0,0 @@
|
||||||
name: my blog
|
|
||||||
baseUrl: https://example.com/blog
|
|
||||||
|
|
||||||
# Whether to apply Pebble templating to posts. Disabled by default, not recommended.
|
|
||||||
# templateArticles: true
|
|
||||||
|
|
||||||
coolProperty: 1231
|
|
||||||
coolerProperty:
|
|
||||||
isMap: true
|
|
||||||
aList:
|
|
||||||
- a value
|
|
||||||
- another value
|
|
||||||
- check out site-config.yml!
|
|
|
@ -1,43 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>{{ article.title }} - {{ site.name }}</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
|
|
||||||
<meta property="og:site_name" content="{{ site.name }}" />
|
|
||||||
<meta property="og:title" content="{{ article.title }}" />
|
|
||||||
<meta property="og:description" content="{{ article.summary }}" />
|
|
||||||
<meta property="og:type" content="article" />
|
|
||||||
<meta property="og:image" content="{{ site.baseUrl }}/{{ asset(article.custom.photo) }}" />
|
|
||||||
<meta property="article:published_time" content="{{ article.createdAt.isoformat }}" />
|
|
||||||
<meta property="article:modified_time" content="{{ article.modifiedAt.isoformat }}" />
|
|
||||||
<meta property="article:author" content="{{ article.modifiedBy }}" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ static('style.css') }}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>{{ site.name }} - {{ site.custom.coolerProperty.get('isMap') }}</h1>
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<h2>
|
|
||||||
<a href="">{{ article.title }}</a>
|
|
||||||
{% if article.draft %}
|
|
||||||
<strong>DRAFT</strong>
|
|
||||||
{% endif %}
|
|
||||||
</h2>
|
|
||||||
<h4>{{ article.summary }}</h4>
|
|
||||||
<p>{{ article.createdAt | date("dd.MM.yyyy") }} by {{ article.createdBy }}</p>
|
|
||||||
|
|
||||||
<!-- article custom properties are converted to lower case -->
|
|
||||||
{% if article.custom.containsKey("customtext") %}
|
|
||||||
<small>{{ article.custom.customtext }}</small>
|
|
||||||
{% endif %}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{{ content | raw }}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,34 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>{{ site.name }}</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ static('style.css') }}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>{{ site.name }} - {{ site.custom.coolProperty }}</h1>
|
|
||||||
{% for article in articles %}
|
|
||||||
<a href="article/{{ article.slug }}.html" class="article-short">
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<p class="title">{{ article.title }}</p>
|
|
||||||
<p class="description">{{ article.summary }}</p>
|
|
||||||
</header>
|
|
||||||
</article>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for e in site.custom.coolerProperty.aList %}
|
|
||||||
<li>{{ e }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<a href="{{ asset('hello.txt') }}">This is an asset that says:</a>
|
|
||||||
<blockquote>Hello, world!</blockquote>
|
|
||||||
|
|
||||||
<a href="{{ asset('another.txt') }}">This is another asset that says things about assets.</a>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1 +0,0 @@
|
||||||
this directory houses files provided by template
|
|
|
@ -1,14 +0,0 @@
|
||||||
html {
|
|
||||||
background: #222;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
article {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
article > header {
|
|
||||||
padding: 5px 20px;
|
|
||||||
background: white;
|
|
||||||
color: black;
|
|
||||||
}
|
|
12
pom.xml
12
pom.xml
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<groupId>eu.m724</groupId>
|
<groupId>eu.m724</groupId>
|
||||||
<artifactId>blog</artifactId>
|
<artifactId>blog</artifactId>
|
||||||
<version>0.0.2</version>
|
<version>0.0.3-SNAPSHOT</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
@ -88,6 +88,13 @@
|
||||||
<artifactId>slf4j-simple</artifactId>
|
<artifactId>slf4j-simple</artifactId>
|
||||||
<version>2.0.17</version> <!-- Released Feb 25, 2025 -->
|
<version>2.0.17</version> <!-- Released Feb 25, 2025 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- https://mvnrepository.com/artifact/in.wilsonl.minifyhtml/minify-html -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>in.wilsonl.minifyhtml</groupId>
|
||||||
|
<artifactId>minify-html</artifactId>
|
||||||
|
<version>0.15.0</version> <!-- Released Dec 24, 2023 -->
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -127,6 +134,7 @@
|
||||||
<artifactSet>
|
<artifactSet>
|
||||||
<excludes>
|
<excludes>
|
||||||
<exclude>com.github.luben:zstd-jni</exclude>
|
<exclude>com.github.luben:zstd-jni</exclude>
|
||||||
|
<exclude>in.wilsonl.minifyhtml:minify-html</exclude>
|
||||||
</excludes>
|
</excludes>
|
||||||
</artifactSet>
|
</artifactSet>
|
||||||
<filters>
|
<filters>
|
||||||
|
@ -216,6 +224,6 @@
|
||||||
|
|
||||||
<scm>
|
<scm>
|
||||||
<developerConnection>scm:git:git@git.m724.eu:Minecon724/blog-software-java.git</developerConnection>
|
<developerConnection>scm:git:git@git.m724.eu:Minecon724/blog-software-java.git</developerConnection>
|
||||||
<tag>blog-0.0.2</tag>
|
<tag>HEAD</tag>
|
||||||
</scm>
|
</scm>
|
||||||
</project>
|
</project>
|
|
@ -6,17 +6,16 @@
|
||||||
|
|
||||||
package eu.m724.blog;
|
package eu.m724.blog;
|
||||||
|
|
||||||
|
import eu.m724.blog.compress.CommonsCompressor;
|
||||||
|
import eu.m724.blog.compress.CompressException;
|
||||||
import eu.m724.blog.compress.FileCompressor;
|
import eu.m724.blog.compress.FileCompressor;
|
||||||
import eu.m724.blog.compress.NoSuchAlgorithmException;
|
import eu.m724.blog.compress.NoSuchAlgorithmException;
|
||||||
import eu.m724.blog.object.Article;
|
import eu.m724.blog.object.*;
|
||||||
import eu.m724.blog.object.Feed;
|
import eu.m724.blog.server.Server;
|
||||||
import eu.m724.blog.object.RenderOptions;
|
|
||||||
import eu.m724.blog.object.Site;
|
|
||||||
import eu.m724.blog.template.TemplateRenderer;
|
import eu.m724.blog.template.TemplateRenderer;
|
||||||
import org.apache.commons.compress.compressors.CompressorException;
|
import eu.m724.blog.vc.GitVersionControl;
|
||||||
|
import eu.m724.blog.vc.VersionControl;
|
||||||
import org.apache.commons.io.file.PathUtils;
|
import org.apache.commons.io.file.PathUtils;
|
||||||
import org.eclipse.jgit.api.Git;
|
|
||||||
import org.eclipse.jgit.lib.RepositoryBuilder;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -29,18 +28,19 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@code BlogBuilder} class facilitates building a static blog by managing templates,
|
* The {@code BlogBuilder} class facilitates building a static blog by managing templates,
|
||||||
* assets, articles, and rendering output files. It uses a Git repository as the
|
* assets, articles, and rendering output files. It uses a version control (Git etc.) repository as the
|
||||||
* source for the blog's content and configuration.
|
* source for the blog's content and configuration.
|
||||||
*/
|
*/
|
||||||
public class BlogBuilder {
|
public class BlogBuilder {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(BlogBuilder.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(BlogBuilder.class);
|
||||||
|
|
||||||
private final Git git;
|
private final Site site;
|
||||||
|
private final RenderOptions renderOptions;
|
||||||
|
private final VersionControl versionControl;
|
||||||
private final Path workingDirectory;
|
private final Path workingDirectory;
|
||||||
|
|
||||||
private Site site;
|
|
||||||
private TemplateRenderer template;
|
private TemplateRenderer template;
|
||||||
private RenderOptions renderOptions;
|
private Minifier minifier;
|
||||||
|
|
||||||
private Path templateDirectory;
|
private Path templateDirectory;
|
||||||
private Path outputDirectory;
|
private Path outputDirectory;
|
||||||
|
@ -49,12 +49,14 @@ public class BlogBuilder {
|
||||||
/**
|
/**
|
||||||
* Constructs a {@link BlogBuilder} instance using the provided Git repository.
|
* Constructs a {@link BlogBuilder} instance using the provided Git repository.
|
||||||
*
|
*
|
||||||
* @param git the Git repository to be used for the blog.
|
* @param versionControl the version control repository to be used for the blog.
|
||||||
*/
|
*/
|
||||||
public BlogBuilder(Git git) {
|
private BlogBuilder(Site site, RenderOptions renderOptions, Path workingDirectory, VersionControl versionControl) {
|
||||||
this.git = git;
|
this.site = site;
|
||||||
|
this.renderOptions = renderOptions;
|
||||||
|
this.versionControl = versionControl;
|
||||||
|
|
||||||
this.workingDirectory = git.getRepository().getDirectory().toPath().getParent();
|
this.workingDirectory = workingDirectory;
|
||||||
this.templateDirectory = workingDirectory.resolve("template");
|
this.templateDirectory = workingDirectory.resolve("template");
|
||||||
this.outputDirectory = workingDirectory.resolve("generated_out");
|
this.outputDirectory = workingDirectory.resolve("generated_out");
|
||||||
}
|
}
|
||||||
|
@ -63,19 +65,19 @@ public class BlogBuilder {
|
||||||
* Creates a new {@link BlogBuilder} instance for the specified working directory.
|
* Creates a new {@link BlogBuilder} instance for the specified working directory.
|
||||||
* The directory is expected to be a Git repository.
|
* The directory is expected to be a Git repository.
|
||||||
*
|
*
|
||||||
* @param workingDirectory the root path of the blog, which must contain a Git repository.
|
* @param directory the root path of the blog, which must be a Git repository.
|
||||||
* @return a {@link BlogBuilder} instance
|
* @return a {@link BlogBuilder} instance
|
||||||
* @throws IOException if there is an error accessing the Git repository
|
* @throws IOException if there is an error accessing the Git repository
|
||||||
*/
|
*/
|
||||||
public static BlogBuilder fromPath(Path workingDirectory) throws IOException {
|
public static BlogBuilder fromGitRepository(Path directory) throws IOException {
|
||||||
var repository = new RepositoryBuilder()
|
return BlogBuilder.fromDirectory(directory, new GitVersionControl(directory));
|
||||||
.setGitDir(workingDirectory.resolve(".git").toFile())
|
}
|
||||||
.build();
|
|
||||||
var git = new Git(repository);
|
|
||||||
|
|
||||||
//
|
public static BlogBuilder fromDirectory(Path directory, VersionControl versionControl) throws IOException {
|
||||||
|
var site = Site.fromConfig(directory.resolve("site.yml"));
|
||||||
|
var renderOptions = RenderOptions.fromConfig(directory.resolve("render.yml"));
|
||||||
|
|
||||||
return new BlogBuilder(git);
|
return new BlogBuilder(site, renderOptions, directory, versionControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -129,25 +131,28 @@ public class BlogBuilder {
|
||||||
* @throws IOException if an I/O error occurs
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public void build() throws IOException {
|
public void build() throws IOException {
|
||||||
LOGGER.debug("Loading site...");
|
if (renderOptions.minify()) {
|
||||||
if (site == null)
|
try {
|
||||||
this.site = Site.fromConfig(workingDirectory.resolve("site.yml"));
|
this.minifier = new Minifier();
|
||||||
|
} catch (NoClassDefFoundError e) {
|
||||||
if (renderOptions == null)
|
LOGGER.warn("Minifier not available");
|
||||||
this.renderOptions = RenderOptions.fromConfig(workingDirectory.resolve("render.yml"));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.debug("Copying static assets...");
|
LOGGER.debug("Copying static assets...");
|
||||||
var fileHashes = copyStaticAssets();
|
var fileHashes = copyStaticAssets();
|
||||||
|
|
||||||
if (template == null)
|
if (template == null)
|
||||||
this.template = new TemplateRenderer(site, templateDirectory, fileHashes);
|
this.template = new TemplateRenderer(site, minifier, templateDirectory, fileHashes);
|
||||||
|
|
||||||
LOGGER.debug("Rendering articles...");
|
LOGGER.debug("Rendering articles...");
|
||||||
var articles = renderArticles();
|
var articles = renderArticles();
|
||||||
|
|
||||||
LOGGER.debug("Rendering meta...");
|
LOGGER.debug("Rendering meta...");
|
||||||
articles.sort(Comparator.comparing(Article::createdAt).reversed());
|
articles.sort(Comparator.comparing(Article::createdAt).reversed());
|
||||||
Files.writeString(outputDirectory.resolve("index.html"), template.renderIndex(articles));
|
|
||||||
|
LOGGER.debug("Rendering pages...");
|
||||||
|
renderIndexAndPages(articles);
|
||||||
|
|
||||||
Files.writeString(outputDirectory.resolve("articles.rss"), Feed.generateRss(site, articles));
|
Files.writeString(outputDirectory.resolve("articles.rss"), Feed.generateRss(site, articles));
|
||||||
|
|
||||||
|
@ -167,6 +172,31 @@ public class BlogBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void renderIndexAndPages(List<Article> articles) throws IOException {
|
||||||
|
int lastPage = Math.max(Math.ceilDiv(articles.size(), site.articlesPerPage()), 1);
|
||||||
|
|
||||||
|
var pageDirectory = outputDirectory.resolve("page");
|
||||||
|
Files.createDirectory(pageDirectory);
|
||||||
|
|
||||||
|
for (int page=1; page<=lastPage; page++) {
|
||||||
|
var startIndex = (page - 1) * site.articlesPerPage();
|
||||||
|
var endIndex = Math.min(startIndex + site.articlesPerPage(), articles.size());
|
||||||
|
var pageArticles = articles.subList(startIndex, endIndex);
|
||||||
|
|
||||||
|
var pageNumbers = PageNumbers.create(page, lastPage);
|
||||||
|
|
||||||
|
if (page == 1) {
|
||||||
|
var renderedIndex = template.renderIndex(pageNumbers, pageArticles);
|
||||||
|
Files.writeString(outputDirectory.resolve("index.html"), renderedIndex);
|
||||||
|
|
||||||
|
if (!site.separateFirstPage()) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var renderedPage = template.renderPage(pageNumbers, pageArticles);
|
||||||
|
Files.writeString(pageDirectory.resolve(page + ".html"), renderedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<Article> renderArticles() throws IOException {
|
private List<Article> renderArticles() throws IOException {
|
||||||
Files.createDirectory(outputDirectory.resolve("article"));
|
Files.createDirectory(outputDirectory.resolve("article"));
|
||||||
var articleDirectory = workingDirectory.resolve("articles");
|
var articleDirectory = workingDirectory.resolve("articles");
|
||||||
|
@ -184,8 +214,7 @@ public class BlogBuilder {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
path = articleDirectory.relativize(path);
|
var article = Article.fromFile(versionControl, path);
|
||||||
var article = Article.fromFile(git, path);
|
|
||||||
|
|
||||||
if (article.draft() && !renderDrafts) {
|
if (article.draft() && !renderDrafts) {
|
||||||
LOGGER.info("[Article {}] Draft. Ignoring, because you didn't specify to render drafts.", path.getFileName());
|
LOGGER.info("[Article {}] Draft. Ignoring, because you didn't specify to render drafts.", path.getFileName());
|
||||||
|
@ -193,14 +222,17 @@ public class BlogBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
var render = template.renderArticle(article);
|
var render = template.renderArticle(article);
|
||||||
var outFile = outputDirectory.resolve("article").resolve(path);
|
var outFile = outputDirectory.resolve("article").resolve(path.getFileName());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectory(outFile.getParent());
|
Files.createDirectory(outFile.getParent());
|
||||||
} catch (FileAlreadyExistsException ignored) { }
|
} catch (FileAlreadyExistsException ignored) { }
|
||||||
|
|
||||||
Files.writeString(outFile, render);
|
Files.writeString(outFile, render);
|
||||||
articles.add(article);
|
|
||||||
|
// TODO Is this the right way? To just ignore hidden? Right now it doesn't matter, but in the future?
|
||||||
|
if (!article.hidden())
|
||||||
|
articles.add(article);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,24 +242,38 @@ public class BlogBuilder {
|
||||||
private Map<String, String> copyStaticAssets() throws IOException {
|
private Map<String, String> copyStaticAssets() throws IOException {
|
||||||
var fileHashes = new HashMap<String, String>();
|
var fileHashes = new HashMap<String, String>();
|
||||||
|
|
||||||
if (renderOptions.remapAssets()) {
|
var userAssetsDir = workingDirectory.resolve("assets");
|
||||||
var assetHashes = CacheBuster.copyTree(workingDirectory.resolve("assets"), outputDirectory.resolve("assets"));
|
var templateStaticDir = templateDirectory.resolve("static");
|
||||||
|
|
||||||
|
var outputUserAssetsDir = outputDirectory.resolve("assets");
|
||||||
|
var outputTemplateStaticDir = outputDirectory.resolve("static");
|
||||||
|
|
||||||
|
|
||||||
|
if (renderOptions.remapAssets()) {
|
||||||
|
var assetHashes = CacheBuster.copyTree(userAssetsDir, outputUserAssetsDir);
|
||||||
|
|
||||||
|
// This looks weird, but it's necessary if we want to use a single map
|
||||||
assetHashes.forEach((k, v) -> {
|
assetHashes.forEach((k, v) -> {
|
||||||
fileHashes.put("assets/" + k, v); // TODO this seems like a hack
|
fileHashes.put("assets/" + k, v);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
copyTree(workingDirectory.resolve("assets"), outputDirectory.resolve("assets"));
|
FileUtils.copyTree(userAssetsDir, outputUserAssetsDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renderOptions.remapTemplateStatic()) {
|
if (renderOptions.remapTemplateStatic()) {
|
||||||
var templateStaticHashes = CacheBuster.copyTree(templateDirectory.resolve("static"), outputDirectory.resolve("static"));
|
var templateStaticHashes = CacheBuster.copyTree(templateStaticDir, outputTemplateStaticDir);
|
||||||
|
|
||||||
|
// This looks weird, but it's necessary if we want to use a single map
|
||||||
templateStaticHashes.forEach((k, v) -> {
|
templateStaticHashes.forEach((k, v) -> {
|
||||||
fileHashes.put("static/" + k, v); // TODO this seems like a hack
|
fileHashes.put("static/" + k, v);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
copyTree(templateDirectory.resolve("static"), outputDirectory.resolve("static"));
|
FileUtils.copyTree(templateStaticDir, outputTemplateStaticDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (minifier != null) {
|
||||||
|
minifier.minifyTree(outputTemplateStaticDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileHashes;
|
return fileHashes;
|
||||||
|
@ -238,7 +284,7 @@ public class BlogBuilder {
|
||||||
|
|
||||||
for (var algorithm : renderOptions.compress()) {
|
for (var algorithm : renderOptions.compress()) {
|
||||||
try {
|
try {
|
||||||
var compressor = new FileCompressor(algorithm);
|
var compressor = new CommonsCompressor(algorithm);
|
||||||
compressors.add(compressor);
|
compressors.add(compressor);
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
LOGGER.error("No such compression algorithm, ignoring: {}", e.getAlgorithm());
|
LOGGER.error("No such compression algorithm, ignoring: {}", e.getAlgorithm());
|
||||||
|
@ -255,30 +301,8 @@ public class BlogBuilder {
|
||||||
for (var path : tree) {
|
for (var path : tree) {
|
||||||
try {
|
try {
|
||||||
compressor.compress(path);
|
compressor.compress(path);
|
||||||
} catch (CompressorException e) {
|
} catch (CompressException e) {
|
||||||
LOGGER.error("Error compressing \"{}\" to \"{}\": {}", path, compressor.getAlgorithm(), e.getMessage());
|
LOGGER.error("Error compressing \"{}\" to \"{}\": {}", path, compressor.getAlgorithmName(), e.getMessage());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Internal functions */
|
|
||||||
|
|
||||||
private void copyTree(Path srcDir, Path destDir) throws IOException {
|
|
||||||
try (var walk = Files.walk(srcDir)) {
|
|
||||||
for (var src : walk.collect(Collectors.toSet())) {
|
|
||||||
var rel = srcDir.relativize(src);
|
|
||||||
var dest = destDir.resolve(rel);
|
|
||||||
|
|
||||||
if (Files.isRegularFile(src)) {
|
|
||||||
var parent = dest.getParent();
|
|
||||||
|
|
||||||
if (!Files.isDirectory(parent)) {
|
|
||||||
Files.createDirectories(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
Files.copy(src, dest);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.zip.CRC32C;
|
import java.util.zip.CRC32C;
|
||||||
|
|
||||||
public class CacheBuster {
|
public class CacheBuster {
|
||||||
|
@ -19,27 +18,17 @@ public class CacheBuster {
|
||||||
public static Map<String, String> copyTree(Path srcDir, Path destDir) throws IOException {
|
public static Map<String, String> copyTree(Path srcDir, Path destDir) throws IOException {
|
||||||
var map = new HashMap<String, String>();
|
var map = new HashMap<String, String>();
|
||||||
|
|
||||||
try (var walk = Files.walk(srcDir)) {
|
for (var e : FileUtils.srcDestMap(srcDir, destDir).entrySet()) {
|
||||||
for (var src : walk.collect(Collectors.toSet())) {
|
var sourceFile = e.getKey();
|
||||||
var rel = srcDir.relativize(src);
|
var destinationFile = e.getValue();
|
||||||
var dest = destDir.resolve(rel);
|
|
||||||
|
|
||||||
if (Files.isRegularFile(src)) {
|
var hash = hashFile(sourceFile);
|
||||||
var parent = dest.getParent();
|
|
||||||
|
|
||||||
if (!Files.isDirectory(parent)) {
|
var filename = insertHashInPath(destinationFile.getFileName().toString(), hash);
|
||||||
Files.createDirectories(parent);
|
destinationFile = destinationFile.resolveSibling(filename);
|
||||||
}
|
|
||||||
|
|
||||||
var hash = hashFile(src);
|
Files.copy(sourceFile, destinationFile);
|
||||||
|
map.put(srcDir.relativize(sourceFile).toString(), hash);
|
||||||
var filename = insertHashInPath(dest.getFileName().toString(), hash);
|
|
||||||
dest = dest.resolveSibling(filename);
|
|
||||||
|
|
||||||
Files.copy(src, dest);
|
|
||||||
map.put(rel.toString(), hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
|
|
71
src/main/java/eu/m724/blog/FileUtils.java
Normal file
71
src/main/java/eu/m724/blog/FileUtils.java
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class FileUtils {
|
||||||
|
public static void copyTree(Path srcDir, Path destDir) throws IOException {
|
||||||
|
for (var e : srcDestMap(srcDir, destDir).entrySet()) {
|
||||||
|
Files.copy(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<Path, Path> srcDestMap(Path srcDir, Path destDir) throws IOException {
|
||||||
|
var map = new HashMap<Path, Path>();
|
||||||
|
|
||||||
|
for (var sourceFile : walkFilesList(srcDir)) {
|
||||||
|
var rel = srcDir.relativize(sourceFile);
|
||||||
|
var destinationFile = destDir.resolve(rel);
|
||||||
|
|
||||||
|
var parent = destinationFile.getParent();
|
||||||
|
|
||||||
|
if (!Files.isDirectory(parent)) {
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.put(sourceFile, destinationFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks over a directory and only returns files.<br>
|
||||||
|
* <em>IMPORTANT</em> Don't forget to close the stream.<br>
|
||||||
|
*
|
||||||
|
* @param start the starting file
|
||||||
|
* @return the {@link Stream} of {@link Path}
|
||||||
|
* @throws IOException if an I/O error is thrown when accessing the starting file.
|
||||||
|
*
|
||||||
|
* @see FileUtils#walkFilesList(Path)
|
||||||
|
*/
|
||||||
|
public static Stream<Path> walkFilesStream(Path start) throws IOException {
|
||||||
|
return Files.walk(start).filter(Files::isRegularFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks over a directory and only returns files.<br>
|
||||||
|
*
|
||||||
|
* @param start the starting file
|
||||||
|
* @return the {@link List} of {@link Path}
|
||||||
|
* @throws IOException if an I/O error is thrown when accessing the starting file.
|
||||||
|
*
|
||||||
|
* @see FileUtils#walkFilesStream(Path)
|
||||||
|
*/
|
||||||
|
public static List<Path> walkFilesList(Path start) throws IOException {
|
||||||
|
try (var stream = walkFilesStream(start)) {
|
||||||
|
return stream.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,7 +38,10 @@ public class Main {
|
||||||
|
|
||||||
var start = System.nanoTime();
|
var start = System.nanoTime();
|
||||||
|
|
||||||
var builder = BlogBuilder.fromPath(workingDirectory)
|
|
||||||
|
LOGGER.debug("Loading site...");
|
||||||
|
|
||||||
|
var builder = BlogBuilder.fromGitRepository(workingDirectory)
|
||||||
.templateDirectory(templateDirectory)
|
.templateDirectory(templateDirectory)
|
||||||
.outputDirectory(outputDirectory)
|
.outputDirectory(outputDirectory)
|
||||||
.renderDrafts(renderDrafts);
|
.renderDrafts(renderDrafts);
|
||||||
|
|
65
src/main/java/eu/m724/blog/Minifier.java
Normal file
65
src/main/java/eu/m724/blog/Minifier.java
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog;
|
||||||
|
|
||||||
|
import in.wilsonl.minifyhtml.Configuration;
|
||||||
|
import in.wilsonl.minifyhtml.MinifyHtml;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class Minifier {
|
||||||
|
private static final Set<String> CONTENT_TYPES_TO_MINIFY = Set.of(
|
||||||
|
"text/html",
|
||||||
|
"text/css",
|
||||||
|
"text/javascript"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final Configuration configuration = new Configuration.Builder()
|
||||||
|
.setMinifyJs(true)
|
||||||
|
.setMinifyCss(true)
|
||||||
|
.setKeepHtmlAndHeadOpeningTags(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public String minify(String text) {
|
||||||
|
return MinifyHtml.minify(text, configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void minifyTree(Path dir) throws IOException {
|
||||||
|
for (var file : FileUtils.walkFilesList(dir)) {
|
||||||
|
var contentType = Files.probeContentType(file);
|
||||||
|
var doMinify = CONTENT_TYPES_TO_MINIFY.contains(contentType);
|
||||||
|
|
||||||
|
if (doMinify) {
|
||||||
|
var content = Files.readString(file);
|
||||||
|
var minified = minify(content);
|
||||||
|
Files.writeString(file, minified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyAndMinifyTree(Path srcDir, Path destDir) throws IOException {
|
||||||
|
for (var e : FileUtils.srcDestMap(srcDir, destDir).entrySet()) {
|
||||||
|
var sourceFile = e.getKey();
|
||||||
|
var destinationFile = e.getValue();
|
||||||
|
|
||||||
|
var contentType = Files.probeContentType(sourceFile);
|
||||||
|
var doMinify = CONTENT_TYPES_TO_MINIFY.contains(contentType);
|
||||||
|
|
||||||
|
if (doMinify) {
|
||||||
|
var content = Files.readString(sourceFile);
|
||||||
|
var minified = minify(content);
|
||||||
|
Files.writeString(destinationFile, minified);
|
||||||
|
} else {
|
||||||
|
Files.copy(sourceFile, destDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
src/main/java/eu/m724/blog/YamlLoader.java
Normal file
25
src/main/java/eu/m724/blog/YamlLoader.java
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog;
|
||||||
|
|
||||||
|
import org.snakeyaml.engine.v2.api.Load;
|
||||||
|
import org.snakeyaml.engine.v2.api.LoadSettings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class YamlLoader {
|
||||||
|
private static final Load LOAD = new Load(LoadSettings.builder().build());
|
||||||
|
|
||||||
|
public static Map<String, Object> loadMap(Path file) throws IOException {
|
||||||
|
try (var inputStream = Files.newInputStream(file)) {
|
||||||
|
return (Map<String, Object>) LOAD.loadFromInputStream(inputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
src/main/java/eu/m724/blog/compress/CommonsCompressor.java
Normal file
58
src/main/java/eu/m724/blog/compress/CommonsCompressor.java
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.compress;
|
||||||
|
|
||||||
|
import org.apache.commons.compress.compressors.CompressorException;
|
||||||
|
import org.apache.commons.compress.compressors.CompressorStreamFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class CommonsCompressor extends FileCompressor {
|
||||||
|
/**
|
||||||
|
* Constructs a {@link FileCompressor} instance using the provided algorithm.
|
||||||
|
*
|
||||||
|
* @param algorithm The algorithm, for valid names see {@link CompressorStreamFactory}
|
||||||
|
* @throws NoSuchAlgorithmException If that algorithm is unavailable or the name is invalid.
|
||||||
|
*/
|
||||||
|
public CommonsCompressor(String algorithm) throws NoSuchAlgorithmException {
|
||||||
|
super(algorithm);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var os = new CompressorStreamFactory().createCompressorOutputStream(algorithm, OutputStream.nullOutputStream());
|
||||||
|
os.close();
|
||||||
|
} catch (NoClassDefFoundError | CompressorException e) {
|
||||||
|
throw new NoSuchAlgorithmException(algorithm, e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Unexpected IOException closing test output stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void compress(Path file) throws IOException, CompressException {
|
||||||
|
var destination = file.resolveSibling(file.getFileName() + "." + getAlgorithmName());
|
||||||
|
compress(file, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void compress(Path source, Path destination) throws IOException, CompressException {
|
||||||
|
if (Files.exists(destination))
|
||||||
|
throw new FileAlreadyExistsException(destination.toString());
|
||||||
|
|
||||||
|
try (
|
||||||
|
var outputStream = new CompressorStreamFactory()
|
||||||
|
.createCompressorOutputStream(getAlgorithmName(), Files.newOutputStream(destination))
|
||||||
|
) {
|
||||||
|
Files.copy(source, outputStream);
|
||||||
|
} catch (CompressorException e) {
|
||||||
|
throw new CompressException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/main/java/eu/m724/blog/compress/CompressException.java
Normal file
14
src/main/java/eu/m724/blog/compress/CompressException.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.compress;
|
||||||
|
|
||||||
|
// TODO really runtime exception?
|
||||||
|
public class CompressException extends RuntimeException {
|
||||||
|
public CompressException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,53 +6,20 @@
|
||||||
|
|
||||||
package eu.m724.blog.compress;
|
package eu.m724.blog.compress;
|
||||||
|
|
||||||
import org.apache.commons.compress.compressors.CompressorException;
|
|
||||||
import org.apache.commons.compress.compressors.CompressorStreamFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
|
|
||||||
public class FileCompressor {
|
public abstract class FileCompressor {
|
||||||
private final String algorithm;
|
private final String algorithmName;
|
||||||
|
|
||||||
/**
|
protected FileCompressor(String algorithmName) {
|
||||||
* Constructs a {@link FileCompressor} instance using the provided algorithm.
|
this.algorithmName = algorithmName;
|
||||||
*
|
|
||||||
* @param algorithm The algorithm, for valid names see {@link CompressorStreamFactory}
|
|
||||||
* @throws NoSuchAlgorithmException If that algorithm is unavailable or the name is invalid.
|
|
||||||
*/
|
|
||||||
public FileCompressor(String algorithm) throws NoSuchAlgorithmException {
|
|
||||||
this.algorithm = algorithm;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var os = new CompressorStreamFactory().createCompressorOutputStream(algorithm, OutputStream.nullOutputStream());
|
|
||||||
os.close();
|
|
||||||
} catch (NoClassDefFoundError | CompressorException e) {
|
|
||||||
throw new NoSuchAlgorithmException(algorithm, e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Unexpected IOException closing test output stream", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void compress(Path source) throws IOException, CompressorException {
|
public String getAlgorithmName() {
|
||||||
var destination = source.resolveSibling(source.getFileName() + "." + algorithm);
|
return algorithmName;
|
||||||
compress(source, destination);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void compress(Path source, Path destination) throws IOException, CompressorException {
|
abstract public void compress(Path file) throws IOException, CompressException;
|
||||||
if (Files.exists(destination))
|
abstract public void compress(Path source, Path destination) throws IOException, CompressException;
|
||||||
throw new FileAlreadyExistsException(destination.toString());
|
|
||||||
|
|
||||||
try (
|
|
||||||
var outputStream = new CompressorStreamFactory()
|
|
||||||
.createCompressorOutputStream(algorithm, Files.newOutputStream(destination))
|
|
||||||
) {
|
|
||||||
Files.copy(source, outputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAlgorithm() {
|
|
||||||
return algorithm;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
package eu.m724.blog.object;
|
package eu.m724.blog.object;
|
||||||
|
|
||||||
import org.eclipse.jgit.api.Git;
|
import eu.m724.blog.vc.VersionControl;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import eu.m724.blog.vc.VersionControlException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import java.util.Map;
|
||||||
* @param createdAt The timestamp of when the article was first created.
|
* @param createdAt The timestamp of when the article was first created.
|
||||||
* @param modifiedBy The name of the author who last modified the article.
|
* @param modifiedBy The name of the author who last modified the article.
|
||||||
* @param modifiedAt The timestamp of the last modification to the article.
|
* @param modifiedAt The timestamp of the last modification to the article.
|
||||||
* @param custom A map of custom properties or metadata associated with the article.
|
* @param custom Map of raw properties, including those that aren't represented with a property
|
||||||
* @param rawContent The raw content of the article, which <em>currently</em> is usually HTML.
|
* @param rawContent The raw content of the article, which <em>currently</em> is usually HTML.
|
||||||
*/
|
*/
|
||||||
public record Article(
|
public record Article(
|
||||||
|
@ -38,6 +38,7 @@ public record Article(
|
||||||
String title,
|
String title,
|
||||||
String summary,
|
String summary,
|
||||||
boolean draft,
|
boolean draft,
|
||||||
|
boolean hidden,
|
||||||
|
|
||||||
int revisions,
|
int revisions,
|
||||||
String createdBy,
|
String createdBy,
|
||||||
|
@ -56,17 +57,16 @@ public record Article(
|
||||||
* The method extracts metadata properties, content, and versioning information
|
* The method extracts metadata properties, content, and versioning information
|
||||||
* based on the Git history of the file.
|
* based on the Git history of the file.
|
||||||
*
|
*
|
||||||
* @param git the Git repository used to retrieve versioning and commit information
|
* @param versionControl the version control repository used to retrieve versioning and commit information
|
||||||
* @param path the relative path to the file within the "articles" directory
|
* @param path the relative path to the file within the "articles" directory
|
||||||
* @return a {@link Article} object populated with data extracted from the specified file
|
* @return a {@link Article} object populated with data extracted from the specified file
|
||||||
* @throws IOException if an error occurs during file reading
|
* @throws IOException if an error occurs during file reading
|
||||||
*/
|
*/
|
||||||
public static Article fromFile(Git git, Path path) throws IOException {
|
public static Article fromFile(VersionControl versionControl, Path path) throws IOException {
|
||||||
/* read properties before filtering */
|
/* read properties before filtering */
|
||||||
|
|
||||||
var slug = path.getFileName().toString().split("\\.")[0];
|
var slug = path.getFileName().toString().split("\\.")[0];
|
||||||
path = Path.of("articles").resolve(path);
|
var lines = Files.readAllLines(path);
|
||||||
var lines = Files.readAllLines(git.getRepository().getDirectory().toPath().getParent().resolve(path));
|
|
||||||
|
|
||||||
var properties = new HashMap<String, String>();
|
var properties = new HashMap<String, String>();
|
||||||
|
|
||||||
|
@ -86,31 +86,12 @@ public record Article(
|
||||||
|
|
||||||
var content = String.join("\n", lines).strip();
|
var content = String.join("\n", lines).strip();
|
||||||
|
|
||||||
/* filter properties from read file */
|
/* read properties */
|
||||||
|
|
||||||
String title = "NO TITLE SET";
|
String title = properties.getOrDefault("title", "NO TITLE SET");
|
||||||
String summary = "NO SUMMARY SET";
|
String summary = properties.getOrDefault("summary", "NO SUMMARY SET"); // TODO maybe it's not always needed?
|
||||||
boolean draft = true;
|
boolean draft = !properties.containsKey("live");
|
||||||
|
boolean hidden = properties.containsKey("hidden");
|
||||||
var custom = new HashMap<String, String>();
|
|
||||||
|
|
||||||
for (Map.Entry<String, String> property : properties.entrySet()) {
|
|
||||||
var value = property.getValue();
|
|
||||||
|
|
||||||
switch (property.getKey()) {
|
|
||||||
case "title":
|
|
||||||
title = value;
|
|
||||||
break;
|
|
||||||
case "summary":
|
|
||||||
summary = value;
|
|
||||||
break;
|
|
||||||
case "live": // an article is live (not draft) if the key is there
|
|
||||||
draft = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
custom.put(property.getKey(), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* get revisions */
|
/* get revisions */
|
||||||
|
|
||||||
|
@ -121,20 +102,20 @@ public record Article(
|
||||||
ZonedDateTime modifiedAt = Instant.ofEpochMilli(0).atZone(ZoneOffset.UTC);
|
ZonedDateTime modifiedAt = Instant.ofEpochMilli(0).atZone(ZoneOffset.UTC);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (var commit : git.log().addPath(path.toString()).call()) {
|
for (var change : versionControl.getChanges(path)) {
|
||||||
createdBy = commit.getAuthorIdent().getName();
|
createdBy = change.author();
|
||||||
createdAt = Instant.ofEpochSecond(commit.getCommitTime()).atZone(ZoneOffset.UTC);
|
createdAt = change.time();
|
||||||
|
|
||||||
if (revisions++ == 0) {
|
if (revisions++ == 0) {
|
||||||
modifiedBy = createdBy;
|
modifiedBy = createdBy;
|
||||||
modifiedAt = createdAt;
|
modifiedAt = createdAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (GitAPIException e) {
|
} catch (VersionControlException e) {
|
||||||
draft = true;
|
draft = true;
|
||||||
LOGGER.warn("[Article {}] Draft because of a Git exception: {}", slug, e.getMessage());
|
LOGGER.warn("[Article {}] Draft because of a VC exception: {}", slug, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Article(slug, title, summary, draft, revisions, createdBy, createdAt, modifiedBy, modifiedAt, custom, content);
|
return new Article(slug, title, summary, draft, hidden, revisions, createdBy, createdAt, modifiedBy, modifiedAt, properties, content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
38
src/main/java/eu/m724/blog/object/PageNumbers.java
Normal file
38
src/main/java/eu/m724/blog/object/PageNumbers.java
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PageNumbers simplifies tracking page numbers
|
||||||
|
* @param current current page number (1 is first)
|
||||||
|
* @param last last page number (equals number of pages)
|
||||||
|
* @param previous previous page number (>=1)
|
||||||
|
* @param next next page number (<=last)
|
||||||
|
* @param isFirstPage is current page the first page (=1)
|
||||||
|
* @param isLastPage is current page the last page (=last)
|
||||||
|
*/
|
||||||
|
public record PageNumbers(
|
||||||
|
int current,
|
||||||
|
int last,
|
||||||
|
|
||||||
|
int previous,
|
||||||
|
int next,
|
||||||
|
|
||||||
|
boolean isFirstPage,
|
||||||
|
boolean isLastPage
|
||||||
|
) {
|
||||||
|
// after upgrade to java 22+ this could be a constructor
|
||||||
|
public static PageNumbers create(int current, int last) {
|
||||||
|
var previous = Math.max(current - 1, 1);
|
||||||
|
var next = Math.min(current + 1, last);
|
||||||
|
|
||||||
|
var isFirstPage = current == 1;
|
||||||
|
var isLastPage = current == last;
|
||||||
|
|
||||||
|
return new PageNumbers(current, last, previous, next, isFirstPage, isLastPage);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,41 +6,61 @@
|
||||||
|
|
||||||
package eu.m724.blog.object;
|
package eu.m724.blog.object;
|
||||||
|
|
||||||
import org.snakeyaml.engine.v2.api.Load;
|
import eu.m724.blog.YamlLoader;
|
||||||
import org.snakeyaml.engine.v2.api.LoadSettings;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options that are relevant to rendering, not about the {@link Site}.
|
||||||
|
*
|
||||||
|
* @param compress list of compression algorithms to compress output
|
||||||
|
* @param remapAssets whether to remap user assets, add a unique hash to the file name to bypass cache
|
||||||
|
* @param remapTemplateStatic whether to remap template static assets, add a unique hash to the file name to bypass cache
|
||||||
|
* @param minify whether to minify output
|
||||||
|
*/
|
||||||
public record RenderOptions(
|
public record RenderOptions(
|
||||||
List<String> compress, // TODO rename?
|
List<String> compress, // TODO rename?
|
||||||
|
|
||||||
|
boolean remapAssets,
|
||||||
boolean remapTemplateStatic,
|
boolean remapTemplateStatic,
|
||||||
boolean remapAssets
|
|
||||||
|
boolean minify
|
||||||
) {
|
) {
|
||||||
|
private static final RenderOptions DEFAULT = new RenderOptions(
|
||||||
|
List.of("gz", "zstd"),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link Site} object by reading and parsing the configuration file at the specified path.<br>
|
* Creates a {@link Site} object by reading and parsing the configuration file at the specified file.<br>
|
||||||
* The configuration file must be a JSON file.
|
* The configuration file must be a JSON file.
|
||||||
*
|
*
|
||||||
* @param path the path to the configuration file
|
* @param file the file to the configuration file
|
||||||
* @return a {@link Site} object initialized with the data from the configuration file
|
* @return a {@link Site} object initialized with the data from the configuration file
|
||||||
* @throws IOException if an error occurs during file reading
|
* @throws IOException if an error occurs during file reading
|
||||||
*/
|
*/
|
||||||
public static RenderOptions fromConfig(Path path) throws IOException {
|
public static RenderOptions fromConfig(Path file) throws IOException {
|
||||||
var load = new Load(LoadSettings.builder().build());
|
var yaml = YamlLoader.loadMap(file);
|
||||||
var yaml = (Map<String, Object>) load.loadFromInputStream(Files.newInputStream(path));
|
|
||||||
|
/* ---- */
|
||||||
|
|
||||||
|
List<String> compress = (List<String>) yaml.getOrDefault("compress", DEFAULT.compress());
|
||||||
|
|
||||||
List<String> compress = (List<String>) yaml.getOrDefault("compress", new ArrayList<>());
|
|
||||||
boolean remapTemplateStatic = (boolean) yaml.getOrDefault("remapTemplateStatic", true);
|
|
||||||
// assets are not remapped by default, because they might be hotlinked
|
// assets are not remapped by default, because they might be hotlinked
|
||||||
boolean remapAssets = (boolean) yaml.getOrDefault("remapAssets", false);
|
boolean remapAssets = (boolean) yaml.getOrDefault("remapAssets", DEFAULT.remapAssets());
|
||||||
|
|
||||||
|
boolean remapTemplateStatic = (boolean) yaml.getOrDefault("remapTemplateStatic", DEFAULT.remapTemplateStatic());
|
||||||
|
|
||||||
|
var minify = (boolean) yaml.getOrDefault("minify", DEFAULT.minify());
|
||||||
|
|
||||||
|
/* ---- */
|
||||||
|
|
||||||
return new RenderOptions(
|
return new RenderOptions(
|
||||||
compress, remapTemplateStatic, remapAssets
|
compress, remapTemplateStatic, remapAssets, minify
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,9 @@
|
||||||
|
|
||||||
package eu.m724.blog.object;
|
package eu.m724.blog.object;
|
||||||
|
|
||||||
import org.snakeyaml.engine.v2.api.Load;
|
import eu.m724.blog.YamlLoader;
|
||||||
import org.snakeyaml.engine.v2.api.LoadSettings;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -29,27 +27,41 @@ public record Site(
|
||||||
|
|
||||||
String directory,
|
String directory,
|
||||||
|
|
||||||
|
int articlesPerPage,
|
||||||
|
|
||||||
boolean templateArticles,
|
boolean templateArticles,
|
||||||
|
boolean separateFirstPage,
|
||||||
|
|
||||||
Map<String, Object> custom
|
Map<String, Object> custom
|
||||||
) {
|
) {
|
||||||
|
private static final Site DEFAULT = new Site(
|
||||||
|
"Misconfigured blog",
|
||||||
|
"/",
|
||||||
|
"/",
|
||||||
|
10,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
Map.of()
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link Site} object by reading and parsing the configuration file at the specified path.<br>
|
* Creates a {@link Site} object by reading and parsing the configuration file at the specified path.<br>
|
||||||
* The configuration file must be a JSON file.
|
* The configuration file must be a JSON file.
|
||||||
*
|
*
|
||||||
* @param path the path to the configuration file
|
* @param file the path to the configuration file
|
||||||
* @return a {@link Site} object initialized with the data from the configuration file
|
* @return a {@link Site} object initialized with the data from the configuration file
|
||||||
* @throws IOException if an error occurs during file reading
|
* @throws IOException if an error occurs during file reading
|
||||||
*/
|
*/
|
||||||
public static Site fromConfig(Path path) throws IOException {
|
public static Site fromConfig(Path file) throws IOException {
|
||||||
var load = new Load(LoadSettings.builder().build());
|
var yaml = YamlLoader.loadMap(file);
|
||||||
var yaml = (Map<String, Object>) load.loadFromInputStream(Files.newInputStream(path));
|
|
||||||
|
|
||||||
String name = (String) yaml.get("name");
|
String name = (String) yaml.getOrDefault("name", DEFAULT.name());
|
||||||
String baseUrl = (String) yaml.getOrDefault("baseUrl", "/");
|
String baseUrl = (String) yaml.getOrDefault("baseUrl", DEFAULT.baseUrl());
|
||||||
var templateArticles = (boolean) yaml.getOrDefault("templateArticles", false);
|
var articlesPerPage = (int) yaml.getOrDefault("articlesPerPage", DEFAULT.articlesPerPage());
|
||||||
|
var templateArticles = (boolean) yaml.getOrDefault("templateArticles", DEFAULT.templateArticles());
|
||||||
|
var separateFirstPage = (boolean) yaml.getOrDefault("separateFirstPage", DEFAULT.separateFirstPage());
|
||||||
|
|
||||||
String directory = "/";
|
String directory = DEFAULT.directory();
|
||||||
if (baseUrl != null) {
|
if (baseUrl != null) {
|
||||||
var temp = baseUrl.substring(baseUrl.indexOf(':') + 3);
|
var temp = baseUrl.substring(baseUrl.indexOf(':') + 3);
|
||||||
var slashIndex = temp.indexOf('/');
|
var slashIndex = temp.indexOf('/');
|
||||||
|
@ -59,7 +71,7 @@ public record Site(
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Site(
|
return new Site(
|
||||||
name, baseUrl, directory, templateArticles, yaml
|
name, baseUrl, directory, articlesPerPage, templateArticles, separateFirstPage, yaml
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* in the project root for the full license text.
|
* in the project root for the full license text.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package eu.m724.blog;
|
package eu.m724.blog.server;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import com.sun.net.httpserver.SimpleFileServer;
|
import com.sun.net.httpserver.SimpleFileServer;
|
|
@ -63,8 +63,50 @@ public class TemplateExtension extends AbstractExtension {
|
||||||
|
|
||||||
return site.directory() + path;
|
return site.directory() + path;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"url_for", new Function() {
|
||||||
|
@Override
|
||||||
|
public List<String> getArgumentNames() {
|
||||||
|
return List.of("path");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
|
||||||
|
var path = args.get("path").toString();
|
||||||
|
|
||||||
|
if (path.startsWith("/")) {
|
||||||
|
path = path.substring(1);
|
||||||
|
} else {
|
||||||
|
// TODO Should we apply special treatment? Relative path?
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.directory() + path;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url_to_page", new Function() {
|
||||||
|
@Override
|
||||||
|
public List<String> getArgumentNames() {
|
||||||
|
return List.of("page");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
|
||||||
|
var pageObj = args.get("page");
|
||||||
|
int page;
|
||||||
|
|
||||||
|
if (pageObj instanceof Number pageNumber) {
|
||||||
|
page = pageNumber.intValue();
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("\"page\" must be Number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page == 1 && !site.separateFirstPage()) {
|
||||||
|
return site.directory();
|
||||||
|
} else {
|
||||||
|
return site.directory() + "page/" + page + ".html";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO make url_for that supports relative and absolute paths
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,16 @@
|
||||||
|
|
||||||
package eu.m724.blog.template;
|
package eu.m724.blog.template;
|
||||||
|
|
||||||
|
import eu.m724.blog.Minifier;
|
||||||
import eu.m724.blog.object.Article;
|
import eu.m724.blog.object.Article;
|
||||||
|
import eu.m724.blog.object.PageNumbers;
|
||||||
import eu.m724.blog.object.Site;
|
import eu.m724.blog.object.Site;
|
||||||
import io.pebbletemplates.pebble.PebbleEngine;
|
import io.pebbletemplates.pebble.PebbleEngine;
|
||||||
|
import io.pebbletemplates.pebble.error.LoaderException;
|
||||||
import io.pebbletemplates.pebble.loader.FileLoader;
|
import io.pebbletemplates.pebble.loader.FileLoader;
|
||||||
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
import io.pebbletemplates.pebble.template.PebbleTemplate;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
|
@ -23,10 +28,13 @@ import java.util.Map;
|
||||||
* using the Pebble templating engine.
|
* using the Pebble templating engine.
|
||||||
*/
|
*/
|
||||||
public class TemplateRenderer {
|
public class TemplateRenderer {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(TemplateRenderer.class);
|
||||||
|
|
||||||
private final Site site;
|
private final Site site;
|
||||||
|
private final Minifier minifier;
|
||||||
|
|
||||||
private final PebbleEngine pebbleEngine;
|
private final PebbleEngine pebbleEngine;
|
||||||
private final PebbleTemplate indexTemplate, articleTemplate;
|
private final PebbleTemplate indexTemplate, articleTemplate, pageTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a TemplateRenderer instance for rendering templates from the specified directory.
|
* Constructs a TemplateRenderer instance for rendering templates from the specified directory.
|
||||||
|
@ -35,7 +43,11 @@ public class TemplateRenderer {
|
||||||
* @param templateDirectory the root directory containing the template file
|
* @param templateDirectory the root directory containing the template file
|
||||||
* @param fileHashes file hashes. currently only applies to assets
|
* @param fileHashes file hashes. currently only applies to assets
|
||||||
*/
|
*/
|
||||||
public TemplateRenderer(Site site, Path templateDirectory, Map<String, String> fileHashes) {
|
public TemplateRenderer(Site site, Minifier minifier, Path templateDirectory, Map<String, String> fileHashes) {
|
||||||
|
this.site = site;
|
||||||
|
this.minifier = minifier;
|
||||||
|
|
||||||
|
|
||||||
var loader = new FileLoader();
|
var loader = new FileLoader();
|
||||||
loader.setPrefix(templateDirectory.toString());
|
loader.setPrefix(templateDirectory.toString());
|
||||||
loader.setSuffix(".html");
|
loader.setSuffix(".html");
|
||||||
|
@ -45,21 +57,51 @@ public class TemplateRenderer {
|
||||||
.extension(new TemplateExtension(site, fileHashes))
|
.extension(new TemplateExtension(site, fileHashes))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
this.site = site;
|
|
||||||
this.indexTemplate = pebbleEngine.getTemplate("index_template");
|
this.indexTemplate = pebbleEngine.getTemplate("index_template");
|
||||||
this.articleTemplate = pebbleEngine.getTemplate("article_template");
|
this.articleTemplate = pebbleEngine.getTemplate("article_template");
|
||||||
|
|
||||||
|
var pageTemplate = indexTemplate;
|
||||||
|
|
||||||
|
try {
|
||||||
|
pageTemplate = pebbleEngine.getTemplate("page_template");
|
||||||
|
} catch (LoaderException e) {
|
||||||
|
LOGGER.debug("Template has no page template, using index_template instead");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pageTemplate = pageTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the index page using this template.
|
* Renders a page using this template.<br>
|
||||||
|
* This is different from index, as this uses page template.
|
||||||
*
|
*
|
||||||
* @param articles the {@link Article}s to be included in the index page
|
* @param pageArticles the {@link Article}s to be included in the <strong>CURRENT</strong> page
|
||||||
|
* @param pageNumbers a {@link PageNumbers} instance for the current page
|
||||||
* @return the rendered index HTML page as a string
|
* @return the rendered index HTML page as a string
|
||||||
* @throws IOException if an error occurs during the template evaluation
|
* @throws IOException if an error occurs during the template evaluation
|
||||||
*/
|
*/
|
||||||
public String renderIndex(List<Article> articles) throws IOException {
|
public String renderPage(PageNumbers pageNumbers, List<Article> pageArticles) throws IOException {
|
||||||
var context = Map.of(
|
var context = Map.of(
|
||||||
"articles", articles
|
"page", pageNumbers,
|
||||||
|
"articles", pageArticles
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderTemplate(pageTemplate, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the index page using this template.<br>
|
||||||
|
* This is different from rendering page 1, as this uses index template.
|
||||||
|
*
|
||||||
|
* @param pageArticles the {@link Article}s to be included in the <strong>CURRENT</strong> page
|
||||||
|
* @param pageNumbers a {@link PageNumbers} instance for the current page
|
||||||
|
* @return the rendered index HTML page as a string
|
||||||
|
* @throws IOException if an error occurs during the template evaluation
|
||||||
|
*/
|
||||||
|
public String renderIndex(PageNumbers pageNumbers, List<Article> pageArticles) throws IOException {
|
||||||
|
var context = Map.of(
|
||||||
|
"page", pageNumbers,
|
||||||
|
"articles", pageArticles
|
||||||
);
|
);
|
||||||
|
|
||||||
return renderTemplate(indexTemplate, context);
|
return renderTemplate(indexTemplate, context);
|
||||||
|
@ -94,7 +136,12 @@ public class TemplateRenderer {
|
||||||
private String renderTemplate(PebbleTemplate template, Map<String, ?> context) throws IOException {
|
private String renderTemplate(PebbleTemplate template, Map<String, ?> context) throws IOException {
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
template.evaluate(writer, (Map<String, Object>) context);
|
template.evaluate(writer, (Map<String, Object>) context);
|
||||||
|
var html = writer.toString();
|
||||||
|
|
||||||
return writer.toString();
|
if (minifier != null) { // not checking site.minify because the minifier may not be available
|
||||||
|
html = minifier.minify(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
src/main/java/eu/m724/blog/vc/Change.java
Normal file
14
src/main/java/eu/m724/blog/vc/Change.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.vc;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
public record Change(
|
||||||
|
String author,
|
||||||
|
ZonedDateTime time
|
||||||
|
) { }
|
56
src/main/java/eu/m724/blog/vc/GitVersionControl.java
Normal file
56
src/main/java/eu/m724/blog/vc/GitVersionControl.java
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.vc;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.RepositoryBuilder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GitVersionControl implements VersionControl {
|
||||||
|
private final Git git;
|
||||||
|
private final Path directory;
|
||||||
|
|
||||||
|
public GitVersionControl(Path directory) throws IOException {
|
||||||
|
this.directory = directory;
|
||||||
|
|
||||||
|
var repository = new RepositoryBuilder()
|
||||||
|
.setGitDir(directory.resolve(".git").toFile())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.git = new Git(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Change> getChanges(Path path) throws VersionControlException {
|
||||||
|
path = path.normalize();
|
||||||
|
if (path.startsWith(directory)) {
|
||||||
|
path = directory.relativize(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var changes = new ArrayList<Change>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (var commit : git.log().addPath(path.toString()).call()) {
|
||||||
|
var author = commit.getAuthorIdent().getName();
|
||||||
|
var time = Instant.ofEpochSecond(commit.getCommitTime()).atZone(ZoneOffset.UTC);
|
||||||
|
|
||||||
|
changes.add(new Change(author, time));
|
||||||
|
}
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
throw new VersionControlException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
}
|
15
src/main/java/eu/m724/blog/vc/VersionControl.java
Normal file
15
src/main/java/eu/m724/blog/vc/VersionControl.java
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.vc;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface VersionControl {
|
||||||
|
List<Change> getChanges(Path path) throws VersionControlException;
|
||||||
|
}
|
||||||
|
|
15
src/main/java/eu/m724/blog/vc/VersionControlException.java
Normal file
15
src/main/java/eu/m724/blog/vc/VersionControlException.java
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.vc;
|
||||||
|
|
||||||
|
// TODO runtime exception really?
|
||||||
|
public class VersionControlException extends RuntimeException {
|
||||||
|
public VersionControlException(Throwable cause) {
|
||||||
|
// TODO maybe I need to raise message here too
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue