From 5938b8c9c9123ad5e22eb705cb10a787012e1a01 Mon Sep 17 00:00:00 2001 From: Shelby Tucker Date: Mon, 2 Jun 2025 21:14:47 -0400 Subject: [PATCH 01/27] support zettelkasten filenames --- docker-compose-build.yml | 1 + docker-compose-dev.yml | 1 + docker-compose.yml | 1 + perlite/helper.php | 98 ++++++++++++++++++++++++++++------------ 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/docker-compose-build.yml b/docker-compose-build.yml index ef89ac68..48a7ebd9 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -5,6 +5,7 @@ services: environment: - NOTES_PATH=Demo - HIDE_FOLDERS=docs,private,trash + - ZETTELKASTEN_FILENAMES_ENABLED=false - HIDDEN_FILE_ACCESS=false - LINE_BREAKS=true - ABSOLUTE_PATHS=false diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 491c285a..97b6f3cb 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -6,6 +6,7 @@ services: environment: - NOTES_PATH=Demo - HIDE_FOLDERS=docs,private,trash + - ZETTELKASTEN_FILENAMES_ENABLED=false - HIDDEN_FILE_ACCESS=false - LINE_BREAKS=true - ABSOLUTE_PATHS=false diff --git a/docker-compose.yml b/docker-compose.yml index 4a968e29..c54a67a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: environment: - NOTES_PATH=Demo - HIDE_FOLDERS=docs,private,trash + - ZETTELKASTEN_FILENAMES_ENABLED=false - HIDDEN_FILE_ACCESS=false - LINE_BREAKS=true - ABSOLUTE_PATHS=false diff --git a/perlite/helper.php b/perlite/helper.php index 170091d1..a9c921c9 100644 --- a/perlite/helper.php +++ b/perlite/helper.php @@ -65,6 +65,11 @@ if (!isset($siteTwitter)) $siteTwitter = getenv('SITE_TWITTER'); +// Use frontmatter 'title' or top level h1 instead of filename +if (!isset($useZettelkastenFilenames)) { + $useZettelkastenFilenames = empty(getenv('ZETTELKASTEN_FILENAMES_ENABLED')) ? false : filter_var(getenv('ZETTELKASTEN_FILENAMES_ENABLED'), FILTER_VALIDATE_BOOLEAN); +} + // Temp PATH for graph linking temp files if (empty($tempPath)) $tempPath = empty(getenv('TEMP_PATH')) ? sys_get_temp_dir() : getenv('TEMP_PATH'); @@ -190,6 +195,7 @@ function menu($dir, $folder = '') global $hiddenFileAccess; global $avFiles; + global $useZettelkastenFilenames; $html = ''; // get all files from current dir $files = glob($dir . '/*'); @@ -232,34 +238,39 @@ function menu($dir, $folder = '') } } - // iterate the files - foreach ($files as $file) { - if (isMDFile($file)) { - - $path = getFileInfos($file)[0]; - $mdFile = getFileInfos($file)[1]; - - $path = '/' . $path; - // push the the path to the array - array_push($avFiles, $path); - - // URL Encode the Path for the JS call - $pathClean = rawurlencode($path); - $pathID = str_replace(' ', '_', $path); - $pathID = preg_replace('/[^A-Za-z0-9\-]/', '_', $path); - - - $html .= ' - - '; - } - } - - return $html; + // Iterate the files + foreach ($files as $file) { + if (isMDFile($file)) { + $pathInfo = getFileInfos($file); + $relativePathForURL = $pathInfo[0]; + $baseFilenameWithoutExtension = $pathInfo[1]; + + $displayTitle = ""; + + if ($useZettelkastenFilenames) { + $displayTitle = getDisplayTitle($file); + } else { + $displayTitle = $baseFilenameWithoutExtension; + } + + $urlClickPath = '/' . $relativePathForURL; + array_push($avFiles, $urlClickPath); + $pathCleanForJS = rawurlencode($urlClickPath); + + // Create a unique ID for the HTML element + $elementId = 'fileid-' . preg_replace('/[^A-Za-z0-9\-_]/', '_', $urlClickPath); + $elementId = str_replace('/', '_', $elementId); // Replace slashes for cleaner ID + + $html .= ' + + '; + } + } + return $html; } function doSearch($dir, $searchfor) @@ -448,6 +459,37 @@ function isCached($jsonMetadaFile, $metadaTempFileSum) return false; } +function getDisplayTitle($filePath) { + $content = @file_get_contents($filePath); + if ($content === false) { + // Fallback to filename if file can't be read + return getFileInfos($filePath)[1]; + } + + // 1. Try to get title from frontmatter + if (preg_match('/^---\s*\n(.*?)\n---\s*\n/s', $content, $frontmatterMatches)) { + $frontmatterRaw = $frontmatterMatches[1]; + if (preg_match('/^title:\s*(.*?)\s*$/im', $frontmatterRaw, $titleMatches)) { + $title = trim($titleMatches[1]); + $title = trim($title, "'\""); + if (!empty($title)) { + return $title; + } + } + } + + // 2. Try to get the first H1 heading + if (preg_match('/^#\s+(.*?)\s*$/m', $content, $h1Matches)) { + $h1Title = trim($h1Matches[1]); + if (!empty($h1Title)) { + return $h1Title; + } + } + + // 3. Fallback to filename (obtained via getFileInfos) + return getFileInfos($filePath)[1]; +} + function getfullGraph($rootDir) { From a84f2e12c89126e0d394410fc7809ef662ec0947 Mon Sep 17 00:00:00 2001 From: dewille Date: Sat, 30 Aug 2025 15:16:59 +0200 Subject: [PATCH 02/27] fix: replace deprecated utf8_decode() with mb_strlen() and null coalescing for PHP 8.2+ --- perlite/.src/PerliteParsedown.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perlite/.src/PerliteParsedown.php b/perlite/.src/PerliteParsedown.php index 36115988..9b8d44f7 100644 --- a/perlite/.src/PerliteParsedown.php +++ b/perlite/.src/PerliteParsedown.php @@ -1020,7 +1020,7 @@ protected function lines(array $lines) unset($parts[0]); foreach ($parts as $part) { - $shortage = 4 - strlen(utf8_decode($input)) % 4; + $shortage = 4 - (mb_strlen($input ?? '', 'UTF-8') % 4); $line .= str_repeat(' ', $shortage); $line .= $part; From 0d4c2ff5e8c0b145ca97e2665f0fee8981ef90c9 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Wed, 5 Nov 2025 22:57:32 +0100 Subject: [PATCH 03/27] added support to open the nav tree and mark the entry as active --- perlite/.js/perlite.js | 13 +++++-------- perlite/settings.php | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/perlite/.js/perlite.js b/perlite/.js/perlite.js index 226c8b23..6d5d2feb 100644 --- a/perlite/.js/perlite.js +++ b/perlite/.js/perlite.js @@ -1052,10 +1052,11 @@ function openNavMenu(target, openAll = false) { // open nav menu to target var navId = decodeURIComponent(target); - linkname = navId.match(/([^\/]*)\/*$/)[1] - + // search and open tree reverse navId = navId.replace(/[^a-zA-Z0-9\-]/g, '_'); + + navId = 'fileid-' + navId; var next = $('#' + navId).parent().closest('.collapse'); do { @@ -1067,13 +1068,9 @@ function openNavMenu(target, openAll = false) { } while (next.length != 0); + // mark active + $('#' + navId).addClass('perlite-link-active is-active'); - // set focus to link - var searchText = linkname; - - $("div").filter(function () { - return $(this).text() === searchText; - }).parent().addClass('perlite-link-active is-active'); }; diff --git a/perlite/settings.php b/perlite/settings.php index b1c83012..27416c49 100644 --- a/perlite/settings.php +++ b/perlite/settings.php @@ -22,6 +22,7 @@ $siteHomepage = ""; // empty for $siteURL $siteGithub = "https://github.com/secure-77"; // empty for no Github $siteTwitter = "@secure_sec77"; +$useZettelkastenFilenames = false; $tempPath = ""; // blanc so it gets it automatically From 338a1b07b4a537ce1d19fc850f4e15b373906149 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Wed, 5 Nov 2025 23:22:51 +0100 Subject: [PATCH 04/27] fixed issue Change theme.css over app.css priority Fixes #167 --- Changelog.md | 5 +++++ docker-compose-build.yml | 2 +- docker-compose-dev.yml | 2 +- docker-compose.yml | 2 +- perlite/index.php | 4 +++- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index df26bfc3..8ed905c3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,8 @@ +## 1.7 +- Support Zettelkasten Filenames, use with caution! - PR [#159](https://github.com/secure-77/Perlite/pull/159) thanks to @smtucker +- replaced deprecated utf8_decode() - PR [#164](https://github.com/secure-77/Perlite/pull/164) thanks to @dewillepl +- fixed theme issue [#167](https://github.com/secure-77/Perlite/issues/167) + ## 1.6 - get rid of mb_strlen, fixed issue [#151](https://github.com/secure-77/Perlite/issues/151) thanks to @Sephral - hide X / Twitter when not set, issue [#152](https://github.com/secure-77/Perlite/issues/152), thanks to @EKNr1 diff --git a/docker-compose-build.yml b/docker-compose-build.yml index 48a7ebd9..8008b2a2 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -5,7 +5,6 @@ services: environment: - NOTES_PATH=Demo - HIDE_FOLDERS=docs,private,trash - - ZETTELKASTEN_FILENAMES_ENABLED=false - HIDDEN_FILE_ACCESS=false - LINE_BREAKS=true - ABSOLUTE_PATHS=false @@ -16,6 +15,7 @@ services: - HOME_FILE=README - FONT_SIZE=15 - HTML_SAFE_MODE=true + - ZETTELKASTEN_FILENAMES_ENABLED=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 97b6f3cb..fc30be6f 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -6,7 +6,6 @@ services: environment: - NOTES_PATH=Demo - HIDE_FOLDERS=docs,private,trash - - ZETTELKASTEN_FILENAMES_ENABLED=false - HIDDEN_FILE_ACCESS=false - LINE_BREAKS=true - ABSOLUTE_PATHS=false @@ -17,6 +16,7 @@ services: - HOME_FILE=README - FONT_SIZE=15 - HTML_SAFE_MODE=true + - ZETTELKASTEN_FILENAMES_ENABLED=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/docker-compose.yml b/docker-compose.yml index c54a67a7..d6a21d11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,6 @@ services: environment: - NOTES_PATH=Demo - HIDE_FOLDERS=docs,private,trash - - ZETTELKASTEN_FILENAMES_ENABLED=false - HIDDEN_FILE_ACCESS=false - LINE_BREAKS=true - ABSOLUTE_PATHS=false @@ -17,6 +16,7 @@ services: - HOME_FILE=README - FONT_SIZE=15 - HTML_SAFE_MODE=true + - ZETTELKASTEN_FILENAMES_ENABLED=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/perlite/index.php b/perlite/index.php index 8570cf1e..3f413a43 100644 --- a/perlite/index.php +++ b/perlite/index.php @@ -30,8 +30,10 @@ - + + + From 61d3c17b3446070ce7fb401c06f7f18c8de8f850 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Wed, 5 Nov 2025 23:37:42 +0100 Subject: [PATCH 05/27] added issue option to add more highlight.js languages Fixes #173 --- .gitignore | 1 + Changelog.md | 1 + docker-compose-build.yml | 1 + docker-compose-dev.yml | 1 + docker-compose.yml | 1 + perlite/helper.php | 11 +++++++++++ perlite/index.php | 4 +++- perlite/settings.php | 1 + 8 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 48ea2a60..970751a9 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ perlite/Link Tests/folder1/folder 2/subfolder_test.md perlite/Link Tests/folder1/folder 2/docs/image_subFolder.png perlite/Link Tests/folder1/folder 2/docs/subfolder_document.md perlite/sec.jpg +perlite/Demo/Demo Documents/test.md diff --git a/Changelog.md b/Changelog.md index 8ed905c3..54181114 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,7 @@ - Support Zettelkasten Filenames, use with caution! - PR [#159](https://github.com/secure-77/Perlite/pull/159) thanks to @smtucker - replaced deprecated utf8_decode() - PR [#164](https://github.com/secure-77/Perlite/pull/164) thanks to @dewillepl - fixed theme issue [#167](https://github.com/secure-77/Perlite/issues/167) +- implemented issue (additional highlight.js support) [#173](https://github.com/secure-77/Perlite/issues/173) ## 1.6 - get rid of mb_strlen, fixed issue [#151](https://github.com/secure-77/Perlite/issues/151) thanks to @Sephral diff --git a/docker-compose-build.yml b/docker-compose-build.yml index 8008b2a2..8b70c6e7 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -9,6 +9,7 @@ services: - LINE_BREAKS=true - ABSOLUTE_PATHS=false - ALLOWED_FILE_LINK_TYPES=pdf,mp4 + - HIGHLIGHTJS_LANGS=powershell - DISABLE_POP_HOVER=true - SHOW_TOC=true - SHOW_LOCAL_GRAPH=true diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index fc30be6f..94c6db87 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -10,6 +10,7 @@ services: - LINE_BREAKS=true - ABSOLUTE_PATHS=false - ALLOWED_FILE_LINK_TYPES=pdf,mp4 + - HIGHLIGHTJS_LANGS=powershell - DISABLE_POP_HOVER=false - SHOW_TOC=true - SHOW_LOCAL_GRAPH=true diff --git a/docker-compose.yml b/docker-compose.yml index d6a21d11..51f35535 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - LINE_BREAKS=true - ABSOLUTE_PATHS=false - ALLOWED_FILE_LINK_TYPES=pdf,mp4 + - HIGHLIGHTJS_LANGS=powershell - DISABLE_POP_HOVER=false - SHOW_TOC=true - SHOW_LOCAL_GRAPH=true diff --git a/perlite/helper.php b/perlite/helper.php index a9c921c9..2314ee88 100644 --- a/perlite/helper.php +++ b/perlite/helper.php @@ -82,6 +82,10 @@ if (empty($allowedFileLinkTypes)) $allowedFileLinkTypes = empty(getenv('ALLOWED_FILE_LINK_TYPES')) ? ['pdf', 'mp4'] : explode(",", getenv('ALLOWED_FILE_LINK_TYPES')); +// highlight.js languages +if (empty($highlightJSLangs)) + $highlightJSLangs = empty(getenv('HIGHLIGHTJS_LANGS')) ? ['powershell'] : explode(",", getenv('HIGHLIGHTJS_LANGS')); + // disable PopHovers if (empty($disablePopHovers)) $disablePopHovers = empty(getenv('DISABLE_POP_HOVER')) ? "false" : getenv('DISABLE_POP_HOVER'); @@ -710,6 +714,7 @@ function loadSettings($rootDir) global $siteName; global $siteTwitter; global $uriPath; + global $highlightJSLangs; // get themes @@ -774,6 +779,12 @@ function loadSettings($rootDir) $defaultSettings .= ''; $defaultSettings .= ''; + // highlight.js languages + //$highlightLangs = explode(',', $highlightJSLangs); + + foreach ($highlightJSLangs as $lang) { + $defaultSettings .= ''; + } return $themes . $defaultSettings; } diff --git a/perlite/index.php b/perlite/index.php index 3f413a43..d95df269 100644 --- a/perlite/index.php +++ b/perlite/index.php @@ -32,6 +32,8 @@ + + @@ -41,7 +43,7 @@ - + diff --git a/perlite/settings.php b/perlite/settings.php index 27416c49..cafa0965 100644 --- a/perlite/settings.php +++ b/perlite/settings.php @@ -23,6 +23,7 @@ $siteGithub = "https://github.com/secure-77"; // empty for no Github $siteTwitter = "@secure_sec77"; $useZettelkastenFilenames = false; +$highlightJSLangs = ["powershell", "x86asm"]; $tempPath = ""; // blanc so it gets it automatically From b1bd8746e155e5ae894205a4cec5af6b7e242133 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:18:51 +0100 Subject: [PATCH 06/27] sustainable mb_strlen / strlen / utf8_decode fix --- Changelog.md | 3 ++- perlite/.src/PerliteParsedown.php | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 54181114..326bc200 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,8 +1,9 @@ -## 1.7 +## 1.7 - dev - Support Zettelkasten Filenames, use with caution! - PR [#159](https://github.com/secure-77/Perlite/pull/159) thanks to @smtucker - replaced deprecated utf8_decode() - PR [#164](https://github.com/secure-77/Perlite/pull/164) thanks to @dewillepl - fixed theme issue [#167](https://github.com/secure-77/Perlite/issues/167) - implemented issue (additional highlight.js support) [#173](https://github.com/secure-77/Perlite/issues/173) +- sustainable mb_strlen / strlen / utf8_decode fix ## 1.6 - get rid of mb_strlen, fixed issue [#151](https://github.com/secure-77/Perlite/issues/151) thanks to @Sephral diff --git a/perlite/.src/PerliteParsedown.php b/perlite/.src/PerliteParsedown.php index 9b8d44f7..cc6001fc 100644 --- a/perlite/.src/PerliteParsedown.php +++ b/perlite/.src/PerliteParsedown.php @@ -1020,7 +1020,18 @@ protected function lines(array $lines) unset($parts[0]); foreach ($parts as $part) { - $shortage = 4 - (mb_strlen($input ?? '', 'UTF-8') % 4); + + + $shortage = 0; + if (function_exists('mb_strlen')) { + $shortage = 4 - (mb_strlen($input ?? '', 'UTF-8') % 4); + } elseif (function_exists('iconv')) { + $converted = @iconv('UTF-8', 'ISO-8859-1//TRANSLIT//IGNORE', $input); + $shortage = 4 - (strlen($converted) % 4); + } else { + // Fallback: count bytes (not characters) + $shortage = 4 - (strlen($input) % 4); + } $line .= str_repeat(' ', $shortage); $line .= $part; From 3e8490ca6aa0ff7f7b58afd0acfa3a32c69361db Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:33:40 +0100 Subject: [PATCH 07/27] removed border from local graph --- Changelog.md | 3 ++- perlite/.styles/perlite.css | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 326bc200..9bb5e39b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,9 +1,10 @@ -## 1.7 - dev +## 1.6.1 - dev - Support Zettelkasten Filenames, use with caution! - PR [#159](https://github.com/secure-77/Perlite/pull/159) thanks to @smtucker - replaced deprecated utf8_decode() - PR [#164](https://github.com/secure-77/Perlite/pull/164) thanks to @dewillepl - fixed theme issue [#167](https://github.com/secure-77/Perlite/issues/167) - implemented issue (additional highlight.js support) [#173](https://github.com/secure-77/Perlite/issues/173) - sustainable mb_strlen / strlen / utf8_decode fix +- removed border from local graph ## 1.6 - get rid of mb_strlen, fixed issue [#151](https://github.com/secure-77/Perlite/issues/151) thanks to @Sephral diff --git a/perlite/.styles/perlite.css b/perlite/.styles/perlite.css index 4652a808..a8fcaef4 100644 --- a/perlite/.styles/perlite.css +++ b/perlite/.styles/perlite.css @@ -56,7 +56,7 @@ /* fix graph canvas height */ #localGraph { - border: calc(0.5px + var(--divider-width)) solid var(--divider-color); + /* border: calc(0.5px + var(--divider-width)) solid var(--divider-color); */ height: 50%; } From e8a9d92c39c0b02afef44bfe4bc60f8d91142f85 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:02:12 +0100 Subject: [PATCH 08/27] removed border from custom page section --- Changelog.md | 3 ++- perlite/.styles/perlite.css | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 9bb5e39b..5516e655 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,7 +4,8 @@ - fixed theme issue [#167](https://github.com/secure-77/Perlite/issues/167) - implemented issue (additional highlight.js support) [#173](https://github.com/secure-77/Perlite/issues/173) - sustainable mb_strlen / strlen / utf8_decode fix -- removed border from local graph +- removed border from local graph and custom page section + ## 1.6 - get rid of mb_strlen, fixed issue [#151](https://github.com/secure-77/Perlite/issues/151) thanks to @Sephral diff --git a/perlite/.styles/perlite.css b/perlite/.styles/perlite.css index a8fcaef4..6eb6cc6e 100644 --- a/perlite/.styles/perlite.css +++ b/perlite/.styles/perlite.css @@ -7,7 +7,7 @@ .custom-page { text-align: center; - border-bottom: var(--divider-width) solid var(--divider-color); + /* border-bottom: var(--divider-width) solid var(--divider-color); */ } .custom-page-logo { From cb6162eaf52f1e7d614c772b6e7266cd9165a2f5 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:13:44 +0100 Subject: [PATCH 09/27] added headline copy Copy Link to Headline Reference Button Fixes #156 --- perlite/.js/perlite.js | 37 ++++++++++++++++++++++++++++++++ perlite/.styles/perlite.css | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/perlite/.js/perlite.js b/perlite/.js/perlite.js index 6d5d2feb..25643351 100644 --- a/perlite/.js/perlite.js +++ b/perlite/.js/perlite.js @@ -448,6 +448,43 @@ function getContent(str, home = false, popHover = false, anchor = "") { } + // Add copyable anchors to all headings (h1–h6) + document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((heading) => { + // Ensure each heading has an ID (generate one if missing) + if (!heading.id) { + heading.id = heading.textContent + .trim() + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w-]/g, ""); + } + + // Create the copy icon + const copyLink = document.createElement("span"); + copyLink.className = "copy-icon"; + copyLink.textContent = "#"; + copyLink.title = "Copy link to clipboard"; + + // Append the icon to the heading + heading.appendChild(copyLink); + + // Add click event to copy link + copyLink.addEventListener("click", (event) => { + event.preventDefault(); + const url = `${window.location.origin}${window.location.pathname}#${heading.id}`; + navigator.clipboard.writeText(url); + + // Visual feedback + copyLink.classList.add("copied"); + copyLink.textContent = "✅"; + setTimeout(() => { + copyLink.textContent = "#"; + copyLink.classList.remove("copied"); + }, 1000); + }); + }); + + // run mobile settings isMobile(); diff --git a/perlite/.styles/perlite.css b/perlite/.styles/perlite.css index 6eb6cc6e..60b7a1d5 100644 --- a/perlite/.styles/perlite.css +++ b/perlite/.styles/perlite.css @@ -150,6 +150,48 @@ span.cm-highlight { margin-top: 30px; } + +/* Position and appearance of copy icon */ +.copy-icon { + cursor: pointer; + color: var(--text-tag); + font-size: 0.9em; + margin-left: 0.25em; + visibility: hidden; + text-decoration: none; + transition: color 0.2s ease, opacity 0.2s ease; +} + +/* Show icon only on hover */ +h1:hover .copy-icon, +h2:hover .copy-icon, +h3:hover .copy-icon, +h4:hover .copy-icon, +h5:hover .copy-icon, +h6:hover .copy-icon { + visibility: visible; +} + +/* Hover effect */ +.copy-icon:hover { + color: var(--text-accent); +} + +/* Temporary success state */ +.copy-icon.copied { + color: green; + visibility: visible; +} + + + + + + + + + + /* --------------- */ /* Mobile / Responive Settings */ From 990a35e335e4c7c6502506d06f92f5d5fde12e71 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:33:34 +0100 Subject: [PATCH 10/27] implemented perlite.js need to use the URI_PATH env Fixes #174 --- perlite/.js/perlite.js | 12 +++++++----- perlite/helper.php | 35 +++++++++++++++++++++++------------ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/perlite/.js/perlite.js b/perlite/.js/perlite.js index 25643351..bf295c31 100644 --- a/perlite/.js/perlite.js +++ b/perlite/.js/perlite.js @@ -9,19 +9,21 @@ //// load default settings -// define perlite location on webserver -//var uriPath = '/perlite/' -var uriPath = '/' - // define home file var homeFile = "README"; if ($('#index').data('option')) { - homeFile = $('#index').data('option'); } +// define uri path +var uriPath = '/' + +if ($('#uri_path').data('option')) { + uriPath = $('#uri_path').data('option'); +} + // disable pophovers if ($('#disablePopHovers').data('option') == true && localStorage.getItem("disablePopUp") === null) { diff --git a/perlite/helper.php b/perlite/helper.php index 2314ee88..b51e3747 100644 --- a/perlite/helper.php +++ b/perlite/helper.php @@ -747,18 +747,34 @@ function loadSettings($rootDir) if ($defaultTheme === $folderName) { - $themes .= ''; + $themes .= ' + '; } else { - $themes .= ''; + $themes .= ' + '; } } } + // default settings + $defaultSettings = ' + '; + $defaultSettings .= ' + '; + $defaultSettings .= ' + '; + $defaultSettings .= ' + '; + $defaultSettings .= ' + '; + // Meta Tags - $defaultSettings = - ' + $defaultSettings .= + ' + + @@ -769,21 +785,16 @@ function loadSettings($rootDir) - - '; - // default settings - $defaultSettings .= ''; - $defaultSettings .= ''; - $defaultSettings .= ''; - $defaultSettings .= ''; + // highlight.js languages //$highlightLangs = explode(',', $highlightJSLangs); foreach ($highlightJSLangs as $lang) { - $defaultSettings .= ''; + $defaultSettings .= ' + '; } return $themes . $defaultSettings; From 8a376bb4cb85b2bd3bccef97f093bd246790c034 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:43:17 +0100 Subject: [PATCH 11/27] Show random note button only when graph data is availible Fixes #168 --- Changelog.md | 2 ++ perlite/.js/perlite.js | 1 + perlite/index.php | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 5516e655..74110754 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,8 @@ - implemented issue (additional highlight.js support) [#173](https://github.com/secure-77/Perlite/issues/173) - sustainable mb_strlen / strlen / utf8_decode fix - removed border from local graph and custom page section +- added headline copy link button [#156](https://github.com/secure-77/Perlite/issues/156) +- show random button only when graph-data is availible [#168](https://github.com/secure-77/Perlite/issues/168) ## 1.6 diff --git a/perlite/.js/perlite.js b/perlite/.js/perlite.js index bf295c31..b13348df 100644 --- a/perlite/.js/perlite.js +++ b/perlite/.js/perlite.js @@ -565,6 +565,7 @@ function renderGraph(modal, path = "", filter_emptyNodes = false, show_tags = tr // no graph found exit if ($("#allGraphNodes").length == 0 || $("#allGraphNodes").text == '[]') { console.log("Graph: no data found") + document.getElementById("random_note").style.display = "none"; return; } diff --git a/perlite/index.php b/perlite/index.php index d95df269..7be6b89f 100644 --- a/perlite/index.php +++ b/perlite/index.php @@ -125,7 +125,7 @@ class="logo-full"> -
+
Date: Thu, 13 Nov 2025 01:06:48 +0100 Subject: [PATCH 12/27] pop over fixes and implement option to use relative markdown links Fixes #170 --- Changelog.md | 3 ++ docker-compose-build.yml | 1 + docker-compose-dev.yml | 1 + docker-compose.yml | 1 + perlite/.js/perlite.js | 20 ++++++-- perlite/.styles/perlite.css | 4 ++ .../Demo/Demo Documents/Links and Embedded.md | 2 + perlite/content.php | 11 +++++ perlite/helper.php | 7 ++- perlite/settings.php | 47 ++++++++++++------- 10 files changed, 75 insertions(+), 22 deletions(-) diff --git a/Changelog.md b/Changelog.md index 74110754..a4c743e9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,9 @@ - removed border from local graph and custom page section - added headline copy link button [#156](https://github.com/secure-77/Perlite/issues/156) - show random button only when graph-data is availible [#168](https://github.com/secure-77/Perlite/issues/168) +- fixed some pop hover issues (use with 100%) and show content +- implemented support for internal Markdown Links [#170](https://github.com/secure-77/Perlite/issues/170) + ## 1.6 diff --git a/docker-compose-build.yml b/docker-compose-build.yml index 8b70c6e7..6fa7f07a 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -17,6 +17,7 @@ services: - FONT_SIZE=15 - HTML_SAFE_MODE=true - ZETTELKASTEN_FILENAMES_ENABLED=false + - INTERNAL_MARKDOWN_LINKS=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 94c6db87..e9f0c6f1 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -18,6 +18,7 @@ services: - FONT_SIZE=15 - HTML_SAFE_MODE=true - ZETTELKASTEN_FILENAMES_ENABLED=false + - INTERNAL_MARKDOWN_LINKS=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/docker-compose.yml b/docker-compose.yml index 51f35535..bc3e2f20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: - FONT_SIZE=15 - HTML_SAFE_MODE=true - ZETTELKASTEN_FILENAMES_ENABLED=false + - INTERNAL_MARKDOWN_LINKS=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/perlite/.js/perlite.js b/perlite/.js/perlite.js index b13348df..1936f64d 100644 --- a/perlite/.js/perlite.js +++ b/perlite/.js/perlite.js @@ -377,7 +377,13 @@ function getContent(str, home = false, popHover = false, anchor = "") { var target = urlParams.get('link'); target = encodeURIComponent(target); } else { - target = unslugURL(window.location.pathname) + target = unslugURL(this.pathname) + target = encodeURIComponent(target); + } + + + if (this.href.split('#').length > 1) { + return; } // get content of link @@ -396,15 +402,19 @@ function getContent(str, home = false, popHover = false, anchor = "") { } mdContent = $("#mdContent")[0] - // handle pop up and hover + // handle pop up and hover } else { + + if (result.trim() == "") { + return; + } // set content $("#mdHoverContent").html(result); $("#popUpContent").html(result); // set title - var title = $("div.mdTitleHide")[1].innerText; + var title = $("div.mdTitleHide").eq(1).text() || ""; title = title.substring(1) titleElements = title.split('/') title = titleElements.splice(-1) @@ -452,6 +462,10 @@ function getContent(str, home = false, popHover = false, anchor = "") { // Add copyable anchors to all headings (h1–h6) document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((heading) => { + + // Skip if heading already has a copy icon + if (heading.querySelector(".copy-icon")) return; + // Ensure each heading has an ID (generate one if missing) if (!heading.id) { heading.id = heading.textContent diff --git a/perlite/.styles/perlite.css b/perlite/.styles/perlite.css index 60b7a1d5..76e39424 100644 --- a/perlite/.styles/perlite.css +++ b/perlite/.styles/perlite.css @@ -141,6 +141,10 @@ h1, h2, h3, h4 { } +.popover.hover-popover > * { + width: 100%; +} + span.cm-highlight { background-color: var(--text-highlight-bg); color: var(--text-normal); diff --git a/perlite/Demo/Demo Documents/Links and Embedded.md b/perlite/Demo/Demo Documents/Links and Embedded.md index 2e8be1d4..825a5498 100644 --- a/perlite/Demo/Demo Documents/Links and Embedded.md +++ b/perlite/Demo/Demo Documents/Links and Embedded.md @@ -6,6 +6,8 @@ Just a link: https://github.com/secure-77/Perlite [Link with custom Name](https://github.com/secure-77/Perlite) +[Internal Markdown Link](Markdown Samples) + # Files Link to another markdown file: [[Markdown Samples]] diff --git a/perlite/content.php b/perlite/content.php index 6cb850e1..eb509108 100644 --- a/perlite/content.php +++ b/perlite/content.php @@ -64,6 +64,7 @@ function parseContent($requestFile) global $allowedFileLinkTypes; global $htmlSafeMode; global $relPathes; + global $internalMarkdownLinks; $Parsedown = new PerliteParsedown(); @@ -82,6 +83,16 @@ function parseContent($requestFile) return; } + // convert internal markdown links to obsidian style links + if ($internalMarkdownLinks) { + + $replaces = '[[\\2|\\1]]'; + $pattern = array('/\[(.*?)\]\((?![^)]*?:)([^)]+)\)/'); + $content = preg_replace($pattern, $replaces, $content); + + } + + $wordCount = str_word_count($content); $charCount = strlen($content); $content = $Parsedown->text($content); diff --git a/perlite/helper.php b/perlite/helper.php index b51e3747..f4c82b23 100644 --- a/perlite/helper.php +++ b/perlite/helper.php @@ -1,7 +1,7 @@ \ No newline at end of file From f0f8886cc433ff5291ef2ce8294d8ef7b122472d Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Thu, 13 Nov 2025 01:33:53 +0100 Subject: [PATCH 13/27] implemented Hidden text displayed Fixes #160 --- Changelog.md | 1 + perlite/.src/PerliteParsedown.php | 25 ++++++++++++++++++- .../Demo/Demo Documents/Links and Embedded.md | 1 - 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index a4c743e9..b287aba7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ - show random button only when graph-data is availible [#168](https://github.com/secure-77/Perlite/issues/168) - fixed some pop hover issues (use with 100%) and show content - implemented support for internal Markdown Links [#170](https://github.com/secure-77/Perlite/issues/170) +- implemented hidden text feature [#160](https://github.com/secure-77/Perlite/issues/160) diff --git a/perlite/.src/PerliteParsedown.php b/perlite/.src/PerliteParsedown.php index cc6001fc..9bbd5496 100644 --- a/perlite/.src/PerliteParsedown.php +++ b/perlite/.src/PerliteParsedown.php @@ -534,7 +534,7 @@ protected function blockYouTube($Line) # extend to obsidian tags - protected $inlineMarkerList = '!"*$_#&[:<>`~\\='; + protected $inlineMarkerList = '!"*$_#&[:<>`~\\=%'; protected $InlineTypes = array( '"' => array('SpecialCharacter'), '!' => array('Image'), @@ -551,6 +551,7 @@ protected function blockYouTube($Line) '~' => array('Strikethrough'), '\\' => array('EscapeSequence'), '=' => array('Highlight'), + '%' => array('Hidden'), ); @@ -579,6 +580,28 @@ protected function inlineHighlight($Excerpt) } + # handle hidden code + protected function inlineHidden($Excerpt) + { + $marker = $Excerpt['text'][1]; + + if (preg_match('/^%%(.+?)%%/s', $Excerpt['text'], $matches)) + { + $content = ""; + $Inline = array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'span', + 'text' => $content, + ), + ); + + return $Inline; + } + + } + + # handle katex code diff --git a/perlite/Demo/Demo Documents/Links and Embedded.md b/perlite/Demo/Demo Documents/Links and Embedded.md index 825a5498..babd74a5 100644 --- a/perlite/Demo/Demo Documents/Links and Embedded.md +++ b/perlite/Demo/Demo Documents/Links and Embedded.md @@ -6,7 +6,6 @@ Just a link: https://github.com/secure-77/Perlite [Link with custom Name](https://github.com/secure-77/Perlite) -[Internal Markdown Link](Markdown Samples) # Files From ba94dd13652645531dc7900ca3271088f4820618 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:34:00 +0100 Subject: [PATCH 14/27] issue rendering bug in code blocks Fixes #177 --- .gitignore | 2 + Changelog.md | 4 +- perlite/.src/PerliteParsedown.php | 439 +++++++++++++----- .../Callouts and Frontmatter.md | 4 +- perlite/content.php | 285 +----------- 5 files changed, 351 insertions(+), 383 deletions(-) diff --git a/.gitignore b/.gitignore index 970751a9..2fc64646 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ perlite/Link Tests/folder1/folder 2/docs/image_subFolder.png perlite/Link Tests/folder1/folder 2/docs/subfolder_document.md perlite/sec.jpg perlite/Demo/Demo Documents/test.md +perlite/Demo/Demo Documents/test2/test.md +perlite/Demo/Demo Documents/test2/Markdown Samples.md diff --git a/Changelog.md b/Changelog.md index b287aba7..ecf56f44 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,8 +8,10 @@ - added headline copy link button [#156](https://github.com/secure-77/Perlite/issues/156) - show random button only when graph-data is availible [#168](https://github.com/secure-77/Perlite/issues/168) - fixed some pop hover issues (use with 100%) and show content -- implemented support for internal Markdown Links [#170](https://github.com/secure-77/Perlite/issues/170) - implemented hidden text feature [#160](https://github.com/secure-77/Perlite/issues/160) +- eleminated regex-based post-processing by vibe code implemented this into PerliteParsedown fixed issue [#177](https://github.com/secure-77/Perlite/issues/177) + +need to move this to PerliteParsedown: implemented support for internal Markdown Links [#170](https://github.com/secure-77/Perlite/issues/170) diff --git a/perlite/.src/PerliteParsedown.php b/perlite/.src/PerliteParsedown.php index 9bbd5496..061dc206 100644 --- a/perlite/.src/PerliteParsedown.php +++ b/perlite/.src/PerliteParsedown.php @@ -1,7 +1,7 @@ path = $path; + $this->uriPath = $uriPath; + $this->allowedFileLinkTypes = $allowedFileLinkTypes; + $this->allowedImageTypes = $allowedImageTypes; + + $this->InlineTypes['!'][] = 'InternalEmbed'; + $this->InlineTypes['['][] = 'InternalLink'; $this->BlockTypes['!'] = array('YouTube'); + } function text($text) @@ -489,26 +524,23 @@ protected function blockHeader($Line) protected function blockYouTube($Line) { - if ( ! isset($Line['text'][1]) or $Line['text'][1] !== '[') - { + if (!isset($Line['text'][1]) or $Line['text'][1] !== '[') { return; } - $Line['text']= substr($Line['text'], 1); + $Line['text'] = substr($Line['text'], 1); $Link = $this->inlineLink($Line); - if ($Link === null) - { + if ($Link === null) { return; } // See: https://stackoverflow.com/a/64320469 $yt = preg_match('%(?:youtube(?:-nocookie)?\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^"&?/ ]{11})%i', $Link['element']['attributes']['href'], $match); - if (! $yt) - { + if (!$yt) { return; } @@ -520,10 +552,11 @@ protected function blockYouTube($Line) 'handler' => 'line', 'attributes' => array( - 'class' => 'external-embed mod-receives-events', 'sandbox' => 'allow-forms allow-presentation allow-same-origin allow-scripts allow-modals allow-popups', + 'class' => 'external-embed mod-receives-events', + 'sandbox' => 'allow-forms allow-presentation allow-same-origin allow-scripts allow-modals allow-popups', 'allow' => 'fullscreen', 'frameborder' => '0', - 'src' => 'https://www.youtube.com/embed/'. $youtubeId, + 'src' => 'https://www.youtube.com/embed/' . $youtubeId, ), ), @@ -532,7 +565,6 @@ protected function blockYouTube($Line) return $Block; } - # extend to obsidian tags protected $inlineMarkerList = '!"*$_#&[:<>`~\\=%'; protected $InlineTypes = array( @@ -554,15 +586,12 @@ protected function blockYouTube($Line) '%' => array('Hidden'), ); - - # handle highlight code protected function inlineHighlight($Excerpt) { $marker = $Excerpt['text'][1]; - if (preg_match('/^==(.+?)==/s', $Excerpt['text'], $matches)) - { + if (preg_match('/^==(.+?)==/s', $Excerpt['text'], $matches)) { $content = $matches[1]; $Inline = array( 'extent' => strlen($matches[0]), @@ -579,14 +608,12 @@ protected function inlineHighlight($Excerpt) } } - # handle hidden code protected function inlineHidden($Excerpt) { $marker = $Excerpt['text'][1]; - if (preg_match('/^%%(.+?)%%/s', $Excerpt['text'], $matches)) - { + if (preg_match('/^%%(.+?)%%/s', $Excerpt['text'], $matches)) { $content = ""; $Inline = array( 'extent' => strlen($matches[0]), @@ -597,19 +624,15 @@ protected function inlineHidden($Excerpt) ); return $Inline; - } - - } - - + } + } # handle katex code protected function inlineKatex($Excerpt) { $marker = $Excerpt['text'][0]; - if (preg_match('/^(\\'.$marker.'+)[ ]*(.+?)[ ]*(? array( 'name' => 'div', - 'elements' => array( - array( + 'elements' => array( + array( 'name' => 'div', 'attributes' => array( 'class' => 'HyperMD-list-line HyperMD-list-line-1 HyperMD-task-line cm-line', 'data-task' => $isActive, - ), ), - array( - 'name' => 'label', - 'attributes' => array('class' => 'task-list-label'), - 'elements' => array( - array( - 'name' => 'input', - 'attributes' => array( - 'class' => 'task-list-item-checkbox', - 'type' => 'checkbox', - 'data-task' => $isActive, - $checked => '', - ), - ), - array( - 'name' => 'label', - 'attributes' => array('class' => 'cm-widgetBuffer'), - 'text' => $text, + ), + array( + 'name' => 'label', + 'attributes' => array('class' => 'task-list-label'), + 'elements' => array( + array( + 'name' => 'input', + 'attributes' => array( + 'class' => 'task-list-item-checkbox', + 'type' => 'checkbox', + 'data-task' => $isActive, + $checked => '', ), ), - ), + array( + 'name' => 'label', + 'attributes' => array('class' => 'cm-widgetBuffer'), + 'text' => $text, + ), + ), + ), ), ), ); - - + + return $Block; } - if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) - { + if (preg_match('/^(' . $pattern . '[ ]+)(.*)/', $Line['text'], $matches)) { $Block = array( 'indent' => $Line['indent'], 'pattern' => $pattern, @@ -730,12 +752,10 @@ protected function blockList($Line) ), ); - if($name === 'ol') - { + if ($name === 'ol') { $listStart = stristr($matches[0], '.', true); - if($listStart !== '1') - { + if ($listStart !== '1') { $Block['element']['attributes'] = array('start' => $listStart); } } @@ -748,7 +768,7 @@ protected function blockList($Line) ), ); - $Block['element']['text'] []= & $Block['li']; + $Block['element']['text'][] = &$Block['li']; return $Block; } @@ -756,8 +776,8 @@ protected function blockList($Line) protected function blockListContinue($Line, array $Block) { - - + + if (preg_match('/(- \[(x| )\])(.*)/', $Line['text'], $matches)) { $text = isset($matches[3]) ? $matches[3] : ''; @@ -770,52 +790,50 @@ protected function blockListContinue($Line, array $Block) - - + + $conBlock = array( - 'name' => 'div', - 'attributes' => array( - 'class' => 'HyperMD-list-line HyperMD-list-line-1 HyperMD-task-line cm-line', - 'data-task' => $isActive, - ), - 'elements' => array( - array( - 'name' => 'label', - 'attributes' => array('class' => 'task-list-label'), - 'elements' => array( - array( - 'name' => 'input', - 'attributes' => array( - 'class' => 'task-list-item-checkbox', - 'type' => 'checkbox', - 'data-task' => $isActive, - $checked => '', - ), - ), - array( - 'name' => 'label', - 'attributes' => array('class' => 'cm-widgetBuffer'), - 'text' => $text, + 'name' => 'div', + 'attributes' => array( + 'class' => 'HyperMD-list-line HyperMD-list-line-1 HyperMD-task-line cm-line', + 'data-task' => $isActive, + ), + 'elements' => array( + array( + 'name' => 'label', + 'attributes' => array('class' => 'task-list-label'), + 'elements' => array( + array( + 'name' => 'input', + 'attributes' => array( + 'class' => 'task-list-item-checkbox', + 'type' => 'checkbox', + 'data-task' => $isActive, + $checked => '', ), ), + array( + 'name' => 'label', + 'attributes' => array('class' => 'cm-widgetBuffer'), + 'text' => $text, + ), ), - ) + ), + ) ); - - - $Block['element']['elements'][ ] = & $conBlock; - + + + $Block['element']['elements'][] = &$conBlock; + return $Block; } $Block['indent'] = isset($Block['indent']) ? $Block['indent'] : '0'; - if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) - { - - if (isset($Block['interrupted'])) - { - $Block['li']['text'] []= ''; + if ($Block['indent'] === $Line['indent'] and preg_match('/^' . $Block['pattern'] . '(?:[ ]+(.*)|$)/', $Line['text'], $matches)) { + + if (isset($Block['interrupted'])) { + $Block['li']['text'][] = ''; $Block['loose'] = true; @@ -834,32 +852,29 @@ protected function blockListContinue($Line, array $Block) ), ); - $Block['element']['text'] []= & $Block['li']; + $Block['element']['text'][] = &$Block['li']; return $Block; } - if ($Line['text'][0] === '[' and $this->blockReference($Line)) - { + if ($Line['text'][0] === '[' and $this->blockReference($Line)) { return $Block; } - if ( ! isset($Block['interrupted'])) - { + if (!isset($Block['interrupted'])) { $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); - $Block['li']['text'] []= $text; + $Block['li']['text'][] = $text; return $Block; } - if ($Line['indent'] > 0) - { - $Block['li']['text'] []= ''; + if ($Line['indent'] > 0) { + $Block['li']['text'][] = ''; $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); - $Block['li']['text'] []= $text; + $Block['li']['text'][] = $text; unset($Block['interrupted']); @@ -1044,7 +1059,7 @@ protected function lines(array $lines) foreach ($parts as $part) { - + $shortage = 0; if (function_exists('mb_strlen')) { $shortage = 4 - (mb_strlen($input ?? '', 'UTF-8') % 4); @@ -1053,7 +1068,7 @@ protected function lines(array $lines) $shortage = 4 - (strlen($converted) % 4); } else { // Fallback: count bytes (not characters) - $shortage = 4 - (strlen($input) % 4); + $shortage = 4 - (strlen($input) % 4); } $line .= str_repeat(' ', $shortage); @@ -1189,4 +1204,208 @@ protected function lines(array $lines) return $markup; } + + protected function inlineInternalLink($Excerpt) + { + if (!preg_match('/^\[\[(.+?)\]\]/', $Excerpt['text'], $matches)) { + return; + } + + $raw = $matches[1]; + + // Split Obsidian-style: file|label|popup + $parts = explode('|', $raw); + $linkFile = $parts[0]; + + $ext = pathinfo($linkFile, PATHINFO_EXTENSION); + $openNewTab = false; + + if (in_array($ext, $this->allowedFileLinkTypes)) { + $openNewTab = true; + } + + $linkText = $parts[1] ?? $parts[0]; + $isPopup = isset($parts[2]); + + $popupClass = $isPopup ? ' internal-popup' : ''; + $popupIcon = $isPopup ? $this->popupIconSvg() : ''; + + // Determine relative traversal + $path = $this->path; + + + if (str_starts_with($linkFile, '../')) { + $depth = substr_count($linkFile, '../'); + $segments = explode('/', $this->path); + $segments = array_slice($segments, 0, count($segments) - $depth); + $path = implode('/', $segments); + $linkFile = preg_replace('#^(\.\./)+#', '', $linkFile); + } + + + if ($openNewTab == false) { + $segments = explode('/', $path); + $segments = array_slice($segments, 1, count($segments)); + $path = implode('/', $segments); + } + + + $urlPath = ltrim($path . '/' . $linkFile, '/'); + + // Same-document anchor + if (str_starts_with($raw, '#')) { + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $linkText, + 'attributes' => array( + 'href' => '#' . ltrim($raw, '#'), + 'class' => 'internal-link' . $popupClass, + ), + ), + ); + } + + // URL normalization (ported exactly) + if ($openNewTab == false) { + $urlPath = str_replace('&', '&', $urlPath); + $urlPath = str_replace('%23', '#', $urlPath); + $urlPath = str_replace('~', '%80', $urlPath); + $urlPath = str_replace('-', '~', $urlPath); + $urlPath = str_replace(' ', '-', $urlPath); + } + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'handler' => 'line', + 'text' => $linkText, + 'attributes' => array( + 'href' => $this->uriPath . $urlPath, + 'class' => 'internal-link' . $popupClass, + 'target' => $openNewTab ? '_blank' : null, + 'rel' => $openNewTab ? 'noopener noreferrer' : null, + ), + 'suffix' => $popupIcon, + ), + ); + } + + protected function inlineInternalEmbed($Excerpt) + { + if (!preg_match('/^!\[\[(.+?)\]\]/', $Excerpt['text'], $m)) { + return; + } + + $raw = $m[1]; + $parts = explode('|', $raw); + + $file = $parts[0]; + $mod1 = $parts[1] ?? null; + $mod2 = $parts[2] ?? null; + + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + $src = rtrim($this->uriPath . $this->path, '/') . '/' . $file; + + /* ---------- PDF ---------- */ + if ($ext === 'pdf') { + return array( + 'extent' => strlen($m[0]), + 'element' => array( + 'name' => 'embed', + 'attributes' => array( + 'src' => $src, + 'type' => 'application/pdf', + 'style' => 'min-height:100vh;width:100%', + ), + ), + ); + } + + /* ---------- Video / Audio ---------- */ + if (in_array($ext, array('mp4', 'm4a'))) { + return array( + 'extent' => strlen($m[0]), + 'element' => array( + 'name' => 'video', + 'handler' => 'line', + 'attributes' => array( + 'controls' => true, + 'src' => $src, + 'type' => $ext === 'mp4' ? 'video/mp4' : 'audio/x-m4a', + ), + 'text' => + 'Download ' . basename($file) . '', + ), + ); + } + + /* ---------- Images ---------- */ + if (in_array($ext, $this->allowedImageTypes)) { + return $this->buildInternalImage($file, $parts, strlen($m[0])); + } + } + + protected function buildInternalImage($file, array $parts, $extent) + { + $src = rtrim($this->uriPath . $this->path, '/') . '/' . $file; + + $alt = ''; + $class = 'images'; + $width = null; + $height = null; + + foreach ($parts as $part) { + if (preg_match('/^(\d*)x(\d*)$/', $part, $m)) { + $width = $m[1] ?: null; + $height = $m[2] ?: null; + } elseif ($part === 'center' || $part === 'right') { + $class .= ' ' . $part; + } elseif ($part !== $file) { + $alt = $part; + } + } + + + return [ + 'extent' => $extent, + 'element' => [ + 'name' => 'p', + 'elements' => [ // <- top-level nested elements go here + [ + 'name' => 'a', + 'attributes' => ['href' => '#', 'class' => 'pop'], + 'elements' => [ // nested elements + [ + 'name' => 'img', + 'attributes' => array_filter([ + 'src' => $src, + 'class' => $class, + 'alt' => $alt ?: 'image', + 'width' => $width, + 'height' => $height, + ]), + ] + ], + ] + ], + ], + ]; + + } + + + protected function popupIconSvg() + { + return ' + + + + + '; + } + } diff --git a/perlite/Demo/Demo Documents/Callouts and Frontmatter.md b/perlite/Demo/Demo Documents/Callouts and Frontmatter.md index e9151c2a..6d6d1424 100644 --- a/perlite/Demo/Demo Documents/Callouts and Frontmatter.md +++ b/perlite/Demo/Demo Documents/Callouts and Frontmatter.md @@ -11,6 +11,8 @@ aliases: Im a YAML front matter document +[[#Collapsible(Callouts]] + # Blockquots @@ -61,7 +63,7 @@ Im a YAML front matter document > Lorem ipsum dolor sit amet -## Collapsible Callouts +## Collapsible(Callouts > [!note]+ Information the user should notice even if skimming. > Test diff --git a/perlite/content.php b/perlite/content.php index eb509108..6671c11e 100644 --- a/perlite/content.php +++ b/perlite/content.php @@ -1,7 +1,7 @@ setSafeMode($htmlSafeMode); - $Parsedown->setBreaksEnabled($lineBreaks); - $cleanFile = ''; - // call menu again to refresh the array menu($rootDir); $path = ''; - // get and parse the content, return if no content is there $content = getContent($requestFile); if ($content === '') { return; } - // convert internal markdown links to obsidian style links - if ($internalMarkdownLinks) { - - $replaces = '[[\\2|\\1]]'; - $pattern = array('/\[(.*?)\]\((?![^)]*?:)([^)]+)\)/'); - $content = preg_replace($pattern, $replaces, $content); - - } - - - $wordCount = str_word_count($content); - $charCount = strlen($content); - $content = $Parsedown->text($content); - // Relative or absolute pathes if ($relPathes) { $path = $startDir; - $mdpath = ''; } else { - $mdpath = $path; $path = $startDir . $path; } - // fix links (not used) - // $oldPath = $startDir . $path; - - // // fix relativ links in parent folders - // $pattern = array('/(\[\[)(\.\.\/.*)(\]\])/'); - // $content = fixLinks($pattern, $content, $oldPath, false); - - // // // fix relativ links in same folders - // $pattern = array('/(\[\[)+([^\/]+?)(\]\])/'); - // $content = fixLinks($pattern, $content, $path, true); - - // // fix relativ links in subfolder and same folders - // $pattern = array('/(\[\[)(?!Demo Documents\/)(.+)(\]\])/'); - // $content = fixLinks($pattern, $content, $path, true); - - $linkFileTypes = implode('|', $allowedFileLinkTypes); - $allowedImageTypes = '(\.png|\.jpg|\.jpeg|\.svg|\.gif|\.bmp|\.tif|\.tiff|\.webp)'; - - $src_path = $uriPath . $path; - - // embedded pdf links - $replaces = ''; - $pattern = array('/(\!\[\[)(.*?.(?:pdf))(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); - - // embedded mp4 links - $replaces = ' - '; - $pattern = array('/(\!\[\[)(.*?.(?:mp4))(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); - - - // embedded m4a links - $replaces = ' - '; - $pattern = array('/(\!\[\[)(.*?.(?:m4a))(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); - - - // links to other files with Alias - $replaces = '\\3'; - $pattern = array('/(\[\[)(.*?.(?:' . $linkFileTypes . '))\|(.*)(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); - - // links to other files without Alias - $replaces = '\\2'; - $pattern = array('/(\[\[)(.*?.(?:' . $linkFileTypes . '))(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); - - // img links with external target link - $replaces = 'noreferrer">image not found'; - $pattern = array('/noreferrer">(\!?\[\[)(.*?)' . $allowedImageTypes . '\|?(\d*)x?(\d*)(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); - - // img links with size - $replaces = '

