Compare commits

..

1 commit

Author SHA1 Message Date
Simon Vieille
9dc3272d40
[wip] mastodon integration
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-07-27 18:28:25 +02:00
124 changed files with 36459 additions and 7704 deletions

11
.env
View file

@ -35,10 +35,7 @@ MAILER_SENDER=example@localhost
ASSET_BASE_URL=null
UMAMI_URL=null
MESSENGER_TRANSPORT_DSN=doctrine://default
INFLUXDB_URL=
INFLUXDB_TOKEN=
INFLUXDB_BUCKET=
INFLUXDB_ORG=
INFLUXDB_DEBUG=1
MASTODON_HOST=
MASTODON_CLIENT_ID=
MASTODON_CLIENT_SECRET=
MASTODON_CLIENT_ACCESS_TOKEN=

View file

@ -18,7 +18,6 @@ magephp:
- "/var/cache/*"
- "/var/log/*"
- "/public/media"
- "/.secrets"
hosts:
- ssh_host
on-deploy:
@ -26,4 +25,3 @@ magephp:
- exec: { cmd: 'make doctrine-migration', desc: 'migration' }
- exec: { cmd: 'php8.1 ./bin/console cache:warmup', desc: 'warmup' }
- exec: { cmd: 'php8.1 ./bin/console cache:warmup', desc: 'warmup2' }
- exec: { cmd: './bin/messenger -a restart', desc: 'messenger' }

View file

@ -1,39 +0,0 @@
environments:
build:
variables:
- name: MYSQLDUMP
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: mysqldump
deploy:
variables:
- name: SSH_USER
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: ssh_user
- name: SSH_HOST
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: ssh_host
- name: SSH_PRIV_KEY
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: ssh_priv_key
- name: APP_DIRECTORY
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: app_directory

View file

