Compare commits
8 Commits
0c820183a6
...
main
Author | SHA1 | Date | |
---|---|---|---|
8723ce88dc | |||
92548e6bad
|
|||
93634f230f
|
|||
e0de885cdd
|
|||
ccccb9d238
|
|||
71f795bae5
|
|||
973274fed3
|
|||
502f1c59de
|
23
Dockerfile
23
Dockerfile
@ -4,28 +4,17 @@
|
||||
#
|
||||
# it exposes the data/ and secrets/ volumes in app root
|
||||
#
|
||||
FROM openresty/openresty:alpine-fat
|
||||
FROM openresty/openresty:1.25.3.2-5-alpine-fat
|
||||
|
||||
RUN apk add --no-cache git make gcc g++ musl-dev libffi-dev openssl-dev sqlite-dev libsodium libsodium-dev imagemagick-dev openssl
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN eval "$(luarocks --lua-version=5.1 path)"
|
||||
# listing all deps one by one until a more stable solution to the luarocks problem
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/javierguerragiraldez/lsqlite3-0.9.6-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/kikito/ansicolors-1.0.2-3.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/argparse/argparse-0.7.1-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/tieske/date-2.2.1-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/leafo/etlua-1.3.0-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/leafo/loadkit-1.1.0-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/gvvaughan/lpeg-1.1.0-2.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/openresty/lua-cjson-2.1.0.10-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/daurnimator/luaossl-20220711-0.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/lunarmodules/luasocket-3.1.0-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/leafo/pgmoon-1.16.0-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/leafo/magick-1.6.0-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/jprjr/luasodium-2.4.0-1.rockspec
|
||||
RUN luarocks --lua-version=5.1 install https://luarocks.org/manifests/leafo/lapis-1.16.0-1.rockspec
|
||||
# RUN luarocks --lua-version=5.1 build --only-deps
|
||||
# if using openresty images, make sure the image version is >= 1.25.3.2-5 or >= 1.27.1.2-2
|
||||
# see https://github.com/openresty/docker-openresty/issues/276#issuecomment-2950726213
|
||||
# otherwise, make sure your image uses luarocks >= 3.12.0
|
||||
# see https://github.com/luarocks/luarocks/issues/1797
|
||||
RUN luarocks --lua-version=5.1 build --only-deps
|
||||
EXPOSE 8080
|
||||
RUN chmod +x /app/start.sh
|
||||
ENTRYPOINT ["/app/start.sh", "production"]
|
||||
|
@ -1,5 +1,7 @@
|
||||
# Porom
|
||||
porous forum
|
||||
# Note
|
||||
Development has moved over to [pyrom](https://git.poto.cafe/yagich/pyrom).
|
||||
|
||||
# License
|
||||
Released under [CNPLv7+](https://thufie.lain.haus/NPL.html).
|
||||
|
@ -25,7 +25,7 @@ Designers: Paul James Miller
|
||||
|
||||
## ICONCINO
|
||||
|
||||
Affected files: [`svg-icons/error.etlua`](./svg-icons/error.etlua) [`svg-icons/info.etlua`](./svg-icons/info.etlua) [`svg-icons/lock.etlua`](./svg-icons/lock.etlua) [`svg-icons/sticky.etlua`](./svg-icons/sticky.etlua) [`svg-icons/warn.etlua`](./svg-icons/warn.etlua)
|
||||
Affected files: [`svg-icons/error.etlua`](./svg-icons/error.etlua) [`svg-icons/image.etlua`](./svg-icons/image.etlua) [`svg-icons/info.etlua`](./svg-icons/info.etlua) [`svg-icons/lock.etlua`](./svg-icons/lock.etlua) [`svg-icons/sticky.etlua`](./svg-icons/sticky.etlua) [`svg-icons/warn.etlua`](./svg-icons/warn.etlua)
|
||||
URL: https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license
|
||||
Copyright: Gabriele Malaspina
|
||||
Designers: Gabriele Malaspina
|
||||
|
@ -176,7 +176,7 @@ pre code {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#delete-dialog {
|
||||
#delete-dialog, .lightbox-dialog {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 2px solid black;
|
||||
@ -190,6 +190,27 @@ pre code {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.lightbox-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
min-width: 400px;
|
||||
background-color: #c1ceb1;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-width: 70vw;
|
||||
max-height: 70vh;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.lightbox-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copy-code-container {
|
||||
position: sticky;
|
||||
width: calc(100% - 4px);
|
||||
@ -442,8 +463,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contain-svg > svg {
|
||||
.contain-svg:not(.full) > svg {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
@ -700,3 +720,16 @@ ul, ol {
|
||||
.inbox-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.babycode-button-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.babycode-button {
|
||||
padding: 5px 10px;
|
||||
min-width: 36px;
|
||||
}
|
||||
.babycode-button > * {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
@ -8,14 +8,40 @@
|
||||
}
|
||||
})
|
||||
|
||||
const inThread = () => {
|
||||
const scheme = window.location.pathname.split("/");
|
||||
return scheme[1] === "threads" && scheme[2] !== "create";
|
||||
}
|
||||
|
||||
ta.addEventListener("input", () => {
|
||||
if (!inThread()) return;
|
||||
|
||||
localStorage.setItem(window.location.pathname, ta.value);
|
||||
})
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (!inThread()) return;
|
||||
const prevContent = localStorage.getItem(window.location.pathname);
|
||||
if (!prevContent) return;
|
||||
ta.value = prevContent;
|
||||
})
|
||||
|
||||
const buttonBold = document.getElementById("post-editor-bold");
|
||||
const buttonItalics = document.getElementById("post-editor-italics");
|
||||
const buttonStrike = document.getElementById("post-editor-strike");
|
||||
const buttonUrl = document.getElementById("post-editor-url");
|
||||
const buttonCode = document.getElementById("post-editor-code");
|
||||
const buttonImg = document.getElementById("post-editor-img");
|
||||
const buttonOl = document.getElementById("post-editor-ol");
|
||||
const buttonUl = document.getElementById("post-editor-ul");
|
||||
|
||||
function insertTag(tagStart, newline = false) {
|
||||
const tagEnd = tagStart;
|
||||
const tagInsertStart = `[${tagStart}]${newline ? "\n" : ""}`;
|
||||
function insertTag(tagStart, newline = false, prefill = "") {
|
||||
const hasAttr = tagStart[tagStart.length - 1] === "=";
|
||||
let tagEnd = tagStart;
|
||||
let tagInsertStart = `[${tagStart}]${newline ? "\n" : ""}`;
|
||||
if (hasAttr) {
|
||||
tagEnd = tagEnd.slice(0, -1);
|
||||
}
|
||||
const tagInsertEnd = `${newline ? "\n" : ""}[/${tagEnd}]`;
|
||||
const hasSelection = ta.selectionStart !== ta.selectionEnd;
|
||||
const text = ta.value;
|
||||
@ -29,13 +55,24 @@
|
||||
const frag = `${tagInsertStart}${text.slice(realStart, realEnd)}${tagInsertEnd}`;
|
||||
const reconst = `${strStart}${frag}${strEnd}`;
|
||||
ta.value = reconst;
|
||||
if (!hasAttr){
|
||||
ta.setSelectionRange(realStart + tagInsertStart.length, realStart + tagInsertStart.length + selectionLength);
|
||||
} else {
|
||||
ta.setSelectionRange(realStart + tagInsertEnd.length - 1, realStart + tagInsertEnd.length - 1); // cursor on attr
|
||||
}
|
||||
ta.focus()
|
||||
} else {
|
||||
if (hasAttr) {
|
||||
tagInsertStart += prefill;
|
||||
}
|
||||
const cursor = ta.selectionStart;
|
||||
const strStart = text.slice(0, cursor);
|
||||
const strEnd = text.substr(cursor);
|
||||
const newCursor = strStart.length + tagInsertStart.length;
|
||||
|
||||
let newCursor = strStart.length + tagInsertStart.length;
|
||||
if (hasAttr) {
|
||||
newCursor = cursor + tagInsertStart.length - prefill.length - 1;
|
||||
}
|
||||
const reconst = `${strStart}${tagInsertStart}${tagInsertEnd}${strEnd}`;
|
||||
ta.value = reconst;
|
||||
ta.setSelectionRange(newCursor, newCursor);
|
||||
@ -55,10 +92,26 @@
|
||||
e.preventDefault();
|
||||
insertTag("s")
|
||||
})
|
||||
buttonUrl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("url=", false, "link label");
|
||||
})
|
||||
buttonCode.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("code", true)
|
||||
})
|
||||
buttonImg.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("img=", false, "alt text");
|
||||
})
|
||||
buttonOl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("ol", true);
|
||||
})
|
||||
buttonUl.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
insertTag("ul", true);
|
||||
})
|
||||
|
||||
const previewEndpoint = "/api/babycode-preview";
|
||||
let previousMarkup = "";
|
||||
|
99
js/ui.js
99
js/ui.js
@ -21,6 +21,86 @@ function activateSelfDeactivateSibs(button) {
|
||||
});
|
||||
}
|
||||
|
||||
function openLightbox(post, idx) {
|
||||
lightboxCurrentPost = post;
|
||||
lightboxCurrentIdx = idx;
|
||||
lightboxObj.img.src = lightboxImages.get(post)[idx].src;
|
||||
lightboxObj.openOriginalAnchor.href = lightboxImages.get(post)[idx].src
|
||||
lightboxObj.prevButton.disabled = lightboxImages.get(post).length === 1
|
||||
lightboxObj.nextButton.disabled = lightboxImages.get(post).length === 1
|
||||
lightboxObj.imageCount.textContent = `Image ${idx + 1} of ${lightboxImages.get(post).length}`
|
||||
|
||||
if (!lightboxObj.dialog.open) {
|
||||
lightboxObj.dialog.showModal();
|
||||
}
|
||||
}
|
||||
|
||||
const modulo = (n, m) => ((n % m) + m) % m
|
||||
|
||||
function lightboxNext() {
|
||||
const l = lightboxImages.get(lightboxCurrentPost).length;
|
||||
const target = modulo(lightboxCurrentIdx + 1, l);
|
||||
openLightbox(lightboxCurrentPost, target);
|
||||
}
|
||||
|
||||
function lightboxPrev() {
|
||||
const l = lightboxImages.get(lightboxCurrentPost).length;
|
||||
const target = modulo(lightboxCurrentIdx - 1, l);
|
||||
openLightbox(lightboxCurrentPost, target);
|
||||
}
|
||||
|
||||
function constructLightbox() {
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.classList.add("lightbox-dialog");
|
||||
dialog.addEventListener("click", (e) => {
|
||||
if (e.target === dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
})
|
||||
const dialogInner = document.createElement("div");
|
||||
dialogInner.classList.add("lightbox-inner");
|
||||
dialog.appendChild(dialogInner);
|
||||
const img = document.createElement("img");
|
||||
img.classList.add("lightbox-image")
|
||||
dialogInner.appendChild(img);
|
||||
const openOriginalAnchor = document.createElement("a")
|
||||
openOriginalAnchor.text = "Open original in new window"
|
||||
openOriginalAnchor.target = "_blank"
|
||||
openOriginalAnchor.rel = "noopener noreferrer nofollow"
|
||||
dialogInner.appendChild(openOriginalAnchor);
|
||||
|
||||
const navSpan = document.createElement("span");
|
||||
navSpan.classList.add("lightbox-nav");
|
||||
const prevButton = document.createElement("button");
|
||||
prevButton.type = "button";
|
||||
prevButton.textContent = "Previous";
|
||||
prevButton.addEventListener("click", lightboxPrev);
|
||||
const nextButton = document.createElement("button");
|
||||
nextButton.type = "button";
|
||||
nextButton.textContent = "Next";
|
||||
nextButton.addEventListener("click", lightboxNext);
|
||||
const imageCount = document.createElement("span");
|
||||
imageCount.textContent = "Image of ";
|
||||
navSpan.appendChild(prevButton);
|
||||
navSpan.appendChild(imageCount);
|
||||
navSpan.appendChild(nextButton);
|
||||
|
||||
dialogInner.appendChild(navSpan);
|
||||
return {
|
||||
img: img,
|
||||
dialog: dialog,
|
||||
openOriginalAnchor: openOriginalAnchor,
|
||||
prevButton: prevButton,
|
||||
nextButton: nextButton,
|
||||
imageCount: imageCount,
|
||||
}
|
||||
}
|
||||
|
||||
let lightboxImages = new Map(); //.post-inner : Array<Object>
|
||||
let lightboxObj = null;
|
||||
let lightboxCurrentPost = null;
|
||||
let lightboxCurrentIdx = -1;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// tabs
|
||||
document.querySelectorAll(".tab-button").forEach(button => {
|
||||
@ -45,4 +125,23 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
toggleButton.addEventListener("click", toggle);
|
||||
});
|
||||
|
||||
//lightboxes
|
||||
lightboxObj = constructLightbox();
|
||||
document.body.appendChild(lightboxObj.dialog);
|
||||
const postImages = document.querySelectorAll(".post-inner img.block-img");
|
||||
postImages.forEach(postImage => {
|
||||
const belongingTo = postImage.closest(".post-inner");
|
||||
const images = lightboxImages.get(belongingTo) ?? [];
|
||||
images.push({
|
||||
src: postImage.src,
|
||||
alt: postImage.alt,
|
||||
});
|
||||
const idx = images.length - 1;
|
||||
lightboxImages.set(belongingTo, images);
|
||||
postImage.style.cursor = "pointer";
|
||||
postImage.addEventListener("click", () => {
|
||||
openLightbox(belongingTo, idx);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -71,7 +71,7 @@ local tags = {
|
||||
b = "<strong>$S</strong>",
|
||||
i = "<em>$S</em>",
|
||||
s = "<del>$S</del>",
|
||||
img = "<div class=\"post-img-container\"><img class=\"block-img\" src=$A alt=%S></div>",
|
||||
img = "<div class=\"post-img-container\"><img class=\"block-img\" src=$A alt=$S></div>",
|
||||
url = "<a href=\"$A\">$S</a>",
|
||||
quote = "<blockquote>$S</blockquote>",
|
||||
code = function(children)
|
||||
|
@ -220,7 +220,7 @@ pre code {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#delete-dialog {
|
||||
#delete-dialog, .lightbox-dialog {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 2px solid black;
|
||||
@ -234,6 +234,27 @@ pre code {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.lightbox-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
min-width: 400px;
|
||||
background-color: $accent_color;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-width: 70vw;
|
||||
max-height: 70vh;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.lightbox-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copy-code-container {
|
||||
position: sticky;
|
||||
// width: 100%;
|
||||
@ -451,12 +472,11 @@ input[type="text"], input[type="password"], textarea, select {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contain-svg > svg {
|
||||
&:not(.full) > svg {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.block-img {
|
||||
object-fit: contain;
|
||||
@ -708,3 +728,17 @@ ul, ol {
|
||||
.inbox-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.babycode-button-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.babycode-button {
|
||||
padding: 5px 10px;
|
||||
min-width: 36px;
|
||||
|
||||
&> * {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
5
svg-icons/image.etlua
Normal file
5
svg-icons/image.etlua
Normal file
@ -0,0 +1,5 @@
|
||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 17L7.58959 13.7694C8.38025 13.0578 9.58958 13.0896 10.3417 13.8417L11.5 15L15.0858 11.4142C15.8668 10.6332 17.1332 10.6332 17.9142 11.4142L20 13.5M11 9C11 9.55228 10.5523 10 10 10C9.44772 10 9 9.55228 9 9C9 8.44772 9.44772 8 10 8C10.5523 8 11 8.44772 11 9ZM6 20H18C19.1046 20 20 19.1046 20 18V6C20 4.89543 19.1046 4 18 4H6C4.89543 4 4 4.89543 4 6V18C4 19.1046 4.89543 20 6 20Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
@ -18,6 +18,6 @@
|
||||
</footer>
|
||||
<script src="/static/js/copy-code.js"></script>
|
||||
<script src="/static/js/date-fmt.js"></script>
|
||||
<script src="/static/js/ui.js"></script>
|
||||
<script src="/static/js/ui.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -4,11 +4,15 @@
|
||||
<button type=button class="tab-button" data-target-id="tab-preview">Preview</button>
|
||||
</div>
|
||||
<div class="tab-content active" id="tab-edit">
|
||||
<span>
|
||||
<button type=button id="post-editor-bold" title="Insert Bold">B</button>
|
||||
<button type=button id="post-editor-italics" title="Insert Italics">I</button>
|
||||
<button type=button id="post-editor-strike" title="Insert Strikethrough">S</button>
|
||||
<button type=button id="post-editor-code" title="Insert Code block">Code</button>
|
||||
<span class="babycode-button-container">
|
||||
<button class="babycode-button" type=button id="post-editor-bold" title="Insert Bold"><strong>B</strong></button>
|
||||
<button class="babycode-button" type=button id="post-editor-italics" title="Insert Italics"><em>I</em></button>
|
||||
<button class="babycode-button" type=button id="post-editor-strike" title="Insert Strikethrough"><del>S</del></button>
|
||||
<button class="babycode-button" type=button id="post-editor-url" title="Insert Link"><code>://</code></button>
|
||||
<button class="babycode-button" type=button id="post-editor-code" title="Insert Code block"><code></></code></button>
|
||||
<button class="babycode-button contain-svg full" type=button id="post-editor-img" title="Insert Image"><% render("svg-icons.image") %></button>
|
||||
<button class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list">1.</button>
|
||||
<button class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list">•</button>
|
||||
</span>
|
||||
<textarea class="babycode-editor" name="<%= ta_name %>" id="babycode-content" placeholder="<%= ta_placeholder or "Post body"%>" <%= not optional and "required" or "" %>><%- prefill or "" %></textarea>
|
||||
<a href="<%= url_for("babycode_guide") %>" target="_blank">babycode guide</a>
|
||||
|
Reference in New Issue
Block a user