image not found

'; - $pattern = array('/(\!?\[\[)(.*?)' . $allowedImageTypes . '\|?(\d*)x?(\d*)(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); + $Parsedown = new PerliteParsedown($path, $uriPath, $allowedFileLinkTypes); + $Parsedown->setSafeMode($htmlSafeMode); + $Parsedown->setBreaksEnabled($lineBreaks); + $cleanFile = ''; - // centerise or right align images with "center"/"right" directive - $pattern = '/(\!?\[\[)(.*?)' . $allowedImageTypes . '\|?(center|right)\|?(\d*)x?(\d*)(\]\])/'; - $replaces = function ($matches) use ($src_path) { - $class = "images"; // Default class for all images - if (strpos($matches[4], 'center') !== false) { - $class .= " center"; // Add 'center' class - } elseif (strpos($matches[4], 'right') !== false) { - $class .= " right"; // Add 'right' class - } - $width = $matches[5] ?? 'auto'; - $height = $matches[6] ?? 'auto'; - return '

'; - }; - $content = preg_replace_callback($pattern, $replaces, $content); - // img links with captions and size - $replaces = '

\\4

'; - $pattern = array('/(\!?\[\[)(.*?)' . $allowedImageTypes . '\|?(.+\|)\|?(\d*)x?(\d*)(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); + // convert internal markdown links to obsidian style links + if ($internalMarkdownLinks) { - // img links with captions - $replaces = '

\\4

'; - $pattern = array('/(\!?\[\[)(.*?)' . $allowedImageTypes . '\|?(.+|)(\]\])/'); - $content = preg_replace($pattern, $replaces, $content); + $replaces = '[[\\2|\\1]]'; + $pattern = array('/\[(.*?)\]\((?![^)]*?:)([^)]+)\)/'); + $content = preg_replace($pattern, $replaces, $content); + } - // handle internal site links - // search for links outside of the current folder - $pattern = array('/(\[\[)(?:\.\.\/)+(.*?)(\]\])/'); - $content = translateLink($pattern, $content, $path, false); - // search for links in the same folder - $pattern = array('/(\[\[)(.*?)(\]\])/'); - $content = translateLink($pattern, $content, $mdpath, true); + $wordCount = str_word_count($content); + $charCount = strlen($content); + $content = $Parsedown->text($content); // add some meta data @@ -222,163 +122,6 @@ function parseContent($requestFile) } -// translate relativ links (not used) -// function fixLinks($pattern, $content, $path, $sameFolder) { - -// return preg_replace_callback($pattern, -// function($matches) use ($path, $sameFolder) { - -// $newAbPath = $path; -// echo "path: " . $path; -// echo "
"; -// $pathSplit = explode("/",$path); -// $linkFilePart = $matches[2]; -// $esapeSequence = "#regex_run#"; -// echo '$matches[1]: ' . $matches[1]; -// echo '
'; -// echo '$matches[2]: ' . $linkFilePart; -// echo '
'; - -// $linkDesc = ""; - -// # handle custom link comments and sizes -// $splitLink = explode("|", $matches[2]); -// if (count($splitLink) > 1) { -// $linkFilePart = $splitLink[0]; -// array_shift($splitLink); -// $linkDesc = '|' .implode("|", $splitLink); -// } - - -// // do extra stuff to get the absolute path -// if ($sameFolder == false) { -// $countDirs = count(explode("../",$linkFilePart)); -// $countDirs = $countDirs -1; -// $newPath = array_splice($pathSplit, 1, -$countDirs); -// $newAbPath = implode('/', $newPath); -// echo "new file path: " . $newAbPath; -// echo "
"; -// echo "old file path: " . $linkFilePart; -// echo "
"; -// } - -// if (substr($newAbPath,0,1) == '/') { -// $newAbPath = substr($newAbPath,1); -// } - - -// $origPath = explode('/', $linkFilePart); -// array_pop($origPath); -// $origPath = implode('/', $origPath); -// //check if its already an absolut path -// echo "new file path: " . $newAbPath; -// echo "
"; -// echo "old file path: " . $origPath; -// echo "
"; - - -// if (count_chars($origPath) >= count_chars($newAbPath)) { - -// $urlPath = $linkFilePart; - -// } else { - -// $linkFile = str_replace("../","",$linkFilePart); -// $urlPath = $newAbPath. '/'. $linkFile; -// } - - -// return '[['.$urlPath.$linkDesc.']]'; -// } -// ,$content); -// } - - - -//internal links -// can be simplified (no need of path translation) -function translateLink($pattern, $content, $path, $sameFolder) -{ - - return preg_replace_callback( - $pattern, - function ($matches) use ($path, $sameFolder) { - - - global $uriPath; - $newAbPath = $path; - $pathSplit = explode("/", $path); - $linkName_full = $matches[2]; - $linkName = $linkName_full; - $linkFile = $matches[2]; - - # handle custom internal obsidian links - $splitLink = explode("|", $matches[2]); - if (count($splitLink) > 1) { - - $linkFile = $splitLink[0]; - $linkName = $splitLink[1]; - } - - # handle internal popups - $popupClass = ''; - $popUpIcon = ''; - - if (count($splitLink) > 2) { - - $popupClass = ' internal-popup'; - $popUpIcon = ''; - } - - - // do extra stuff to get the absolute path - if ($sameFolder == false) { - $countDirs = count(explode("../", $matches[0])); - $countDirs = $countDirs - 1; - $newPath = array_splice($pathSplit, 1, -$countDirs); - $newAbPath = implode('/', $newPath); - } - - - $urlPath = $newAbPath . '/' . $linkFile; - if (substr($urlPath, 0, 1) == '/') { - #$urlPath = '/' . $urlPath; - $urlPath = substr($urlPath, 1); - } - - $refName = ''; - - # if same document heading reference - if (substr($linkName_full, 0, 1) == '#') { - - $splitLink = explode("#", $urlPath); - $urlPath = ''; - $refName = $splitLink[1]; - $refName = '#' . $refName; - $href = 'href="'; - } else { - #$href = 'href="?link='; - $href = 'href="' . $uriPath; - } - - $urlPath = str_replace('&', '&', $urlPath); - - #$urlPath = rawurlencode($urlPath); - $urlPath = str_replace('%23', '#', $urlPath); - - $urlPath = str_replace('~', '%80', $urlPath); - $urlPath = str_replace('-', '~', $urlPath); - $urlPath = str_replace(' ', '-', $urlPath); - - - return '' . $linkName . '' . $popUpIcon; - } - , - $content - ); -} - - // read content from file function getContent($requestFile) { From c7722e4d9f01ad547ec3008be0647493b7d6e151 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Sun, 28 Dec 2025 00:11:58 +0100 Subject: [PATCH 15/27] issue Wikilinks/image within tables formatting bug Fixes #142 --- Changelog.md | 6 +- docker-compose-build.yml | 1 - docker-compose-dev.yml | 1 - docker-compose.yml | 1 - perlite/.src/PerliteParsedown.php | 205 +++++++++++++++++++++++------- perlite/content.php | 13 +- perlite/helper.php | 5 - perlite/index.php | 4 +- perlite/settings.php | 2 +- 9 files changed, 164 insertions(+), 74 deletions(-) diff --git a/Changelog.md b/Changelog.md index ecf56f44..51246a0a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,9 +9,9 @@ - show random button only when graph-data is availible [#168](https://github.com/secure-77/Perlite/issues/168) - fixed some pop hover issues (use with 100%) and show content - implemented hidden text feature [#160](https://github.com/secure-77/Perlite/issues/160) -- eleminated regex-based post-processing by vibe code implemented this into PerliteParsedown fixed issue [#177](https://github.com/secure-77/Perlite/issues/177) - -need to move this to PerliteParsedown: implemented support for internal Markdown Links [#170](https://github.com/secure-77/Perlite/issues/170) +- eleminated regex-based post-processing by implementing it into PerliteParsedown, this fixed issue [#177](https://github.com/secure-77/Perlite/issues/177) +- implemented support for internal Markdown Links [#170](https://github.com/secure-77/Perlite/issues/170) +- implemented support for parameter based obsidian image attribute syntax to fix issue [#142](https://github.com/secure-77/Perlite/issues/142) diff --git a/docker-compose-build.yml b/docker-compose-build.yml index 6fa7f07a..8b70c6e7 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -17,7 +17,6 @@ services: - FONT_SIZE=15 - HTML_SAFE_MODE=true - ZETTELKASTEN_FILENAMES_ENABLED=false - - INTERNAL_MARKDOWN_LINKS=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index e9f0c6f1..94c6db87 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -18,7 +18,6 @@ services: - FONT_SIZE=15 - HTML_SAFE_MODE=true - ZETTELKASTEN_FILENAMES_ENABLED=false - - INTERNAL_MARKDOWN_LINKS=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/docker-compose.yml b/docker-compose.yml index bc3e2f20..51f35535 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,6 @@ services: - FONT_SIZE=15 - HTML_SAFE_MODE=true - ZETTELKASTEN_FILENAMES_ENABLED=false - - INTERNAL_MARKDOWN_LINKS=false - TEMP_PATH=/tmp - SITE_TITLE=Demo - SITE_TYPE=article diff --git a/perlite/.src/PerliteParsedown.php b/perlite/.src/PerliteParsedown.php index 061dc206..7849d144 100644 --- a/perlite/.src/PerliteParsedown.php +++ b/perlite/.src/PerliteParsedown.php @@ -16,17 +16,37 @@ class PerliteParsedown extends Parsedown protected $path; + protected $uriPath; + protected $niceLinks; protected $allowedFileLinkTypes; protected $allowedImageTypes; - // public function __construct() - // { + protected $inlineMarkerList = '!"*$_#&[:<>`~\\=%'; + + protected $InlineTypes = array( + '"' => array('SpecialCharacter'), + '!' => array('Image', 'InternalEmbed'), + '&' => array('SpecialCharacter'), + '*' => array('Emphasis'), + ':' => array('Url'), + '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), + '>' => array('SpecialCharacter'), + '[' => array('Link', 'InternalMarkdownLink', 'InternalLink'), + '#' => array('Tag'), + '$' => array('Katex'), + '_' => array('Emphasis'), + '`' => array('Code'), + '~' => array('Strikethrough'), + '\\' => array('EscapeSequence'), + '=' => array('Highlight'), + '%' => array('Hidden'), + ); - // } public function __construct( $path = '', $uriPath = '/', + $niceLinks = false, array $allowedFileLinkTypes = array('mp4', 'm4a', 'pdf'), array $allowedImageTypes = array( 'png', @@ -40,15 +60,13 @@ public function __construct( 'webp' ) ) { - //parent::__construct(); $this->path = $path; $this->uriPath = $uriPath; + $this->niceLinks = $niceLinks; $this->allowedFileLinkTypes = $allowedFileLinkTypes; $this->allowedImageTypes = $allowedImageTypes; - $this->InlineTypes['!'][] = 'InternalEmbed'; - $this->InlineTypes['['][] = 'InternalLink'; $this->BlockTypes['!'] = array('YouTube'); } @@ -565,26 +583,6 @@ protected function blockYouTube($Line) return $Block; } - # extend to obsidian tags - protected $inlineMarkerList = '!"*$_#&[:<>`~\\=%'; - protected $InlineTypes = array( - '"' => array('SpecialCharacter'), - '!' => array('Image'), - '&' => array('SpecialCharacter'), - '*' => array('Emphasis'), - ':' => array('Url'), - '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), - '>' => array('SpecialCharacter'), - '[' => array('Link'), - '#' => array('Tag'), - '$' => array('Katex'), - '_' => array('Emphasis'), - '`' => array('Code'), - '~' => array('Strikethrough'), - '\\' => array('EscapeSequence'), - '=' => array('Highlight'), - '%' => array('Hidden'), - ); # handle highlight code protected function inlineHighlight($Excerpt) @@ -1240,15 +1238,22 @@ protected function inlineInternalLink($Excerpt) $segments = array_slice($segments, 0, count($segments) - $depth); $path = implode('/', $segments); $linkFile = preg_replace('#^(\.\./)+#', '', $linkFile); + + // use only the file name for nice links + if ($this->niceLinks == true) { + $segments = explode('/', $linkText); + $segments = array_slice($segments, count($segments) - 1, 1); + $linkText = $segments[0]; + } } - + if ($openNewTab == false) { $segments = explode('/', $path); $segments = array_slice($segments, 1, count($segments)); - $path = implode('/', $segments); + $path = implode('/', $segments); } - + $urlPath = ltrim($path . '/' . $linkFile, '/'); @@ -1293,15 +1298,58 @@ protected function inlineInternalLink($Excerpt) ); } + protected function inlineInternalMarkdownLink($Excerpt) + { + // Match [label](path) — but NOT external URLs + if (!preg_match('/^\[([^\]]+)\]\(([^)]+)\)/', $Excerpt['text'], $m)) { + return; + } + + $label = $m[1]; + $path = $m[2]; + + // Reject external links explicitly + if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $path)) { + return; + } + + // Reject protocol-relative URLs + if (str_starts_with($path, '//')) { + return; + } + + // Convert into Obsidian-style payload + // [[path|label]] + $synthetic = '[[' . $path . '|' . $label . ']]'; + + // Delegate to inlineInternalLink() + $result = $this->inlineInternalLink([ + 'text' => $synthetic, + ]); + + if ($result === null) { + return; + } + + // Adjust extent to original Markdown syntax length + $result['extent'] = strlen($m[0]); + + return $result; + } + protected function inlineInternalEmbed($Excerpt) { if (!preg_match('/^!\[\[(.+?)\]\]/', $Excerpt['text'], $m)) { return; } + + $raw = $m[1]; $parts = explode('|', $raw); + + $file = $parts[0]; $mod1 = $parts[1] ?? null; $mod2 = $parts[2] ?? null; @@ -1309,6 +1357,10 @@ protected function inlineInternalEmbed($Excerpt) $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); $src = rtrim($this->uriPath . $this->path, '/') . '/' . $file; + if (str_contains($file, '#')) { + $ext = strtolower(pathinfo(explode('#', $file)[0], PATHINFO_EXTENSION)); + } + /* ---------- PDF ---------- */ if ($ext === 'pdf') { return array( @@ -1343,58 +1395,115 @@ protected function inlineInternalEmbed($Excerpt) ); } - /* ---------- Images ---------- */ + /* ---------- Image ---------- */ if (in_array($ext, $this->allowedImageTypes)) { - return $this->buildInternalImage($file, $parts, strlen($m[0])); + + // syntax: image.png#caption=...&size=... + if (str_contains($file, '#')) { + return $this->buildInternalImageFromFragment( + $file, + strlen($m[0]) + ); + } + + // syntax: image.png|Caption|300x200|center + return $this->buildInternalImageFromLegacy( + $raw, + strlen($m[0]) + ); } + + } - protected function buildInternalImage($file, array $parts, $extent) + + protected function buildInternalImage(string $file, array $attrs, int $extent) { $src = rtrim($this->uriPath . $this->path, '/') . '/' . $file; - $alt = ''; $class = 'images'; + $alt = $attrs['caption'] ?? 'image'; $width = null; $height = null; - foreach ($parts as $part) { - if (preg_match('/^(\d*)x(\d*)$/', $part, $m)) { - $width = $m[1] ?: null; - $height = $m[2] ?: null; - } elseif ($part === 'center' || $part === 'right') { - $class .= ' ' . $part; - } elseif ($part !== $file) { - $alt = $part; - } + if (!empty($attrs['align'])) { + $class .= ' ' . $attrs['align']; } + if (!empty($attrs['size']) && preg_match('/^(\d*)x(\d*)$/', $attrs['size'], $m)) { + $width = $m[1] ?: null; + $height = $m[2] ?: null; + } return [ 'extent' => $extent, 'element' => [ 'name' => 'p', - 'elements' => [ // <- top-level nested elements go here + 'elements' => [ [ 'name' => 'a', - 'attributes' => ['href' => '#', 'class' => 'pop'], - 'elements' => [ // nested elements + 'attributes' => [ + 'href' => '#', + 'class' => 'pop', + ], + 'elements' => [ [ 'name' => 'img', 'attributes' => array_filter([ 'src' => $src, 'class' => $class, - 'alt' => $alt ?: 'image', + 'alt' => $alt, 'width' => $width, 'height' => $height, ]), - ] + ], ], - ] + ], ], ], ]; + } + + + protected function buildInternalImageFromFragment(string $file, int $extent) + { + [$file, $fragment] = explode('#', $file, 2); + + parse_str($fragment, $attrs); + + return $this->buildInternalImage( + $file, + [ + 'caption' => $attrs['caption'] ?? null, + 'size' => $attrs['size'] ?? null, + 'align' => $attrs['align'] ?? null, + ], + $extent + ); + } + + protected function buildInternalImageFromLegacy(string $raw, int $extent) + { + $parts = explode('|', $raw); + $file = array_shift($parts); + + $attrs = [ + 'caption' => null, + 'size' => null, + 'align' => null, + ]; + + foreach ($parts as $part) { + if (preg_match('/^\d*x\d*$/', $part)) { + $attrs['size'] = $part; + } elseif (in_array($part, ['center', 'right'], true)) { + $attrs['align'] = $part; + } elseif ($part !== '') { + $attrs['caption'] = $part; + } + } + return $this->buildInternalImage($file, $attrs, $extent); } diff --git a/perlite/content.php b/perlite/content.php index 6671c11e..4abc1640 100644 --- a/perlite/content.php +++ b/perlite/content.php @@ -64,7 +64,6 @@ function parseContent($requestFile) global $allowedFileLinkTypes; global $htmlSafeMode; global $relPathes; - global $internalMarkdownLinks; // call menu again to refresh the array @@ -87,22 +86,12 @@ function parseContent($requestFile) - $Parsedown = new PerliteParsedown($path, $uriPath, $allowedFileLinkTypes); + $Parsedown = new PerliteParsedown($path, $uriPath,true, $allowedFileLinkTypes); $Parsedown->setSafeMode($htmlSafeMode); $Parsedown->setBreaksEnabled($lineBreaks); $cleanFile = ''; - // convert internal markdown links to obsidian style links - if ($internalMarkdownLinks) { - - $replaces = '[[\\2|\\1]]'; - $pattern = array('/\[(.*?)\]\((?![^)]*?:)([^)]+)\)/'); - $content = preg_replace($pattern, $replaces, $content); - - } - - $wordCount = str_word_count($content); $charCount = strlen($content); $content = $Parsedown->text($content); diff --git a/perlite/helper.php b/perlite/helper.php index f4c82b23..617657bb 100644 --- a/perlite/helper.php +++ b/perlite/helper.php @@ -78,11 +78,6 @@ if (!isset($lineBreaks)) $lineBreaks = empty(getenv('LINE_BREAKS')) ? true : filter_var(getenv('LINE_BREAKS'), FILTER_VALIDATE_BOOLEAN); -// line breaks -if (!isset($internalMarkdownLinks)) - $internalMarkdownLinks = empty(getenv('INTERNAL_MARKDOWN_LINKS')) ? false : filter_var(getenv('INTERNAL_MARKDOWN_LINKS'), FILTER_VALIDATE_BOOLEAN); - - // file types if (empty($allowedFileLinkTypes)) $allowedFileLinkTypes = empty(getenv('ALLOWED_FILE_LINK_TYPES')) ? ['pdf', 'mp4'] : explode(",", getenv('ALLOWED_FILE_LINK_TYPES')); diff --git a/perlite/index.php b/perlite/index.php index 7be6b89f..f86d2f82 100644 --- a/perlite/index.php +++ b/perlite/index.php @@ -48,8 +48,8 @@ - - + + diff --git a/perlite/settings.php b/perlite/settings.php index 2239c631..add095a9 100644 --- a/perlite/settings.php +++ b/perlite/settings.php @@ -32,7 +32,7 @@ $highlightJSLangs = ["powershell", "x86asm"]; $allowedFileLinkTypes = ['pdf', 'mp4']; $tempPath = ""; // path for graph html, leave empty for automatic -$internalMarkdownLinks = false; + // --- Metadata Settings --- $siteType = "article"; From 6d19bb07e59124a06ee4fcd679b4ffe250d237e9 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Sun, 28 Dec 2025 00:55:09 +0100 Subject: [PATCH 16/27] implemented nice links and fixed Code blocks with new lines are broken, when starting directly after another block Fixes #172 --- Changelog.md | 3 ++- docker-compose-build.yml | 1 + docker-compose-dev.yml | 1 + docker-compose.yml | 1 + perlite/.src/PerliteParsedown.php | 23 ++++++++++------------- perlite/content.php | 7 ++++--- perlite/helper.php | 9 +++++++-- perlite/settings.php | 5 +++-- 8 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Changelog.md b/Changelog.md index 51246a0a..9197523a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -12,7 +12,8 @@ - eleminated regex-based post-processing by implementing it into PerliteParsedown, this fixed issue [#177](https://github.com/secure-77/Perlite/issues/177) - implemented support for internal Markdown Links [#170](https://github.com/secure-77/Perlite/issues/170) - implemented support for parameter based obsidian image attribute syntax to fix issue [#142](https://github.com/secure-77/Perlite/issues/142) - +- implemented support for "nice" internal links, this way always the filename only will be displayed without the path +- fixed issue [#172](https://github.com/secure-77/Perlite/issues/172) ## 1.6 diff --git a/docker-compose-build.yml b/docker-compose-build.yml index 8b70c6e7..530390bc 100644 --- a/docker-compose-build.yml +++ b/docker-compose-build.yml @@ -11,6 +11,7 @@ services: - ALLOWED_FILE_LINK_TYPES=pdf,mp4 - HIGHLIGHTJS_LANGS=powershell - DISABLE_POP_HOVER=true + - NICE_LINKS=true - SHOW_TOC=true - SHOW_LOCAL_GRAPH=true - HOME_FILE=README diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 94c6db87..7a1ced05 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -12,6 +12,7 @@ services: - ALLOWED_FILE_LINK_TYPES=pdf,mp4 - HIGHLIGHTJS_LANGS=powershell - DISABLE_POP_HOVER=false + - NICE_LINKS=true - SHOW_TOC=true - SHOW_LOCAL_GRAPH=true - HOME_FILE=README diff --git a/docker-compose.yml b/docker-compose.yml index 51f35535..d1c9cec1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - ALLOWED_FILE_LINK_TYPES=pdf,mp4 - HIGHLIGHTJS_LANGS=powershell - DISABLE_POP_HOVER=false + - NICE_LINKS=true - SHOW_TOC=true - SHOW_LOCAL_GRAPH=true - HOME_FILE=README diff --git a/perlite/.src/PerliteParsedown.php b/perlite/.src/PerliteParsedown.php index 7849d144..0c127db5 100644 --- a/perlite/.src/PerliteParsedown.php +++ b/perlite/.src/PerliteParsedown.php @@ -860,6 +860,9 @@ protected function blockListContinue($Line, array $Block) } if (!isset($Block['interrupted'])) { + if (preg_match('/^[`~]{3,}/', $Line['text'])) { + return null; + } $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); $Block['li']['text'][] = $text; @@ -1239,12 +1242,13 @@ protected function inlineInternalLink($Excerpt) $path = implode('/', $segments); $linkFile = preg_replace('#^(\.\./)+#', '', $linkFile); - // use only the file name for nice links - if ($this->niceLinks == true) { - $segments = explode('/', $linkText); - $segments = array_slice($segments, count($segments) - 1, 1); - $linkText = $segments[0]; - } + } + + // use only the file name for nice links + if ($this->niceLinks == true) { + $segments = explode('/', $linkText); + $segments = array_slice($segments, count($segments) - 1, 1); + $linkText = $segments[0]; } @@ -1343,13 +1347,9 @@ protected function inlineInternalEmbed($Excerpt) return; } - - $raw = $m[1]; $parts = explode('|', $raw); - - $file = $parts[0]; $mod1 = $parts[1] ?? null; $mod2 = $parts[2] ?? null; @@ -1416,7 +1416,6 @@ protected function inlineInternalEmbed($Excerpt) } - protected function buildInternalImage(string $file, array $attrs, int $extent) { $src = rtrim($this->uriPath . $this->path, '/') . '/' . $file; @@ -1464,7 +1463,6 @@ protected function buildInternalImage(string $file, array $attrs, int $extent) ]; } - protected function buildInternalImageFromFragment(string $file, int $extent) { [$file, $fragment] = explode('#', $file, 2); @@ -1506,7 +1504,6 @@ protected function buildInternalImageFromLegacy(string $raw, int $extent) return $this->buildInternalImage($file, $attrs, $extent); } - protected function popupIconSvg() { return ' diff --git a/perlite/content.php b/perlite/content.php index 4abc1640..0a9c1c7c 100644 --- a/perlite/content.php +++ b/perlite/content.php @@ -63,7 +63,8 @@ function parseContent($requestFile) global $lineBreaks; global $allowedFileLinkTypes; global $htmlSafeMode; - global $relPathes; + global $absolutePath; + global $niceLinks; // call menu again to refresh the array @@ -78,7 +79,7 @@ function parseContent($requestFile) // Relative or absolute pathes - if ($relPathes) { + if ($absolutePath) { $path = $startDir; } else { $path = $startDir . $path; @@ -86,7 +87,7 @@ function parseContent($requestFile) - $Parsedown = new PerliteParsedown($path, $uriPath,true, $allowedFileLinkTypes); + $Parsedown = new PerliteParsedown($path, $uriPath,$niceLinks, $allowedFileLinkTypes); $Parsedown->setSafeMode($htmlSafeMode); $Parsedown->setBreaksEnabled($lineBreaks); $cleanFile = ''; diff --git a/perlite/helper.php b/perlite/helper.php index 617657bb..c037df12 100644 --- a/perlite/helper.php +++ b/perlite/helper.php @@ -40,8 +40,8 @@ $hiddenFileAccess = empty(getenv('HIDDEN_FILE_ACCESS')) ? false : filter_var(getenv('HIDDEN_FILE_ACCESS'), FILTER_VALIDATE_BOOLEAN); // use absolut paths instead of relative paths -if (!isset($relPathes)) - $relPathes = empty(getenv('ABSOLUTE_PATHS')) ? false : filter_var(getenv('ABSOLUTE_PATHS'), FILTER_VALIDATE_BOOLEAN); +if (!isset($absolutePath)) + $absolutePath = empty(getenv('ABSOLUTE_PATHS')) ? false : filter_var(getenv('ABSOLUTE_PATHS'), FILTER_VALIDATE_BOOLEAN); // Meta Tags infos if (empty($siteTitle)) @@ -78,6 +78,11 @@ if (!isset($lineBreaks)) $lineBreaks = empty(getenv('LINE_BREAKS')) ? true : filter_var(getenv('LINE_BREAKS'), FILTER_VALIDATE_BOOLEAN); +// nice links +if (!isset($niceLinks)) + $niceLinks = empty(getenv('NICE_LINKS')) ? true : filter_var(getenv('NICE_LINKS'), FILTER_VALIDATE_BOOLEAN); + + // file types if (empty($allowedFileLinkTypes)) $allowedFileLinkTypes = empty(getenv('ALLOWED_FILE_LINK_TYPES')) ? ['pdf', 'mp4'] : explode(",", getenv('ALLOWED_FILE_LINK_TYPES')); diff --git a/perlite/settings.php b/perlite/settings.php index add095a9..cfde9db0 100644 --- a/perlite/settings.php +++ b/perlite/settings.php @@ -21,17 +21,18 @@ $showLocalGraph = "true"; $font_size = "15"; $hideFolders = "docs,trash"; +$niceLinks = true; // --- Advanced Settings --- $hiddenFileAccess = false; -$relPathes = false; +$absolutePath = false; $uriPath = "/"; $htmlSafeMode = true; $useZettelkastenFilenames = false; $highlightJSLangs = ["powershell", "x86asm"]; $allowedFileLinkTypes = ['pdf', 'mp4']; -$tempPath = ""; // path for graph html, leave empty for automatic +$tempPath = ""; // path to store the pre-rendered graph relations, leave empty for /tmp // --- Metadata Settings --- From a66d9c081b51bdfe726a4c15dceb6e95a70cf088 Mon Sep 17 00:00:00 2001 From: sec77 <31564517+secure-77@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:20:45 +0100 Subject: [PATCH 17/27] fixed quotation mark missmatches --- perlite/helper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perlite/helper.php b/perlite/helper.php index c037df12..1131fa4b 100644 --- a/perlite/helper.php +++ b/perlite/helper.php @@ -272,7 +272,7 @@ function menu($dir, $folder = '') $html .= '