@ -1,56 +1,58 @@
variables:
volumes: &volumes
- node_cache:/root/.npm
- /data/${CI_REPO}:/builds
- &volumes
- node16_cache:/root/.npm
when:
event: [push, pull_request, tag, manual]
branch: [master, master-*, develop, develop-*, feature/*]
steps:
"Wait the database":
pipeline:
db-wait:
image: gitnet.fr/deblan/timeout:latest
commands:
- /bin/timeout -t 30 -v -c 'while true; do nc -z -v db 3306 2>&1 | grep succeeded && exit 0; sleep 0.5; done'
"Create database":
db-create:
image: mariadb:10.3
environment:
MYSQLDUMP:
from_secret: mysqldump
secrets: [mysqldump]
commands:
- mysql -hdb -uroot -proot -e "CREATE DATABASE app"
- eval "$MYSQLDUMP" | mysql -hdb -uroot -proot app
when:
branch: [master, master-*, develop, develop-*]
"Configure app":
app-config:
image: deblan/php:8.1
commands:
- echo APP_ENV=prod >> .env.local
- echo APP_SECRET=$(openssl rand -hex 32) >> .env.local
- echo DATABASE_URL=mysql://root:root@db/app >> .env.local
when:
branch: [master, master-*, develop, develop-*]
"Installs PHP dependencies":
php-composer:
image: deblan/php:8.1
commands:
- apt-get update && apt-get -y install git
- composer install --no-scripts
"Migrates database":
image: deblan/php:8.3
db-migrate:
image: deblan/php:8.1
environment:
PHP: php
- PHP=php
commands:
- ./bin/doctrine-migrate
when:
branch: [master, master-*, develop, develop-*, feature/*]
"Generates JS routes":
image: deblan/php:8.3
app-jsroutes:
image: deblan/php:8.1
commands:
- php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json
when:
branch: [master, master-*, develop, develop-*, feature/*]
"Build assets":
image: node:20-alpine
node-build:
image: node:16-alpine
environment:
CPU_COUNT: 3
- CPU_COUNT=3
volumes: *volumes
commands:
- apk add --no-cache git
- npm install -g svg2ttf ttf2eot ttf2woff2
@ -61,24 +63,36 @@ steps:
- test -f public/js/fos_js_routes.json || echo "{}" > public/js/fos_js_routes.json
- npm run build
"Check dependencies":
security-check:
image: gitnet.fr/deblan/osv-detector:v0.9
commands:
- osv-detector composer.lock yarn.lock
failure: ignore
"Build the cache":
image: deblan/mage
volumes: *volumes
app-deploy:
image: deblan/php:8.1
secrets: [ssh_user, ssh_host, ssh_priv_key, app_directory]
commands:
- cd /builds
- rsync -az "$CI_WORKSPACE/" "$CI_COMMIT_SHA"
- apt-get update && apt-get -y install rsync openssh-client
- mkdir "$HOME/.ssh"
- echo "$SSH_PRIV_KEY" > "$HOME/.ssh/id_ed25519"
- chmod 700 "$HOME/.ssh"
- chmod 600 "$HOME/.ssh/id_ed25519"
- composer global require andres-montanez/magallanes
- cp .mage.yml.dist .mage.yml
- sed -i "s/ssh_user/$SSH_USER/g" .mage.yml
- sed -i "s/ssh_host/$SSH_HOST/g" .mage.yml
- sed -i "s#app_directory#$APP_DIRECTORY#g" .mage.yml
- /root/.config/composer/vendor/bin/mage deploy "$CI_BUILD_DEPLOY_TARGET"
when:
event: [deployment]
services:
db:
image: mariadb:10.3
environment:
MARIADB_ROOT_PASSWORD: root
- MARIADB_ROOT_PASSWORD=root
volumes:
node_cache:
node16_cache:
temp: {}

View file

@ -1,33 +0,0 @@
variables:
volumes: &volumes
- /data/${CI_REPO}:/builds
when:
event: [deployment]
skip_clone: true
steps:
"Deploy":
image: deblan/mage
environment:
SSH_PRIV_KEY:
from_secret: ssh_priv_key
SSH_USER:
from_secret: ssh_user
SSH_HOST:
from_secret: ssh_host
APP_DIRECTORY:
from_secret: app_directory
volumes: *volumes
commands:
- cd "/builds/$CI_COMMIT_SHA"
- mkdir "$HOME/.ssh"
- echo "$SSH_PRIV_KEY" > "$HOME/.ssh/id_ed25519"
- chmod 700 "$HOME/.ssh"
- chmod 600 "$HOME/.ssh/id_ed25519"
- cp .mage.yml.dist .mage.yml
- sed -i "s/ssh_user/$SSH_USER/g" .mage.yml
- sed -i "s/ssh_host/$SSH_HOST/g" .mage.yml
- sed -i "s#app_directory#$APP_DIRECTORY#g" .mage.yml
- mage deploy "$CI_PIPELINE_DEPLOY_TARGET"

View file

@ -1,63 +1,57 @@
@import "../../vendor/murph/murph-core/src/core/Resources/assets/css/admin.scss";
@import "@kangc/v-md-editor/lib/style/base-editor.css";
@import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
@import "~simplemde/dist/simplemde.min.css";
.CodeMirror-fullscreen, .editor-toolbar.fullscreen {
z-index: 2000;
}
.ejs-link {
margin: 10px auto;
max-width: 80%;
border: 2px solid #333;
border-radius: 5px;
margin: 10px auto;
max-width: 80%;
border: 2px solid #333;
border-radius: 5px;
&--anchor {
display: block;
padding: 30px;
}
&-content {
display: inline-block;
vertical-align: top;
&--title {
font-weight: bold;
&--anchor {
display: block;
padding: 30px;
}
&--description {
font-size: 15px;
&-content {
display: inline-block;
vertical-align: top;
&--title {
font-weight: bold;
}
&--description {
font-size: 15px;
}
&--link {
padding-top: 10px;
font-size: 14px;
line-height: 20px;
}
}
&--link {
padding-top: 10px;
font-size: 14px;
line-height: 20px;
$image-size: 85px;
&--anchor--with-image &-content {
width: calc(100% - $image-size - 5px);
padding-right: 25px;
}
}
$image-size: 85px;
&--anchor--with-image &-content {
width: calc(100% - $image-size - 5px);
padding-right: 25px;
}
&--image {
display: inline-block;
width: $image-size;
height: $image-size;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
&--image {
display: inline-block;
width: $image-size;
height: $image-size;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
}
.choices__list--dropdown {
z-index: 3;
}
.v-md-editor {
border: 1px solid $input-border-color;
box-shadow: none;
}
.v-md-editor--fullscreen {
z-index: 3000;
}

View file

@ -1,26 +1,29 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "app/config";
@import "app/prism";
@import 'app/prism';
@import "app/typo";
@import "~tingle.js/src/tingle.css";
@font-face {
font-family: "MainFont";
src: url("../fonts/ubuntu/ubuntu-light.woff2?20211108") format("woff2"), url("../fonts/ubuntu/ubuntu-light.woff?20211108") format("woff");
// url('../fonts/atkinson/WOFF2/Atkinson-Hyperlegible-Regular-102a.woff2?20220911') format('woff2'),
// url('../fonts/atkinson/WOFF/Atkinson-Hyperlegible-Regular-102.woff?20211108w20220911') format('woff');
src:
url('../fonts/ubuntu/ubuntu-light.woff2?20211108') format('woff2'),
url('../fonts/ubuntu/ubuntu-light.woff?20211108') format('woff');
// url('../fonts/atkinson/WOFF2/Atkinson-Hyperlegible-Regular-102a.woff2?20220911') format('woff2'),
// url('../fonts/atkinson/WOFF/Atkinson-Hyperlegible-Regular-102.woff?20211108w20220911') format('woff');
}
@font-face {
font-family: "deblan-icon";
src: url("../fonts/deblan/deblan-icon.eot?20211108");
src: url("../fonts/deblan/deblan-icon.woff2?20211108") format("woff2"), url("../fonts/deblan/deblan-icon.woff?20211108") format("woff"), url("../fonts/deblan/deblan-icon.ttf?20211108") format("truetype");
src: url('../fonts/deblan/deblan-icon.eot?20211108');
src:
url('../fonts/deblan/deblan-icon.woff2?20211108') format('woff2'),
url('../fonts/deblan/deblan-icon.woff?20211108') format('woff'),
url('../fonts/deblan/deblan-icon.ttf?20211108') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
@ -58,7 +61,7 @@ $dicons: coffee server search project share contact list response twitter diaspo
}
.text-right {
text-align: right;
text-align: right;
}
.list--inline {
@ -154,9 +157,8 @@ pre[class*="language-"] {
height: 50px;
overflow: hidden;
display: inline-block;
margin-bottom: -19px;
margin-bottom: -19px;i
i
&:active, &:focus {
background: none;
}
@ -171,7 +173,6 @@ pre[class*="language-"] {
background: url(../images/Refresh_icon.svg);
}
}
//
// &-captcha {
// label {
@ -303,11 +304,9 @@ pre[class*="language-"] {
0% {
background-position: 0 0%;
}
50% {
background-position: 0 75%;
}
100% {
background-position: 0 0%;
}
@ -348,20 +347,18 @@ pre[class*="language-"] {
width: 100%;
height: 10px;
background: rgb(0, 0, 0);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(255, 255, 255, 0) 100%);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(255,255,255,0) 100%);
}
@keyframes HeaderGradient {
0% {
background-position: 0 50%;
background-position: 0 50%
}
50% {
background-position: 100% 50%;
background-position: 100% 50%
}
100% {
background-position: 0 50%;
background-position: 0 50%
}
}
@ -414,9 +411,6 @@ pre[class*="language-"] {
.h1 {
font-weight: normal;
font-size: 40px;
font-family: MainFont;
text-shadow: none;
color: hsla(0, 0%, 100%, 0.7);
}
.h3 {
@ -478,7 +472,7 @@ pre[class*="language-"] {
}
p a:not(.btn), ul:not(.btn-group) a:not(.btn) {
background: url("../images/link.svg") bottom left repeat-x;
background: url('../images/link.svg') bottom left repeat-x;
padding-bottom: 5px;
}
@ -509,35 +503,6 @@ pre[class*="language-"] {
@include make-pre-code;
}
&.mermaid {
box-shadow: none;
background: #343c53;
border: 0;
text, tspan {
fill: #fff !important;
}
.actor, .noteText {
text, tspan {
fill: #333 !important;
}
}
.labelText {
fill: #333 !important;
}
line, path {
stroke: #fff !important;
}
svg {
display: block;
margin: auto;
}
}
&.with-title {
margin-top: 0;
border-top-right-radius: 0;
@ -548,41 +513,11 @@ pre[class*="language-"] {
padding-bottom: 10px;
padding-top: 10px;
border: 1px solid $color-code-border;
// overflow: hidden;
//
// &:hover {
// overflow: auto;
// }
}
}
}
.post-author-wrapper {
.post-author-avatar {
vertical-align: top;
width: 100px;
padding: 10px;
display: inline-block;
}
.post-author {
display: inline-block;
width: calc(100% - 100px);
padding: 10px 10px 10px 30px;
}
@media screen and (max-width: 774px) {
.post-author-avatar {
display: block;
margin: auto;
}
.post-author {
display: block;
width: 100%;
padding-right: 20px;
}
}
}
@ -642,7 +577,7 @@ pre[class*="language-"] {
@for $i from 1 through 6 {
.review.offset-#{$i} {
margin-left: 5% * $i - 1%;
width: 100% - $i * 5%;
width: 100% - ($i * 5%);
}
}
@ -651,6 +586,7 @@ pre[class*="language-"] {
max-width: calc($content-max-width - 60px - 2rem);
overflow: auto;
.review-avatar, .review-avatar img {
width: 60px;
max-width: 60px;
@ -665,7 +601,7 @@ pre[class*="language-"] {
border: 1px solid $color-hr-border;
.review-content p {
margin-top: 0;
margin-top: 0;
}
> ul {
@ -730,7 +666,7 @@ pre[class*="language-"] {
.code-window {
height: 50px;
background: $color-code-title-background url("../images/window.svg") no-repeat center right;
background: $color-code-title-background url('../images/window.svg') no-repeat center right;
padding-left: 15px;
font-family: Monospace;
color: #ccc;
@ -764,8 +700,7 @@ pre[class*="language-"] {
width: 100%;
height: 450px;
background-position: center center;
background: #f2f2f2 url("../images/quick-post-load.png") no-repeat center center;
background: #f2f2f2 url('../images/quick-post-load.png') no-repeat center center;
// border: 2px solid $color-very-light-grey;
border-bottom: 0;
cursor: pointer;
@ -904,7 +839,19 @@ pre[class*="language-"] {
border: 1px solid $color-white;
}
$links: (twitter: #20b8ff, rss: #fd9f13, linkedin: #006699, diaspora: #90b92e, github: #8cc345, code: #51d066, mastodon: #2984d2, pixelfed: #e72151, matrix: #1a588a, gpg: #42a73b, murph: #19b4db);
$links: (
twitter: #20b8ff,
rss: #fd9f13,
linkedin: #006699,
diaspora: #90b92e,
github: #8cc345,
code: #51d066,
mastodon: #2984d2,
pixelfed: #e72151,
matrix: #1a588a,
gpg: #42a73b,
murph: #19b4db
);
@each $site, $bg in $links {
.link-#{$site} {
@ -1007,29 +954,25 @@ $links: (twitter: #20b8ff, rss: #fd9f13, linkedin: #006699, diaspora: #90b92e, g
}
@keyframes bounceIn {
0% {
0%{
opacity: 0;
}
50% {
50%{
opacity: 0.9;
}
80% {
80%{
opacity: 1;
}
100% {
100%{
opacity: 1;
}
}
@keyframes knmc {
0% {
left: -256px;
0%{
left: -256px;;
}
100% {
100%{
left: 150vw;
}
}
@ -1335,13 +1278,13 @@ $links: (twitter: #20b8ff, rss: #fd9f13, linkedin: #006699, diaspora: #90b92e, g
}
.invalid-feedback {
color: red;
padding-left: 10px;
padding-right: 10px;
color: red;
padding-left: 10px;
padding-right: 10px;
.form-error-icon {
display: none;
}
.form-error-icon {
display: none;
}
}
@media (prefers-color-scheme: dark) {
@ -1379,7 +1322,7 @@ $links: (twitter: #20b8ff, rss: #fd9f13, linkedin: #006699, diaspora: #90b92e, g
}
.header-shadow {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(255, 255, 255, 0) 100%);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(255,255,255,0) 100%);
}
}
@ -1392,18 +1335,3 @@ $links: (twitter: #20b8ff, rss: #fd9f13, linkedin: #006699, diaspora: #90b92e, g
}
}
}
.deprecated {
color: #fff;
background: #3abff8;
padding: 1rem;
border-radius: var(--rounded-box, 1rem);
text-align: center;
svg {
display: inline-block;
height: 25px;
vertical-align: top;
margin-right: 8px;
}
}

View file

@ -1,34 +1,34 @@
.alert {
padding: 20px;
border: 1px solid #333;
padding: 20px;
border: 1px solid #333;
&-success {
border-color: #9db024;
background: #c6ff69;
color: #415f29;
}
&-success {
border-color: #9db024;
background: #c6ff69;
color: #415f29;
}
&-notice {
border-color: $color-blue;
background: #66e6ff;
color: #254e5f;
}
&-notice {
border-color: $color-blue;
background: #66e6ff;
color: #254e5f;
}
&-warning {
border-color: #b07f29;
background: #ffd465;
color: #5f4520;
}
&-warning {
border-color: #b07f29;
background: #ffd465;
color: #5f4520;
}
&-error {
border-color: #b02e2a;
background: #ff6363;
color: #5f2521;
}
&-error {
border-color: #b02e2a;
background: #ff6363;
color: #5f2521;
}
&-notice-light {
border-color: $color-blue;
background: #d9fffc;
color: #254e5f;
}
&-notice-light {
border-color: $color-blue;
background: #d9fffc;
color: #254e5f;
}
}

View file

@ -62,7 +62,6 @@ $color-code-text: #f8f8f2;
$color-code-mark-background: $color-light-blue;
$color-code-title-background: #1d2231;
$color-code-title-text: #e0e0e0;
/* --- */
:root {

View file

@ -8,59 +8,61 @@ https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clik
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
color: #f8f8f2;
background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #272822;
background: #272822;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #8292a2;
color: #8292a2;
}
.token.punctuation {
color: #f8f8f2;
color: #f8f8f2;
}
.token.namespace {
opacity: .7;
opacity: .7;
}
.token.property,
@ -68,12 +70,12 @@ pre[class*="language-"] {
.token.constant,
.token.symbol,
.token.deleted {
color: #f92672;
color: #f92672;
}
.token.boolean,
.token.number {
color: #ae81ff;
color: #ae81ff;
}
.token.selector,
@ -82,7 +84,7 @@ pre[class*="language-"] {
.token.char,
.token.builtin,
.token.inserted {
color: #a6e22e;
color: #a6e22e;
}
.token.operator,
@ -91,34 +93,34 @@ pre[class*="language-"] {
.language-css .token.string,
.style .token.string,
.token.variable {
color: #f8f8f2;
color: #f8f8f2;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #e6db74;
color: #e6db74;
}
.token.keyword {
color: #66d9ef;
color: #66d9ef;
}
.token.regex,
.token.important {
color: #fd971f;
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
font-weight: bold;
}
.token.italic {
font-style: italic;
font-style: italic;
}
.token.entity {
cursor: help;
cursor: help;
}

View file

@ -16,35 +16,34 @@
// }
//
.h1, h1 {
font-size: 3.2rem;
line-height: 1.32;
font-size: 3.2rem;
line-height: 1.32
}
.h2, h2 {
font-size: 2.6rem;
line-height: 1.35;
font-size: 2.6rem;
line-height: 1.35
}
.h3, h3 {
font-size: 2.0rem;
line-height: 1.45;
font-size: 2.0rem;
line-height: 1.45
}
.h4, h4 {
font-size: 1.4rem;
line-height: 1.6;
font-size: 1.4rem;
line-height: 1.6
}
.h5, h5 {
font-size: 1.1rem;
line-height: 1.75;
font-size: 1.1rem;
line-height: 1.75
}
.h6, h6 {
font-size: 1.0rem;
line-height: 1.9;
font-size: 1.0rem;
line-height: 1.9
}
//
// p {
// margin-top: 17px;
@ -57,6 +56,7 @@
// }
// }
.h1,
.h2,
.h3,
@ -69,7 +69,8 @@ h3,
h4,
h5,
h6 {
font-weight: 600;
margin-bottom: 1rem;
margin-top: 0;
font-weight: 600;
margin-bottom: 1rem;
margin-top: 0
}

View file

@ -1,10 +1,10 @@
body {
margin: 0;
padding: 0;
overflow: hidden;
margin: 0;
padding: 0;
overflow: hidden;
}
#mesh-viewer {
width: 100vw;
height: 100vh;
width: 100vw;
height: 100vh;
}

View file

@ -1,6 +1,6 @@
import '../../vendor/murph/murph-core/src/core/Resources/assets/js/admin.js'
require('./admin_modules/md-editor')()
require('./admin_modules/simplemde')()
const $ = require('jquery')
const Sortable = require('sortablejs').Sortable

View file

@ -1,34 +0,0 @@
const Vue = require('vue').default
const VueMarkdownEditor = require('@kangc/v-md-editor')
const githubTheme = require('@kangc/v-md-editor/lib/theme/github.js')
const fr = require('@kangc/v-md-editor/lib/lang/fr-FR').default
const hljs = require('highlight.js')
VueMarkdownEditor.use(githubTheme, {Hljs: hljs})
VueMarkdownEditor.lang.use('fr-FR', fr)
Vue.use(VueMarkdownEditor)
module.exports = () => {
const components = document.querySelectorAll('.markdown-editor')
components.forEach((component) => {
return new Vue({
el: component,
template: `
<div>
<textarea :name="name" v-model="value" class="d-none"></textarea>
<v-md-editor v-model="value" mode="edit"></v-md-editor>
</div>
`,
data() {
return {
name: component.getAttribute('data-name'),
value: JSON.parse(component.getAttribute('data-value')),
}
},
components: {
VueMarkdownEditor
}
})
})
}

View file

@ -13,7 +13,8 @@ module.exports = () => {
autoDownloadFontAwesome: false,
spellChecker: false,
styleSelectedText: false,
toolbar: [{
toolbar: [
{
name: 'bold',
action: Editor.toggleBold,
className: 'fa fa-bold',

View file

@ -27,6 +27,6 @@ const app = new App([
new SmallMenu(window)
])
window.addEventListener('load', function() {
window.addEventListener('load', function () {
app.init()
}, false)

View file

@ -1,19 +1,19 @@
class App {
constructor (components) {
this.components = components || []
}
add (c) {
this.components.push(c)
return this
}
init () {
for (let u = 0, x = this.components.length; u < x; u++) {
this.components[u].init()
constructor(components) {
this.components = components || []
}
add(c) {
this.components.push(c)
return this
}
init() {
for (let u = 0, x = this.components.length; u < x; u++) {
this.components[u].init()
}
}
}
}
module.exports = App

View file

@ -22,79 +22,78 @@ require('prismjs/components/prism-shell-session')
require('prismjs/plugins/keep-markup/prism-keep-markup')
require('prismjs/plugins/line-highlight/prism-line-highlight')
require('prismjs/plugins/line-numbers/prism-line-numbers')
require('mermaid')
class Code {
constructor(w) {
this.window = w
}
init() {
Prism.highlightAllUnder(document)
let elements = this.window.document.querySelectorAll('code[data-title], div[data-title]')
for (let i = 0, len = elements.length; i < len; i++) {
const element = elements[i]
if (element.tagName === 'CODE') {
var code = element
var pre = code.parentNode
var post = pre.parentNode
} else {
var code = element.querySelector('code')
if (!code) {
continue
}
var pre = code.parentNode
var post = pre.parentNode
}
if (!pre || !post) {
continue
}
pre.classList.add('with-title')
const title = this.window.document.createElement('div')
title.classList.add('code-title')
title.textContent = element.getAttribute('data-title')
post.insertBefore(title, pre)
constructor(w) {
this.window = w
}
elements = this.window.document.querySelectorAll('code.window')
init() {
Prism.highlightAllUnder(document)
let elements = this.window.document.querySelectorAll('code[data-title], div[data-title]')
for (let i = 0, len = elements.length; i < len; i++) {
const element = elements[i]
for (let i = 0, len = elements.length; i < len; i++) {
const element = elements[i]
if (element.tagName === 'CODE') {
var code = element
var pre = code.parentNode
var post = pre.parentNode
} else {
var code = element.querySelector('code')
if (element.tagName === 'CODE') {
var code = element
var pre = code.parentNode
var post = pre.parentNode
} else {
var code = element.querySelector('code')
if (!code) {
continue
if (!code) {
continue
}
var pre = code.parentNode
var post = pre.parentNode
}
if (!pre || !post) {
continue
}
pre.classList.add('with-title')
const title = this.window.document.createElement('div')
title.classList.add('code-title')
title.textContent = element.getAttribute('data-title')
post.insertBefore(title, pre)
}
var pre = code.parentNode
var post = pre.parentNode
}
elements = this.window.document.querySelectorAll('code.window')
if (!pre || !post) {
continue
}
for (let i = 0, len = elements.length; i < len; i++) {
const element = elements[i]
pre.classList.add('with-title')
const win = this.window.document.createElement('div')
win.classList.add('code-window')
if (element.tagName === 'CODE') {
var code = element
var pre = code.parentNode
var post = pre.parentNode
} else {
var code = element.querySelector('code')
post.insertBefore(win, pre)
if (!code) {
continue
}
var pre = code.parentNode
var post = pre.parentNode
}
if (!pre || !post) {
continue
}
pre.classList.add('with-title')
const win = this.window.document.createElement('div')
win.classList.add('code-window')
post.insertBefore(win, pre)
}
}
}
}
module.exports = Code

View file

@ -1,32 +1,32 @@
const Routing = require('./routing')
class FormPnw {
constructor (w) {
this.window = w
}
constructor(w) {
this.window = w
}
init () {
const doc = this.window.document
init() {
const doc = this.window.document
doc.addEventListener('mousemove', function () {
const forms = doc.querySelectorAll('form[data-form-bot]')
doc.addEventListener('mousemove', function() {
const forms = doc.querySelectorAll('form[data-form-bot]')
for (let i = 0, len = forms.length; i < len; i++) {
const form = forms[i]
let action = form.getAttribute('action')
action = action.replace(
Routing.generate('blog_tech_form_without_javascript', {
_domain: window.location.hostname
}, false) + '?page=', ''
)
for (let i = 0, len = forms.length; i < len; i++) {
const form = forms[i]
let action = form.getAttribute('action')
action = action.replace(
Routing.generate('blog_tech_form_without_javascript', {
_domain: window.location.hostname
}, false) + '?page=', ''
)
action = decodeURIComponent(action)
action = decodeURIComponent(action)
form.setAttribute('action', action)
form.removeAttribute('data-form-bot')
}
})
}
form.setAttribute('action', action)
form.removeAttribute('data-form-bot')
}
})
}
}
module.exports = FormPnw

View file

@ -1,36 +1,36 @@
const Mario = require('../../images/mario.gif')
class Knmc {
constructor (w) {
this.window = w
}
constructor(w) {
this.window = w
}
init () {
let chars = ''
const seq = '38384040373937396665'
const body = this.window.document.querySelector('body')
init() {
let chars = ''
const seq = '38384040373937396665'
const body = this.window.document.querySelector('body')
body.addEventListener('keyup', function (e) {
chars += e.keyCode.toString()
body.addEventListener('keyup', function(e) {
chars += e.keyCode.toString()
if (chars.indexOf(seq) !== -1) {
chars = ''
const url = Mario
const image = new Image()
image.classList.add('fixed')
image.style.position = 'fixed'
image.style.bottom = '-11px'
image.style.left = '-256px'
if (chars.indexOf(seq) !== -1) {
chars = ''
const url = Mario
const image = new Image()
image.classList.add('fixed')
image.style.position = 'fixed'
image.style.bottom = '-11px'
image.style.left = '-256px'
image.onload = function () {
image.classList.add('knmc')
body.appendChild(image)
}
image.onload = function() {
image.classList.add('knmc')
body.appendChild(image)
}
image.src = url
}
})
}
image.src = url
}
})
}
}
module.exports = Knmc

View file

@ -1,10 +1,10 @@
const lozad = require('lozad')
class LazyLoad {
init () {
const observer = lozad('.lazy-img')
observer.observe()
}
init() {
const observer = lozad('.lazy-img')
observer.observe()
}
}
module.exports = LazyLoad

View file

@ -1,36 +1,36 @@
const tingle = require('tingle.js/src/tingle.js')
class MeshViewer {
constructor(w) {
this.window = w
}
init() {
const openers = this.window.document.querySelectorAll('*[data-modal]')
for (let i = 0, len = openers.length; i < len; i++) {
openers[i].addEventListener('click', (e) => {
e.preventDefault()
let target = e.target
if (target.tagName != 'A') {
target = target.parentNode
}
const modal = new tingle.modal({
footer: false,
stickyFooter: false,
closeMethods: ['overlay', 'button', 'escape'],
closeLabel: 'Close',
cssClass: ['tingle-modal-box--mesh']
})
modal.setContent('<iframe src="' + target.getAttribute('href') + '"></iframe>')
modal.open()
})
constructor(w) {
this.window = w
}
init() {
const openers = this.window.document.querySelectorAll('*[data-modal]')
for (let i = 0, len = openers.length; i < len; i++) {
openers[i].addEventListener('click', (e) => {
e.preventDefault()
let target = e.target
if (target.tagName != 'A') {
target = target.parentNode
}
const modal = new tingle.modal({
footer: false,
stickyFooter: false,
closeMethods: ['overlay', 'button', 'escape'],
closeLabel: 'Close',
cssClass: ['tingle-modal-box--mesh']
})
modal.setContent('<iframe src="' + target.getAttribute('href') + '"></iframe>')
modal.open()
})
}
}
}
}
module.exports = MeshViewer

View file

@ -1,47 +1,47 @@
require('particles.js')
class Particles {
constructor(w) {
this.window = w
}
start() {
if (this.window.innerWidth < 708) {
return
constructor(w) {
this.window = w
}
const height = this.header.offsetHeight
const width = this.header.offsetWidth
start() {
if (this.window.innerWidth < 708) {
return
}
this.particles.style.maxHeight = height + 'px'
this.particles.style.maxWidth = width + 'px'
const height = this.header.offsetHeight
const width = this.header.offsetWidth
particlesJS.load('particles', '/js/particles.json?v=3')
}
this.particles.style.maxHeight = height + 'px'
this.particles.style.maxWidth = width + 'px'
clean() {
this.particles.innerHTML = ''
}
init() {
this.particles = this.window.document.getElementById('particles')
if (!this.particles) {
return
particlesJS.load('particles', '/js/particles.json?v=3')
}
this.header = this.particles.parentNode
clean() {
this.particles.innerHTML = ''
}
this.clean()
this.start()
init() {
this.particles = this.window.document.getElementById('particles')
const that = this
if (!this.particles) {
return
}
this.window.addEventListener('resize', function() {
that.clean()
that.start()
}, false)
}
this.header = this.particles.parentNode
this.clean()
this.start()
const that = this
this.window.addEventListener('resize', function() {
that.clean()
that.start()
}, false)
}
}
module.exports = Particles

View file

@ -1,122 +1,122 @@
const Routing = require('./routing')
class Post {
constructor (w) {
this.window = w
}
commentsEvents () {
const document = this.window.document
const parentCommentIdField = document.getElementById('user_comment_parentCommentId')
if (!parentCommentIdField) {
return
constructor(w) {
this.window = w
}
const isAnswerAlert = document.getElementById('answer-alert')
const cancelAnswerButton = document.getElementById('cancel-answer')
commentsEvents() {
const document = this.window.document
const toogleAnswerAlert = function () {
if (parentCommentIdField.value) {
isAnswerAlert.classList.remove('hidden')
} else {
isAnswerAlert.classList.add('hidden')
}
}
const parentCommentIdField = document.getElementById('user_comment_parentCommentId')
toogleAnswerAlert()
if (!parentCommentIdField) {
return
}
const answerButtons = document.querySelectorAll('a[data-answer]')
const isAnswerAlert = document.getElementById('answer-alert')
const cancelAnswerButton = document.getElementById('cancel-answer')
const toogleAnswerAlert = function() {
if (parentCommentIdField.value) {
isAnswerAlert.classList.remove('hidden')
} else {
isAnswerAlert.classList.add('hidden')
}
}
for (let i = 0, len = answerButtons.length; i < len; i++) {
answerButtons[i].addEventListener('click', function (e) {
parentCommentIdField.value = e.target.getAttribute('data-answer')
toogleAnswerAlert()
}, false)
}
cancelAnswerButton.addEventListener('click', function (e) {
e.preventDefault()
const answerButtons = document.querySelectorAll('a[data-answer]')
parentCommentIdField.value = null
toogleAnswerAlert()
}, false)
const previewButton = document.querySelector('.preview-button')
const previewRender = document.querySelector('#preview')
previewButton.addEventListener('click', function () {
if (previewRender.innerHTML === '') {
previewRender.innerHTML = '<p>Chargement en cours…</p>'
}
const content = document.querySelector('#user_comment_content').value
const httpRequest = new XMLHttpRequest()
httpRequest.onreadystatechange = function (data) {
if (httpRequest.readyState === 4 && httpRequest.status === 200) {
const json = JSON.parse(httpRequest.response)
previewRender.innerHTML = json.render
document.location.href = '#preview'
for (let i = 0, len = answerButtons.length; i < len; i++) {
answerButtons[i].addEventListener('click', function(e) {
parentCommentIdField.value = e.target.getAttribute('data-answer')
toogleAnswerAlert()
}, false)
}
}
httpRequest.open('POST', Routing.generate('api_blog_comment_preview'))
httpRequest.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded'
)
httpRequest.send('content=' + encodeURIComponent(content))
}, false)
}
cancelAnswerButton.addEventListener('click', function(e) {
e.preventDefault()
imagesEvents () {
const document = this.window.document
let isFullscreen = false
const images = document.querySelectorAll('.body img')
const handleClick = function (image) {
if (isFullscreen) {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
}
} else {
if (image.requestFullscreen) {
image.requestFullscreen()
} else if (image.webkitRequestFullscreen) {
image.webkitRequestFullscreen()
} else if (image.mozRequestFullScreen) {
image.mozRequestFullScreen()
}
}
isFullscreen = !isFullscreen
}
for (let i = 0, len = images.length; i < len; i++) {
const image = images[i]
if (image.parentNode.tagName === 'A') {
continue
}
(function (i) {
i.addEventListener('click', function () {
handleClick(i)
parentCommentIdField.value = null
toogleAnswerAlert()
}, false)
})(image)
}
}
init () {
this.commentsEvents()
this.imagesEvents()
}
const previewButton = document.querySelector('.preview-button')
const previewRender = document.querySelector('#preview')
previewButton.addEventListener('click', function() {
if (previewRender.innerHTML === '') {
previewRender.innerHTML = '<p>Chargement en cours…</p>'
}
const content = document.querySelector('#user_comment_content').value
const httpRequest = new XMLHttpRequest()
httpRequest.onreadystatechange = function(data) {
if (httpRequest.readyState === 4 && httpRequest.status === 200) {
const json = JSON.parse(httpRequest.response)
previewRender.innerHTML = json.render
document.location.href = '#preview'
}
}
httpRequest.open('POST', Routing.generate('api_blog_comment_preview'))
httpRequest.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded'
)
httpRequest.send('content=' + encodeURIComponent(content))
}, false)
}
imagesEvents() {
const document = this.window.document
let isFullscreen = false
const images = document.querySelectorAll('.body img')
const handleClick = function(image) {
if (isFullscreen) {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
}
} else {
if (image.requestFullscreen) {
image.requestFullscreen()
} else if (image.webkitRequestFullscreen) {
image.webkitRequestFullscreen()
} else if (image.mozRequestFullScreen) {
image.mozRequestFullScreen()
}
}
isFullscreen = !isFullscreen
}
for (let i = 0, len = images.length; i < len; i++) {
const image = images[i]
if (image.parentNode.tagName === 'A') {
continue
}
(function(i) {
i.addEventListener('click', function() {
handleClick(i)
}, false)
})(image)
}
}
init() {
this.commentsEvents()
this.imagesEvents()
}
}
module.exports = Post

View file

@ -1,34 +1,34 @@
class PxImage {
constructor (w) {
this.window = w
}
init () {
const doc = this.window.document
const images = doc.querySelectorAll('.quick-image img, .card figure img')
for (let i = 0, len = images.length; i < len; i++) {
((image) => {
const source = image.getAttribute('data-src')
const sourceError = image.getAttribute('data-src-error')
const color = image.getAttribute('data-color')
const loader = new Image()
loader.onload = () => {
image.style.background = `${color || null} url(${source})`
image.style.backgroundSize = 'cover'
image.style.backgroundPosition = 'center'
}
loader.onerror = () => {
image.style.background = `${color || null} url('${sourceError}') center center`
}
loader.src = source
})(images[i])
constructor(w) {
this.window = w
}
init() {
const doc = this.window.document
const images = doc.querySelectorAll('.quick-image img, .card figure img')
for (let i = 0, len = images.length; i < len; i++) {
((image) => {
const source = image.getAttribute('data-src')
const sourceError = image.getAttribute('data-src-error')
const color = image.getAttribute('data-color')
const loader = new Image()
loader.onload = () => {
image.style.background = `${color ? color : null} url(${source})`
image.style.backgroundSize = 'cover'
image.style.backgroundPosition = 'center'
}
loader.onerror = () => {
image.style.background = `${color ? color : null} url('${sourceError}') center center`
}
loader.src = source
})(images[i])
}
}
}
}
module.exports = PxImage

View file

@ -1,24 +1,24 @@
const Routing = require('./routing')
class SmallMenu {
constructor(w) {
this.window = w
}
constructor(w) {
this.window = w
}
addEvent() {
const document = this.window.document
const menu = document.querySelector('.small-menu')
const opener = document.querySelector('.menu-opener')
addEvent() {
const document = this.window.document
const menu = document.querySelector('.small-menu')
const opener = document.querySelector('.menu-opener')
opener.addEventListener('click', () => {
menu.classList.toggle('is-open')
opener.classList.toggle('is-open')
})
}
opener.addEventListener('click', () => {
menu.classList.toggle('is-open')
opener.classList.toggle('is-open')
})
}
init() {
this.addEvent()
}
init() {
this.addEvent()
}
}
module.exports = SmallMenu

View file

@ -1,19 +1,19 @@
class Stats {
init() {
(function(f, a, t, h, o, m) {
a[h] = a[h] || function() {
(a[h].q = a[h].q || []).push(arguments)
}
o = f.createElement('script'),
m = f.getElementsByTagName('script')[0]
o.async = 1;
o.src = t;
o.id = 'fathom-script'
m.parentNode.insertBefore(o, m)
})(document, window, '//ftm.deblan.org/tracker.js', 'fathom')
fathom('set', 'siteId', 'HQAWS')
fathom('trackPageview')
}
init() {
(function(f, a, t, h, o, m) {
a[h] = a[h] || function() {
(a[h].q = a[h].q || []).push(arguments)
}
o = f.createElement('script'),
m = f.getElementsByTagName('script')[0]
o.async = 1;
o.src = t;
o.id = 'fathom-script'
m.parentNode.insertBefore(o, m)
})(document, window, '//ftm.deblan.org/tracker.js', 'fathom')
fathom('set', 'siteId', 'HQAWS')
fathom('trackPageview')
}
}
module.exports = Stats

View file

@ -1,15 +1,15 @@
class VideoRatio {
constructor (w) {
this.window = w
}
init () {
const videos = this.window.document.querySelectorAll('.video-ratio')
for (let i = 0, len = videos.length; i < len; i++) {
videos[i].style.paddingBottom = videos[i].getAttribute('data-ratio')
constructor(w) {
this.window = w
}
init() {
const videos = this.window.document.querySelectorAll('.video-ratio')
for (let i = 0, len = videos.length; i < len; i++) {
videos[i].style.paddingBottom = videos[i].getAttribute('data-ratio')
}
}
}
}
module.exports = VideoRatio

View file

@ -2,11 +2,10 @@ import '../css/viewer.scss'
const container = document.getElementById('mesh-viewer')
const viewer = new StlViewer(
container, {
container,
{
auto_rotate: true,
allow_drag_and_drop: true,
models: [{
filename: container.getAttribute('data-file')
}]
models: [{ filename: container.getAttribute('data-file') }]
}
)

View file

@ -1,129 +0,0 @@
#!/bin/sh
set -eu
usage() {
printf "Usage: %s [-l DEBUG_LEVEL] [-h] start|stop|restart\n" "$0"
}
help() {
cat << EOH
SYNOPSIS
$0 [-l DEBUG_LEVEL] [-h] -a start|stop|restart
DESCRIPTION
$0 manages symfony messenger
OPTIONS
-h Show this help
-l debug|info|notice|warning|error
Debug level
-a start|stop|restart|status
EOH
}
on_interrupt() {
log -l notice ""
log -l notice "Process aborted!"
exit 130
}
start_messenger() {
nohup php8.1 bin/console messenger:consume 2>/dev/null >/dev/null &
log -t -l notice "Started"
}
stop_messenger() {
php8.1 bin/console messenger:stop-workers 2>/dev/null >/dev/null
log -t -l notice "Stopped"
}
get_pid() {
pgrep -f messenger:consume
}
main() {
cd "$(dirname "0")"
ACTION=
while getopts "l:ha:" option; do
case "${option}" in
h) help; exit 0;;
l) LOG_VERBOSE="$OPTARG";;
a) ACTION="$OPTARG";;
?) log -l error "$(usage)"; exit 1;;
esac
done
if [ "$ACTION" = "start" ]; then
start_messenger
elif [ "$ACTION" = "stop" ]; then
stop_messenger
elif [ "$ACTION" = "restart" ]; then
stop_messenger
start_messenger
elif [ "$ACTION" = "status" ]; then
get_pid
else
log -l error "Action is required."
fi
exit 0
}
log() {
LOG_VERBOSE="${LOG_VERBOSE:-info}"
LEVEL=info
TIME=
while getopts "tl:" option; do
case "${option}" in
l) LEVEL="$OPTARG"; shift $((OPTIND-1));;
t) TIME="$(printf "[%s] " "$(date +'%Y-%m-%dT%H:%M:%S.%s')")"; shift $((OPTIND-1));;
*) exit 1;;
esac
done
if [ -t 2 ] && [ -z "${NO_COLOR-}" ]; then
case "${LEVEL}" in
debug) COLOR="$(tput setaf 3)";;
notice) COLOR="$(tput setaf 4)";;
warning) COLOR="$(tput setaf 5)";;
error) COLOR="$(tput setaf 1)";;
*) COLOR="$(tput sgr0)";;
esac
fi
case "${LEVEL}" in
debug) LEVEL=100;;
notice) LEVEL=250;;
warning) LEVEL=300;;
error) LEVEL=400;;
*) LEVEL=200;;
esac
case "${LOG_VERBOSE}" in
debug) LOG_VERBOSE_VALUE=100;;
notice) LOG_VERBOSE_VALUE=250;;
warning) LOG_VERBOSE_VALUE=300;;
error) LOG_VERBOSE_VALUE=400;;
*) LOG_VERBOSE_VALUE=200;;
esac
if [ $LEVEL -ge $LOG_VERBOSE_VALUE ]; then
printf "%s\n" "$*" | while IFS='' read -r LINE; do
printf "%s%s%s\n" "${COLOR:-}" "${TIME:-}" "$LINE" >&2
done
fi
}
trap on_interrupt INT
main "$@"

View file

@ -7,13 +7,11 @@
"php": ">=8.0.0",
"beberlei/doctrineextensions": "^1.3",
"friendsofsymfony/jsrouting-bundle": "^2.7",
"fundevogel/php-mastodon": "^0.6.0",
"gregwar/captcha-bundle": "^2.2",
"guzzlehttp/guzzle": "^7.8",
"influxdata/influxdb-client-php": "^3.4",
"knplabs/knp-markdown-bundle": "^1.9",
"knplabs/knp-menu-bundle": "^3.1",
"murph/murph-core": "dev-master",
"symfony/messenger": "5.4.*",
"twig/intl-extra": "^3.5"
},
"require-dev": {
@ -34,8 +32,7 @@
"sort-packages": true,
"allow-plugins": {
"symfony/flex": true,
"symfony/runtime": true,
"php-http/discovery": true
"symfony/runtime": true
}
},
"autoload": {

View file

@ -13,21 +13,18 @@ liip_imagine:
max: [600, 600]
crop:
size: [600, 270]
start: [0, 0]
project_preview_filter:
filters:
downscale:
max: [600, 600]
crop:
size: [600, 270]
start: [0, 0]
post_preview_filter:
filters:
downscale:
max: [600, 600]
crop:
size: [600, 300]
start: [0, 0]
site_avatar:
filters:
downscale:

View file

@ -1,7 +0,0 @@
framework:
messenger:
transports:
async: "%env(MESSENGER_TRANSPORT_DSN)%"
routing:
'App\Message\PageViewMessage': async

View file

@ -1,6 +1,6 @@
twig:
default_path: '%kernel.project_dir%/templates'
form_themes: ['form/bootstrap_4_form_theme.html.twig']
form_themes: ['@Core/form/bootstrap_4_form_theme.html.twig']
auto_reload: true
paths:
'%kernel.project_dir%/templates/core/': Core

View file

@ -4,11 +4,10 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
influxdb_url: '%env(INFLUXDB_URL)%'
influxdb_token: '%env(INFLUXDB_TOKEN)%'
influxdb_bucket: '%env(INFLUXDB_BUCKET)%'
influxdb_org: '%env(INFLUXDB_ORG)%'
influxdb_debug: '%env(INFLUXDB_DEBUG)%'
mastodon_host: '%env(MASTODON_HOST)%'
mastodon_client_id: '%env(MASTODON_CLIENT_ID)%'
mastodon_client_secret: '%env(MASTODON_CLIENT_SECRET)%'
mastodon_client_access_token: '%env(MASTODON_CLIENT_ACCESS_TOKEN)%'
services:
# default configuration for services in *this* file
@ -52,13 +51,12 @@ services:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
App\Api\InfluxDB:
App\Api\MastodonClient:
arguments:
$url: '%influxdb_url%'
$token: '%influxdb_token%'
$bucket: '%influxdb_bucket%'
$org: '%influxdb_org%'
$debug: '%influxdb_debug%'
$host: '%mastodon_host%'
$clientId: '%mastodon_client_id%'
$clientSecret: '%mastodon_client_secret%'
$clientAccessToken: '%mastodon_client_access_token%'
site.route_loader:
class: App\Core\Router\SiteRouteLoader
@ -82,9 +80,5 @@ services:
tags:
- {name: markdown.parser, alias: comment}
App\EventListener\StatListener:
tags:
- { name: kernel.event_listener, event: kernel.request }
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

29948
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -8,16 +8,13 @@
"build": "./node_modules/.bin/encore production"
},
"dependencies": {
"@kangc/v-md-editor": "^1.7.12",
"daisyui": "^2.31.0",
"editorjs-hyperlink": "^1.0.6",
"editorjs-inline-tool": "^0.4.0",
"encore": "^0.0.30-beta",
"lozad": "^1.16.0",
"mermaid": "^11.0.2",
"murph-project": "^1.9.4",
"murph-project": "^1",
"particles.js": "^2.0.0",
"prismjs": "^1.23.0",
"simplemde": "^1.11.2",
"tingle.js": "^0.16.0",
"vanillajs-datepicker": "^1.1.4",
"vue": "^2.6.14"
@ -28,6 +25,5 @@
"postcss": "^8.4.16",
"postcss-loader": "^7.0.1",
"tailwindcss": "^3.1.8"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}

View file

@ -29,7 +29,7 @@ class DateRangeAnalytic extends BaseDateRangeAnalytic
foreach ($entities as $key => $entity) {
if ('view' === $type) {
if (null === $this->path || str_starts_with($entity->getPath(), $this->path)) {
if ($this->path === null || str_starts_with($entity->getPath(), $this->path)) {
$newEntities[] = $entity;
}
}

View file

@ -1,46 +0,0 @@
<?php
namespace App\Api;
use InfluxDB2\Client;
use InfluxDB2\Model\WritePrecision;
/**
* class InfluxDB.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class InfluxDB
{
protected ?Client $client = null;
public function __construct(
protected ?string $url,
protected ?string $token,
protected ?string $bucket,
protected ?string $org,
protected bool $debug = false
) {
if (isset($this->url, $this->token, $this->bucket, $this->org)) {
$this->client = new Client([
'url' => $this->url,
'token' => $this->token,
'bucket' => $this->bucket,
'org' => $this->org,
'debug' => $this->debug,
'precision' => WritePrecision::S,
'timeout' => 1,
]);
}
}
public function isAvailable(): bool
{
return null !== $this->getClient();
}
public function getClient(): ?Client
{
return $this->client;
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace App\Api;
use Fundevogel\Mastodon\Api as Client;
/**
* class MastodonClient.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class MastodonClient
{
protected ?Client $client = null;
public function __construct(
protected ?string $host,
protected ?string $clientId,
protected ?string $clientSecret,
protected ?string $clientAccessToken
) {
}
public function __call(string $name, array $arguments)
{
if (!$this->client) {
$this->createClient();
}
return call_user_func_array([$this->client, $name], $arguments);
}
protected function createClient(): self
{
$this->client = new Client($this->host);
$this->client->accessToken = $this->clientAccessToken;
return $this;
}
public function extractId(string $url): string
{
return basename($url);
}
public function getTree(string $postId, array &$tree = []): array
{
$tree['id'] = $postId;
$tree = array_merge($tree, $this->statuses()->get($postId)->data);
$tree = array_merge($tree, $this->statuses()->context($postId)->data);
foreach ($tree['descendants'] as $key => $descendant) {
if ($descendant['in_reply_to_id'] === $postId) {
$descendants['descendants'][$key] = $this->getTree($descendant['id'], $descendants['descendants'][$key]);
}
}
return $tree;
}
}

View file

@ -14,7 +14,7 @@ class TTRssClient
$result = @file_get_contents('https://tiny.deblan.org/deblan_api/?itemsPerPage=10&page='.$page);
if ($result) {
$result = str_replace('\u0092', "'", $result);
$result = str_replace('\\u0092', "'", $result);
$result = str_replace('&#039;', "'", $result);
return json_decode($result, true);

View file

@ -0,0 +1,49 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use App\Api\MastodonClient;
use App\Core\Manager\EntityManager;
use App\Repository\Blog\PostRepositoryQuery;
#[AsCommand(
name: 'app:mastodon:comments-sync',
description: 'Add a short description for your command',
)]
class MastodonCommentsSyncCommand extends Command
{
public function __construct(
protected MastodonClient $client,
protected PostRepositoryQuery $query,
protected EntityManager $manager
)
{
parent::__construct();
}
protected function configure(): void
{
// $this
// ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description')
// ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description')
// ;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
foreach ($this->query->create()->where('.mastodonUrl is not null')->find() as $post) {
dump($this->client->getTree($this->client->extractId($post->getMastodonUrl())));
}
return Command::SUCCESS;
}
}

View file

@ -2,30 +2,31 @@
namespace App\Controller\Blog;
use App\Analytic\DateRangeAnalytic;
use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field\DatetimeField;
use App\Core\Crud\Field\TextField;
use App\Core\Entity\EntityInterface;
use App\Core\Form\FileUploadHandler;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;
use App\Entity\Blog\Post;
use App\Entity\Blog\Post as Entity;
use App\Factory\Blog\PostFactory as EntityFactory;
use App\Form\Blog\Filter\PostFilterType;
use App\Form\Blog\PostType;
use App\Form\Blog\PostType as EntityType;
use App\Repository\Blog\PostRepositoryQuery as RepositoryQuery;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\Form;
use App\Core\Entity\EntityInterface;
use App\Entity\Blog\Post;
use App\Analytic\DateRangeAnalytic;
use App\Core\Repository\Site\NodeRepository;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
#[Route(path: '/admin/blog/post')]
class PostAdminController extends CrudController
@ -67,7 +68,7 @@ class PostAdminController extends CrudController
'attr' => ['class' => 'miw-400'],
])
->setField('index', 'ID', TextField::class, [
'property_builder' => function (EntityInterface $entity) {
'property_builder' => function(EntityInterface $entity) {
return sprintf('#%d', $entity->getId());
},
'sort' => ['id', '.id'],
@ -84,7 +85,7 @@ class PostAdminController extends CrudController
'format' => 'd/m/Y H:i',
'sort' => ['publishedAt', '.publishedAt'],
'attr' => ['class' => 'miw-200'],
'inline_form' => function (FormBuilderInterface $builder) {
'inline_form' => function(FormBuilderInterface $builder) {
$builder->add(
'publishedAt',
DateTimeType::class,
@ -106,7 +107,7 @@ class PostAdminController extends CrudController
'view' => 'blog/post_admin/field/status.html.twig',
'sort' => ['status', '.status'],
'attr' => ['class' => 'miw-100'],
'inline_form' => function (FormBuilderInterface $builder) {
'inline_form' => function(FormBuilderInterface $builder) {
$builder->add(
'status',
ChoiceType::class,
@ -124,15 +125,15 @@ class PostAdminController extends CrudController
],
]
);
},
}
])
->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
$manager->delete($entity);
})
->setBatchAction('index', 'draft', 'Statut : publier', function (EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'draft', 'Statut : publier', function(EntityInterface $entity, EntityManager $manager) {
$manager->update($entity->setStatus(Post::PUBLISHED));
})
->setBatchAction('index', 'publish', 'Statut : brouillon', function (EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'publish', 'Statut : brouillon', function(EntityInterface $entity, EntityManager $manager) {
$manager->update($entity->setStatus(Post::DRAFT));
})
;
@ -151,7 +152,7 @@ class PostAdminController extends CrudController
$factory->create($this->getUser()),
$entityManager,
$request,
function (Entity $entity, Form $form, Request $request) use ($fileUpload) {
function(Entity $entity, Form $form, Request $request) use ($fileUpload) {
$directory = 'uploads/post/'.date('Y');
$fileUpload->handleForm(
@ -172,7 +173,7 @@ class PostAdminController extends CrudController
$entity,
$entityManager,
$request,
function (Entity $entity, Form $form, Request $request) use ($fileUpload) {
function(Entity $entity, Form $form, Request $request) use ($fileUpload) {
$directory = 'uploads/post/'.date('Y');
$fileUpload->handleForm(
@ -186,7 +187,7 @@ class PostAdminController extends CrudController
);
}
#[Route(path: '/inline_edit/{entity}/{context}/{label}', name: 'admin_blog_post_inline_edit', methods: ['GET', 'POST'])]
#[Route(path: "/inline_edit/{entity}/{context}/{label}", name: 'admin_blog_post_inline_edit', methods: ['GET', 'POST'])]
public function inlineEdit(string $context, string $label, Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doInlineEdit($context, $label, $entity, $entityManager, $request);
@ -267,7 +268,8 @@ class PostAdminController extends CrudController
DateRangeAnalytic $analytic,
NodeRepository $nodeRepository,
string $range = '7days'
): Response {
): Response
{
if (!in_array($range, ['7days', '30days', '90days', '1year'])) {
throw $this->createNotFoundException();
}

View file

@ -7,19 +7,19 @@ use App\Core\Controller\Site\PageController;
use App\Core\Manager\EntityManager;
use App\Core\Site\SiteRequest;
use App\Core\Site\SiteStore;
use App\Core\Twig\Extension\BuilderExtension;
use App\Core\Twig\Extension\EditorJsExtension;
use App\Entity\Blog\Category;
use App\Entity\Blog\Post;
use App\Factory\Blog\CommentFactory;
use App\Form\Blog\UserCommentType;
use App\Manager\PostFollowManager;
use App\Markdown\Parser\Post as PostParser;
use App\Repository\Blog\PostRepositoryQuery;
use App\UrlGenerator\PostGenerator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use App\Factory\Blog\PostFollowFactory;
use App\Manager\PostFollowManager;
use App\Core\Twig\Extension\EditorJsExtension;
class PostController extends PageController
{
@ -149,13 +149,14 @@ class PostController extends PageController
]);
}
public function tag(string $tag, int $page = 1): Response {}
public function tag(string $tag, int $page = 1): Response
{
}
public function createQuery(bool $isPreview = false): PostRepositoryQuery
{
$query = $this->postQuery->create()
->orderBy('.publishedAt', 'DESC')
;
->orderBy('.publishedAt', 'DESC');
if (!$isPreview) {
$query->published();
@ -164,19 +165,14 @@ class PostController extends PageController
return $query;
}
public function rss(
PostParser $parser,
EditorJsExtension $editorJsExtension,
BuilderExtension $builderExtension
): Response {
public function rss(PostParser $parser, EditorJsExtension $editorJsExtension): Response
{
$entities = $this->createQuery()->paginate(1, 20);
$items = [];
foreach ($entities as $entity) {
if ('editorjs' === $entity->getContentFormat()) {
if ($entity->getContentFormat() === 'editorjs') {
$description = $editorJsExtension->buildHtml($entity->getContent());
} elseif ('builder' === $entity->getContentFormat()) {
$description = $builderExtension->buildHtml($entity->getContent());
} else {
$description = $parser->transformMarkdown($entity->getContent());
}
@ -197,7 +193,7 @@ class PostController extends PageController
'post' => $entity->getId(),
'slug' => $entity->getSlug(),
], UrlGeneratorInterface::ABSOLUTE_URL),
'linkGemini' => sprintf('gemini://deblan.fr/posts/%d.gmi', $entity->getId()),
'linkGemini' => sprintf('gemini://deblan.io/posts/%d.gmi', $entity->getId()),
];
}

View file

@ -4,10 +4,10 @@ namespace App\Controller;
use App\Core\Controller\Site\PageController;
use App\Core\Notification\MailNotifier;
use App\Core\Setting\SettingManager;
use App\Form\ContactType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Core\Setting\SettingManager;
class ContactController extends PageController
{

View file

@ -3,10 +3,10 @@
namespace App\Controller;
use App\Core\Controller\Dashboard\DashboardAdminController as Controller;
use App\Repository\Blog\PostRepositoryQuery;
use App\Repository\ProjectRepositoryQuery;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\Blog\PostRepositoryQuery;
use App\Repository\ProjectRepositoryQuery;
#[Route(path: '/admin')]
class DashboardAdminController extends Controller
@ -15,7 +15,8 @@ class DashboardAdminController extends Controller
public function index(
PostRepositoryQuery $postQuery,
ProjectRepositoryQuery $projectQuery
): Response {
): Response
{
$posts = $postQuery->create()
->orderBy('.id', 'DESC')
->paginate(1, 4)

View file

@ -7,7 +7,6 @@ use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field;
use App\Core\Entity\EntityInterface;
use App\Core\Manager\EntityManager;
use App\Entity\Project;
use App\Entity\Project as Entity;
use App\Factory\ProjectFactory as Factory;
use App\Form\ProjectType as Type;
@ -16,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\Project;
class ProjectAdminController extends CrudController
{
@ -114,13 +114,13 @@ class ProjectAdminController extends CrudController
'attr' => ['class' => 'miw-100'],
])
->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
$manager->delete($entity);
})
->setBatchAction('index', 'draft', 'Statut : publier', function (EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'draft', 'Statut : publier', function(EntityInterface $entity, EntityManager $manager) {
$manager->update($entity->setStatus(Project::PUBLISHED));
})
->setBatchAction('index', 'publish', 'Statut : brouillon', function (EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'publish', 'Statut : brouillon', function(EntityInterface $entity, EntityManager $manager) {
$manager->update($entity->setStatus(Project::DRAFT));
})
;

View file

@ -110,7 +110,7 @@ class StlMeshAdminController extends CrudController
'attr' => ['class' => 'col-md-12'],
])
->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
$manager->delete($entity);
})
;

View file

@ -2,11 +2,13 @@
namespace App\Controller;
use App\Api\TTRssClient;
use App\Core\Controller\Site\PageController;
use App\Entity\StlMesh;
use App\Repository\StlMeshRepositoryQuery;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use App\Markdown\Parser\Post as PostParser;
use Symfony\Component\HttpFoundation\Response;
use App\Repository\StlMeshRepositoryQuery;
use App\Entity\StlMesh;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Annotation\Route;
@ -16,8 +18,7 @@ class StlMeshController extends PageController
{
$pager = $query->create()
->orderBy('.sortOrder')
->paginate(1, 200)
;
->paginate(1, 200);
return $this->defaultRender($this->siteRequest->getPage()->getTemplate(), [
'pager' => $pager,

View file

@ -2,7 +2,9 @@
namespace App\Controller;
use App\Api\TTRssClient;
use App\Core\Controller\Site\PageController;
use App\Markdown\Parser\Post as PostParser;
use Symfony\Component\HttpFoundation\Response;
class TextController extends PageController

View file

@ -1,79 +0,0 @@
<?php
namespace App\Controller;
use App\Core\Controller\User\UserAdminController as BaseUserAdminController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Factory\UserFactory as Factory;
use App\Core\Manager\EntityManager;
use App\Core\Security\TokenGenerator;
use App\Entity\User as Entity;
use App\Repository\UserRepositoryQuery as RepositoryQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class UserAdminController extends BaseUserAdminController
{
#[Route(path: '/admin/user/{page}', name: 'admin_user_index', methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return parent::index($query, $request, $session, $page);
}
#[Route(path: '/admin/user/new', name: 'admin_user_new', methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request, TokenGenerator $tokenGenerator): Response
{
return parent::new($factory, $entityManager, $request, $tokenGenerator);
}
#[Route(path: '/admin/user/show/{entity}', name: 'admin_user_show', methods: ['GET'])]
public function show(Entity $entity): Response
{
return parent::show($entity);
}
#[Route(path: '/admin/user/filter', name: 'admin_user_filter', methods: ['GET'])]
public function filter(Session $session): Response
{
return parent::filter($session);
}
#[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::edit($entity, $entityManager, $request);
}
#[Route(path: '/admin/user/inline_edit/{entity}/{context}/{label}', name: 'admin_user_inline_edit', methods: ['GET', 'POST'])]
public function inlineEdit(string $context, string $label, Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::inlineEdit($context, $label, $entity, $entityManager, $request);
}
#[Route(path: '/admin/user/delete/{entity}', name: 'admin_user_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::delete($entity, $entityManager, $request);
}
#[Route(path: '/admin/user/resetting_request/{entity}', name: 'admin_user_resetting_request', methods: ['POST'])]
public function requestResetting(Entity $entity, EventDispatcherInterface $eventDispatcher, Request $request): Response
{
return parent::requestResetting($entity, $eventDispatcher, $request);
}
protected function getConfiguration(): CrudConfiguration
{
if ($this->configuration) {
return $this->configuration;
}
return parent::getConfiguration()
->setView('form', 'admin/user_admin/_form.html.twig')
->setView('show_entity', 'admin/user_admin/_show.html.twig')
;
}
}

View file

@ -7,6 +7,9 @@ use Symfony\Component\DependencyInjection\Extension\Extension;
class AppExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = $this->getConfiguration($configs, $container);
@ -15,6 +18,9 @@ class AppExtension extends Extension
$container->setParameter('app', $config);
}
/**
* {@inheritdoc}
*/
public function getConfiguration(array $configs, ContainerBuilder $container)
{
return new Configuration();

View file

@ -55,16 +55,6 @@ class Comment implements EntityInterface
$this->postFollows = new ArrayCollection();
}
public function __toString()
{
return sprintf(
'[%s] (%s) %s',
$this->getAuthor(),
$this->getCreatedAt()->format('d/m/Y'),
substr($this->getContent(), 0, 20).'…'
);
}
public function getId(): ?int
{
return $this->id;
@ -199,12 +189,25 @@ class Comment implements EntityInterface
*/
public function getAvatar(): string
{
$mail = $this->getEmail() ?? sprintf('%d@deblan.fr', $this->getId());
$mail = $this->getEmail() ?? sprintf('%d@deblan.io', $this->getId());
$hash = md5($mail);
return 'https://cdn.libravatar.org/avatar/'.$hash.'?s=90&d=retro';
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return sprintf(
'[%s] (%s) %s',
$this->getAuthor(),
$this->getCreatedAt()->format('d/m/Y'),
substr($this->getContent(), 0, 20).'…'
);
}
/**
* @return Collection|PostFollow[]
*/

View file

@ -90,18 +90,17 @@ class Post implements EntityInterface
#[ORM\Column(type: 'array')]
private $parameters = [];
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'deprecatedPosts')]
private $recommandedPost;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private $mastodonUrl;
#[ORM\OneToMany(mappedBy: 'recommandedPost', targetEntity: self::class)]
private $deprecatedPosts;
#[ORM\Column(type: 'json')]
private $mastodonComments = [];
public function __construct()
{
$this->categories = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->postFollows = new ArrayCollection();
$this->deprecatedPosts = new ArrayCollection();
}
public function getId(): ?int
@ -202,7 +201,7 @@ class Post implements EntityInterface
}
/**
* @return Category[]|Collection
* @return Collection|Category[]
*/
public function getCategories(): Collection
{
@ -483,44 +482,26 @@ class Post implements EntityInterface
)[0]['value'] ?? null;
}
public function getRecommandedPost(): ?self
public function getMastodonUrl(): ?string
{
return $this->recommandedPost;
return $this->mastodonUrl;
}
public function setRecommandedPost(?self $recommandedPost): self
public function setMastodonUrl(?string $mastodonUrl): self
{
$this->recommandedPost = $recommandedPost;
$this->mastodonUrl = $mastodonUrl;
return $this;
}
/**
* @return Collection<int, self>
*/
public function getDeprecatedPosts(): Collection
public function getMastodonComments(): ?array
{
return $this->deprecatedPosts;
return $this->mastodonComments;
}
public function addDeprecatedPost(self $deprecatedPost): self
public function setMastodonComments(array $mastodonComments): self
{
if (!$this->deprecatedPosts->contains($deprecatedPost)) {
$this->deprecatedPosts[] = $deprecatedPost;
$deprecatedPost->setRecommandedPost($this);
}
return $this;
}
public function removeDeprecatedPost(self $deprecatedPost): self
{
if ($this->deprecatedPosts->removeElement($deprecatedPost)) {
// set the owning side to null (unless already changed)
if ($deprecatedPost->getRecommandedPost() === $this) {
$deprecatedPost->setRecommandedPost(null);
}
}
$this->mastodonComments = $mastodonComments;
return $this;
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Entity;
use App\Repository\ExportQueueRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ExportQueueRepository::class)]
class ExportQueue
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
public function getId(): ?int
{
return $this->id;
}
}

View file

@ -5,4 +5,6 @@ namespace App\Entity\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class CategoriesPage extends TitledPage {}
class CategoriesPage extends TitledPage
{
}

View file

@ -5,4 +5,6 @@ namespace App\Entity\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class CategoryPage extends TitledPage {}
class CategoryPage extends TitledPage
{
}

View file

@ -5,4 +5,6 @@ namespace App\Entity\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class ContactPage extends SimplePage {}
class ContactPage extends SimplePage
{
}

View file

@ -5,4 +5,6 @@ namespace App\Entity\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class LinksPage extends SimplePage {}
class LinksPage extends SimplePage
{
}

View file

@ -5,4 +5,6 @@ namespace App\Entity\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class MeshPage extends SimplePage {}
class MeshPage extends SimplePage
{
}

View file

@ -5,4 +5,6 @@ namespace App\Entity\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class PostPage extends TitledPage {}
class PostPage extends TitledPage
{
}

View file

@ -5,4 +5,6 @@ namespace App\Entity\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class PostsPage extends TitledPage {}
class PostsPage extends TitledPage
{
}

View file

@ -5,4 +5,6 @@ namespace App\Entity\Page;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class SearchPage extends TitledPage {}
class SearchPage extends TitledPage
{
}

View file

@ -4,7 +4,7 @@ namespace App\Entity\Page;
use App\Core\Entity\Site\Page\Block;
use App\Core\Entity\Site\Page\FileBlock;
use App\Form\MarkdownBlockType;
use App\Form\Type\SimpleMdTextareaBlockType;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Form\FormBuilderInterface;
@ -17,7 +17,7 @@ class SimplePage extends TitledPage
$builder->add(
'content',
MarkdownBlockType::class,
SimpleMdTextareaBlockType::class,
[
'label' => 'Contenu',
'options' => [

View file

@ -3,10 +3,11 @@
namespace App\Entity\Page;
use App\Core\Entity\Site\Page\Block;
use App\Core\Entity\Site\Page\Page;
use App\Core\Entity\Site\Page\FileBlock;
use App\Core\Form\Site\Page\TextareaBlockType;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Form\FormBuilderInterface;
use App\Core\Entity\Site\Page\Page;
#[ORM\Entity]
class TextPage extends Page

View file

@ -13,8 +13,8 @@ class Project implements EntityInterface
{
use Timestampable;
public const DRAFT = 0;
public const PUBLISHED = 1;
const DRAFT = 0;
const PUBLISHED = 1;
#[ORM\Id]
#[ORM\GeneratedValue]

View file

@ -2,9 +2,9 @@
namespace App\Entity;
use App\Core\Entity\EntityInterface;
use App\Repository\StlMeshRepository;
use Doctrine\ORM\Mapping as ORM;
use App\Core\Entity\EntityInterface;
#[ORM\Entity(repositoryClass: StlMeshRepository::class)]
class StlMesh implements EntityInterface
@ -86,7 +86,7 @@ class StlMesh implements EntityInterface
{
$this->files = (array) $this->files;
usort($this->files, function ($a, $b) {
usort($this->files, function($a, $b) {
if ($a['position'] > $b['position']) {
return 1;
}

View file

@ -50,7 +50,9 @@ class User implements PasswordAuthenticatedUserInterface, UserInterface, TwoFact
#[ORM\Column(type: 'boolean', options: ['default' => 0])]
private $isWriter;
public function __construct() {}
public function __construct()
{
}
public function __toString()
{

View file

@ -1,22 +0,0 @@
<?php
namespace App\EventListener;
use App\Message\PageViewMessage;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* class StatListener.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class StatListener
{
public function __construct(protected MessageBusInterface $bus) {}
public function onKernelRequest(RequestEvent $event)
{
$this->bus->dispatch(new PageViewMessage(time()));
}
}

View file

@ -4,12 +4,11 @@ namespace App\EventSubscriber;
use App\Core\Event\Setting\SettingEvent;
use App\Core\EventSubscriber\SettingEventSubscriber as EventSubscriber;
use App\Core\Form\FileManager\FilePickerType;
use App\Core\Form\Type\TinymceTextareaType;
use App\Core\Setting\SettingManager;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use App\Core\Form\FileManager\FilePickerType;
/**
* class SettingEventSubscriber.
@ -37,7 +36,6 @@ class SettingEventSubscriber extends EventSubscriber
$this->manager->init('stats_umami_url', '📊 Statistiques', 'Adresse tableau de bord Umami', '');
$this->manager->init('stats_umami_tag', '📊 Statistiques', 'Script Umami', '');
$this->manager->init('stats_grafana_url', '📊 Statistiques', 'Adresse tableau de bord Grafana', '');
$this->manager->init('post_author_description', '🖊️ Article', 'Description auteur', '');
@ -70,7 +68,7 @@ class SettingEventSubscriber extends EventSubscriber
);
}
if (in_array($entity->getCode(), ['giphy_api_key', 'stats_umami_url', 'stats_grafana_url'])) {
if (in_array($entity->getCode(), ['giphy_api_key', 'stats_umami_url'])) {
$builder->add(
'value',
TextType::class,
@ -95,7 +93,7 @@ class SettingEventSubscriber extends EventSubscriber
);
}
if (in_array($entity->getCode(), ['post_author_description'])) {
if (in_array($entity->getCode(), ['blog_footer', 'post_author_description'])) {
$event->setOption('view', 'large');
$builder->add(
@ -109,20 +107,5 @@ class SettingEventSubscriber extends EventSubscriber
]
);
}
if (in_array($entity->getCode(), ['blog_footer'])) {
$event->setOption('view', 'large');
$builder->add(
'value',
TinymceTextareaType::class,
[
'label' => $entity->getLabel(),
'attr' => [
'rows' => 20,
],
]
);
}
}
}

View file

@ -9,4 +9,6 @@ use App\Core\Factory\UserFactory as BaseUserFactory;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class UserFactory extends BaseUserFactory {}
class UserFactory extends BaseUserFactory
{
}

View file

@ -3,8 +3,6 @@
namespace App\Form\Blog;
use App\Entity\Blog\Category;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@ -12,6 +10,8 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
class CategoryType extends AbstractType
{

View file

@ -2,6 +2,7 @@
namespace App\Form\Blog;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

View file

@ -2,13 +2,8 @@
namespace App\Form\Blog;
use App\Core\Form\FileManager\FilePickerType;
use App\Core\Form\Type\BuilderType;
use App\Core\Form\Type\CollectionType as MurphCollectionType;
use App\Core\Form\Type\EditorJsTextareaType;
use App\Entity\Blog\Category;
use App\Entity\Blog\Post;
use App\Form\MarkdownType;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
@ -26,6 +21,10 @@ use Symfony\Component\Validator\Constraints\Image;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Range;
use Symfony\Component\Validator\Constraints\Url;
use App\Form\Type\SimpleMdTextareaType;
use App\Core\Form\Type\EditorJsTextareaType;
use App\Core\Form\FileManager\FilePickerType;
use App\Core\Form\Type\CollectionType as MurphCollectionType;
class PostType extends AbstractType
{
@ -52,7 +51,6 @@ class PostType extends AbstractType
'required' => true,
'choices' => [
'Markdown' => 'markdown',
'Builder' => 'builder',
'HTML' => 'html',
'Editor JS' => 'editorjs',
],
@ -63,9 +61,8 @@ class PostType extends AbstractType
);
$types = [
'markdown' => MarkdownType::class,
'builder' => BuilderType::class,
'html' => MarkdownType::class,
'markdown' => SimpleMdTextareaType::class,
'html' => SimpleMdTextareaType::class,
'editorjs' => EditorJsTextareaType::class,
];
@ -116,28 +113,6 @@ class PostType extends AbstractType
]
);
$builder->add(
'recommandedPost',
EntityType::class,
[
'label' => 'Article recommandé',
'class' => Post::class,
'choice_label' => 'title',
'required' => false,
'multiple' => false,
'attr' => [
'data-jschoice' => '',
],
'query_builder' => function (EntityRepository $repo) {
return $repo->createQueryBuilder('a')
->orderBy('a.title', 'ASC')
;
},
'constraints' => [
],
]
);
$builder->add(
'status',
ChoiceType::class,
@ -197,7 +172,6 @@ class PostType extends AbstractType
'attr' => [
],
'constraints' => [
new Image(),
],
]
);
@ -228,6 +202,19 @@ class PostType extends AbstractType
]
);
$builder->add(
'mastodonUrl',
TextType::class,
[
'label' => 'URL Mastodon',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'quickUrl',
TextType::class,
@ -326,19 +313,19 @@ class PostType extends AbstractType
]
);
// $builder->add(
// 'parameters',
// MurphCollectionType::class,
// [
// 'label' => 'Paramètres',
// 'entry_type' => PostParameterType::class,
// 'by_reference' => false,
// 'allow_add' => true,
// 'allow_delete' => true,
// 'prototype' => true,
// 'row_attr' => ['class' => 'mb-3'],
// ]
// );
$builder->add(
'parameters',
MurphCollectionType::class,
[
'label' => 'Paramètres',
'entry_type' => PostParameterType::class,
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'row_attr' => ['class' => 'mb-3'],
]
);
}
public function configureOptions(OptionsResolver $resolver)

View file

@ -3,9 +3,7 @@
namespace App\Form\Blog;
use App\Entity\Blog\Comment;
use Gregwar\CaptchaBundle\Type\CaptchaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@ -16,6 +14,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Url;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Gregwar\CaptchaBundle\Type\CaptchaType;
class UserCommentType extends AbstractType
{

View file

@ -2,7 +2,6 @@
namespace App\Form;
use Gregwar\CaptchaBundle\Type\CaptchaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@ -11,6 +10,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\NotBlank;
use Gregwar\CaptchaBundle\Type\CaptchaType;
class ContactType extends AbstractType
{

View file

@ -1,21 +0,0 @@
<?php
namespace App\Form;
use App\Core\Form\Site\Page\TextareaBlockType;
use Symfony\Component\Form\FormBuilderInterface;
class MarkdownBlockType extends TextareaBlockType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'value',
MarkdownType::class,
array_merge([
'required' => false,
'label' => false,
], $options['options']),
);
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace App\Form;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class MarkdownType extends TextareaType
{
public function getBlockPrefix()
{
return 'markdown';
}
}

View file

@ -3,10 +3,10 @@
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class ProjectLinkType extends AbstractType
{

View file

@ -2,13 +2,13 @@
namespace App\Form;
use App\Core\Form\FileManager\FilePickerType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Core\Form\FileManager\FilePickerType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class StlFileType extends AbstractType
{

View file

@ -8,6 +8,9 @@ use Symfony\Component\Form\FormView;
class SimpleMdTextareaType extends TextareaType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (!isset($view->vars['attr']['data-simplemde'])) {

View file

@ -4,4 +4,6 @@ namespace App\Form;
use App\Core\Form\UserType as BaseUserType;
class UserType extends BaseUserType {}
class UserType extends BaseUserType
{
}

View file

@ -2,9 +2,7 @@
namespace App;
use App\Core\DependencyInjection\Compiler\BuilderBlockPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
@ -37,9 +35,4 @@ class Kernel extends BaseKernel
(require $path)($routes->withPath($path), $this);
}
}
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new BuilderBlockPass());
}
}

View file

@ -11,9 +11,15 @@ use Knp\Bundle\MarkdownBundle\Parser\MarkdownParser;
*/
class Post extends MarkdownParser
{
// protected $id_class_attr_catch_re = '\{((?'.'>[ ]*[#.a-z][-_:a-zA-Z0-9="\'\/\*-]+){1,})[ ]*\}';
/**
* {@inheritdoc}
*/
//protected $id_class_attr_catch_re = '\{((?'.'>[ ]*[#.a-z][-_:a-zA-Z0-9="\'\/\*-]+){1,})[ ]*\}';
protected $id_class_attr_catch_re = '\{([^\}]+)\}';
/**
* {@inheritdoc}
*/
public function transformMarkdown($text)
{
$html = parent::transformMarkdown($text);
@ -21,6 +27,9 @@ class Post extends MarkdownParser
return str_replace(' role="doc-endnote"', '', $html);
}
/**
* {@inheritdoc}
*/
protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = [])
{
if (empty($attr) && !$defaultIdValue && empty($classes)) {
@ -93,11 +102,8 @@ class Post extends MarkdownParser
$codeblock
);
$codeblock = preg_replace_callback(
'/^\n+/',
[$this, '_doFencedCodeBlocks_newlines'],
$codeblock
);
$codeblock = preg_replace_callback('/^\n+/',
[$this, '_doFencedCodeBlocks_newlines'], $codeblock);
$classes = [];
if ('' != $classname) {
@ -109,12 +115,7 @@ class Post extends MarkdownParser
$attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? 'pre' : 'code', $attrs, null, $classes);
$pre_attr_str = $this->code_attr_on_pre ? $attr_str : '';
$code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
if (preg_match('/mermaid/', $code_attr_str)) {
$codeblock = "<pre{$code_attr_str}>{$codeblock}</pre>";
} else {
$codeblock = "<pre{$pre_attr_str}><code{$code_attr_str}>{$codeblock}</code></pre>";
}
$codeblock = "<pre{$pre_attr_str}><code{$code_attr_str}>{$codeblock}</code></pre>";
return "\n\n".$this->hashBlock($codeblock)."\n\n";
}

View file

@ -1,13 +0,0 @@
<?php
namespace App\Message;
final class PageViewMessage
{
public function __construct(public int $time) {}
public function getTime(): int
{
return $this->time;
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace App\MessageHandler;
use App\Api\InfluxDB;
use App\Message\PageViewMessage;
use InfluxDB2\Point;
use InfluxDB2\WriteType;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
final class PageViewMessageHandler implements MessageHandlerInterface
{
public function __construct(protected InfluxDB $influxDB) {}
public function __invoke(PageViewMessage $message)
{
if (!$this->influxDB->isAvailable()) {
return;
}
$client = $this->influxDB->getClient();
$writeApi = $client->createWriteApi(['writeType' => WriteType::SYNCHRONOUS]);
$pageView = new Point('page_view');
$pageView
->addTag('request', 'view')
->addField('value', 1)
->time($message->getTime())
;
$writeApi->write($pageView);
$writeApi->close();
$client->close();
}
}

View file

@ -1,16 +0,0 @@
<?php
namespace App\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
final class PageViewMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
// ...
return $stack->next()->handle($envelope, $stack);
}
}

View file

@ -9,8 +9,8 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AppEntityBlogPost>
*
* @method null|AppEntityBlogPost find($id, $lockMode = null, $lockVersion = null)
* @method null|AppEntityBlogPost findOneBy(array $criteria, array $orderBy = null)
* @method AppEntityBlogPost|null find($id, $lockMode = null, $lockVersion = null)
* @method AppEntityBlogPost|null findOneBy(array $criteria, array $orderBy = null)
* @method AppEntityBlogPost[] findAll()
* @method AppEntityBlogPost[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
@ -39,28 +39,28 @@ class AppEntityBlogPostRepository extends ServiceEntityRepository
}
}
// /**
// * @return AppEntityBlogPost[] Returns an array of AppEntityBlogPost objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('a.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// /**
// * @return AppEntityBlogPost[] Returns an array of AppEntityBlogPost objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('a.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?AppEntityBlogPost
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
// public function findOneBySomeField($value): ?AppEntityBlogPost
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method null|Comment find($id, $lockMode = null, $lockVersion = null)
* @method null|Comment findOneBy(array $criteria, array $orderBy = null)
* @method Comment|null find($id, $lockMode = null, $lockVersion = null)
* @method Comment|null findOneBy(array $criteria, array $orderBy = null)
* @method Comment[] findAll()
* @method Comment[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/

View file

@ -3,8 +3,8 @@
namespace App\Repository\Blog;
use App\Core\Repository\RepositoryQuery;
use App\Repository\Blog\PostFollowRepository as Repository;
use Knp\Component\Pager\PaginatorInterface;
use App\Repository\Blog\PostFollowRepository as Repository;
class PostFollowRepositoryQuery extends RepositoryQuery
{

View file

@ -54,13 +54,15 @@ class PostRepositoryQuery extends RepositoryQuery
;
}
protected function filterHandler(string $name, $value)
{
if ('category' === $name) {
$this->inCategory($value);
}
}
public function search(?string $keywords, ?string $tag)
{
$keywords = explode(' ', $keywords);
$filterWords = fn ($keyword) => '' !== trim($keyword) && preg_match('/[a-zA-Z]+/', $keyword);
$keywords = array_filter($keywords, $filterWords);
if ($keywords) {
$conn = $this->repository->getEm()->getConnection();
@ -68,106 +70,42 @@ class PostRepositoryQuery extends RepositoryQuery
'SELECT
post.id,
post.title,
post.content,
post.published_at
MATCH(post.title) AGAINST(:search) AS MATCH_TITLE,
MATCH(post.content) AGAINST(:search) AS MATCH_CONTENT
FROM post
WHERE
post.status = 1 AND
post.published_at < :date
'
);
ORDER BY
MATCH_TITLE DESC,
MATCH_CONTENT DESC
');
$statement = $query->execute([
':search' => $keywords,
':date' => (new \DateTime())->format('Y-m-d H:i:s'),
]);
$results = $statement->fetchAll();
$ids = [];
$matches = [];
foreach ($results as $k => $v) {
$initWords = explode(' ', $v['title']);
$words = [];
$rate = ($v['MATCH_TITLE'] * 2) + $v['MATCH_CONTENT'];
foreach ($initWords as $initWord) {
$words = array_merge($words, preg_split('/[:_\'-]+/', $initWord));
}
$words = array_filter($words, $filterWords);
foreach ($keywords as $keyword) {
if (str_contains(mb_strtolower($v['content']), mb_strtolower($keyword))) {
$similarity = 99;
if (isset($matches[$v['id']])) {
$matches[$v['id']]['similarity'] += $similarity;
++$matches[$v['id']]['count'];
} else {
$matches[$v['id']] = [
'id' => $v['id'],
'title' => $v['title'],
'published_at' => $v['published_at'],
'similarity' => $similarity,
'count' => 1,
];
}
}
foreach ($words as $word) {
if (str_contains(mb_strtolower($word), mb_strtolower($keyword))) {
$similarity = 150;
if (isset($matches[$v['id']])) {
$matches[$v['id']]['similarity'] += $similarity;
++$matches[$v['id']]['count'];
} else {
$matches[$v['id']] = [
'id' => $v['id'],
'title' => $v['title'],
'published_at' => $v['published_at'],
'similarity' => $similarity,
'count' => 1,
];
}
} else {
$lev = levenshtein($word, $keyword);
$similarity = 100 - ($lev * 100 / mb_strlen($word));
if ($similarity > 70) {
if (isset($matches[$v['id']])) {
$matches[$v['id']]['similarity'] += $similarity;
} else {
$matches[$v['id']] = [
'id' => $v['id'],
'title' => $v['title'],
'published_at' => $v['published_at'],
'similarity' => $similarity,
'count' => 1,
];
}
}
}
}
if ($rate >= 7) {
$ids[] = $v['id'];
}
}
$matches = array_filter($matches, function ($match) use ($keywords) {
return (100 * $match['count'] / count($keywords)) > 80;
});
if (0 == count($ids)) {
foreach ($results as $k => $v) {
$rate = ($v['MATCH_TITLE'] * 2) + $v['MATCH_CONTENT'];
usort($matches, function ($a, $b) {
if ($a['similarity'] > $b['similarity']) {
return -1;
if ($rate >= 6) {
$ids[] = $v['id'];
}
}
if ($b['similarity'] > $a['similarity']) {
return 1;
}
return ($a['published_at'] != $b['published_at']) * -1;
});
$ids = array_column($matches, 'id');
}
if (!$ids) {
$ids = [-1];
@ -189,11 +127,4 @@ class PostRepositoryQuery extends RepositoryQuery
return $this;
}
protected function filterHandler(string $name, $value)
{
if ('category' === $name) {
$this->inCategory($value);
}
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Repository;
use App\Entity\ExportQueue;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<ExportQueue>
*
* @method ExportQueue|null find($id, $lockMode = null, $lockVersion = null)
* @method ExportQueue|null findOneBy(array $criteria, array $orderBy = null)
* @method ExportQueue[] findAll()
* @method ExportQueue[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ExportQueueRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ExportQueue::class);
}
public function add(ExportQueue $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(ExportQueue $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return ExportQueue[] Returns an array of ExportQueue objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('e')
// ->andWhere('e.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('e.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?ExportQueue
// {
// return $this->createQueryBuilder('e')
// ->andWhere('e.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -9,8 +9,8 @@ use Doctrine\ORM\ORMException;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method null|Project find($id, $lockMode = null, $lockVersion = null)
* @method null|Project findOneBy(array $criteria, array $orderBy = null)
* @method Project|null find($id, $lockMode = null, $lockVersion = null)
* @method Project|null findOneBy(array $criteria, array $orderBy = null)
* @method Project[] findAll()
* @method Project[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/

View file

@ -3,9 +3,9 @@
namespace App\Repository;
use App\Core\Repository\RepositoryQuery;
use App\Entity\Project;
use App\Repository\ProjectRepository as Repository;
use Knp\Component\Pager\PaginatorInterface;
use App\Repository\ProjectRepository as Repository;
use App\Entity\Project;
class ProjectRepositoryQuery extends RepositoryQuery
{

View file

@ -7,8 +7,8 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method null|StlMesh find($id, $lockMode = null, $lockVersion = null)
* @method null|StlMesh findOneBy(array $criteria, array $orderBy = null)
* @method StlMesh|null find($id, $lockMode = null, $lockVersion = null)
* @method StlMesh|null findOneBy(array $criteria, array $orderBy = null)
* @method StlMesh[] findAll()
* @method StlMesh[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/

View file

@ -3,8 +3,8 @@
namespace App\Repository;
use App\Core\Repository\RepositoryQuery;
use App\Repository\StlMeshRepository as Repository;
use Knp\Component\Pager\PaginatorInterface;
use App\Repository\StlMeshRepository as Repository;
class StlMeshRepositoryQuery extends RepositoryQuery
{

View file

@ -6,8 +6,9 @@ use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{

View file

@ -40,9 +40,8 @@ class BlogExtension extends AbstractExtension
$text = str_replace('http://upload.deblan.fr', 'https://upload.deblan.org', $text);
$text = str_replace('http://dedi.geneweb.fr', 'http://kim.deblan.fr', $text);
$text = str_replace('http://mediaplayer.deblan.fr', 'https://mediaplayer.deblan.org', $text);
$text = str_replace('http://blog.deblan.fr', 'https://www.deblan.fr', $text);
$text = str_replace('http://www.deblan.tv', 'https://www.deblan.fr', $text);
$text = str_replace('http://www.deblan.io', 'https://www.deblan.fr', $text);
$text = str_replace('http://blog.deblan.fr', 'https://www.deblan.io', $text);
$text = str_replace('http://www.deblan.tv', 'https://www.deblan.io', $text);
$text = preg_replace_callback(
'`<p([^>]*)>(.*)</p([^>]*)>`isU',
@ -135,12 +134,10 @@ class BlogExtension extends AbstractExtension
'#<code langage=\'([^\']*)\'>(.*)</code>#isU',
],
function ($data) {
$lang = strtolower(
str_replace(
['console', 'texte', 'apache'],
['bash', 'text', 'html'],
$data[1]
)
$lang = strtolower(str_replace(
['console', 'texte', 'apache'],
['bash', 'text', 'html'],
$data[1])
);
$class = 'language-'.$lang;

View file

@ -30,9 +30,9 @@ class ColorExtension extends AbstractExtension
return sprintf(
'#%s%s%s',
2 != strlen($red) ? '0'.$red : $red,
2 != strlen($green) ? '0'.$green : $green,
2 != strlen($blue) ? '0'.$blue : $blue,
strlen($red) != 2 ? '0'.$red : $red,
strlen($green) != 2 ? '0'.$green : $green,
strlen($blue) != 2 ? '0'.$blue : $blue,
);
}
}

View file

@ -21,7 +21,7 @@ class LazyLoadExtension extends AbstractExtension
public function lazyLoad($text)
{
return preg_replace_callback(
$text = preg_replace_callback(
'`<img src="([^"]+)" alt="([^"]+)" />`isU',
function ($data) {
$lazy = sprintf('<img class="lazy-img" src="#" data-src="%s" alt="%s" />', $data[1], $data[2]);
@ -31,5 +31,7 @@ class LazyLoadExtension extends AbstractExtension
},
$text
);
return $text;
}
}

Some files were not shown because too many files have changed in this diff Show more