Add files via upload
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
session_start();
|
||||
$directory = __DIR__ . '/files';
|
||||
if (!file_exists($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
|
||||
$directory = realpath($directory);
|
||||
if ($directory === false || !is_dir($directory)) {
|
||||
die("Invalid directory");
|
||||
}
|
||||
|
||||
$files = glob($directory . '/*.md');
|
||||
|
||||
usort($files, function($a, $b) {
|
||||
return filemtime($b) - filemtime($a);
|
||||
});
|
||||
|
||||
$latestFiles = array_slice($files, 0, 5);
|
||||
$olderFiles = array_slice($files, 5);
|
||||
|
||||
$alert = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (isset($_POST['action']) && isset($_POST['csrf_token']) && $_POST['csrf_token'] === $_SESSION['csrf_token']) {
|
||||
if ($_POST['action'] === 'create' || $_POST['action'] === 'edit') {
|
||||
$date = date('Ymd');
|
||||
$filename = $date . '-' . preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['filename']) . '.md';
|
||||
$filepath = $directory . '/' . $filename;
|
||||
|
||||
if (strpos(realpath(dirname($filepath)), $directory) !== 0) {
|
||||
die("Invalid file path");
|
||||
}
|
||||
|
||||
$content = strip_tags($_POST['content']);
|
||||
file_put_contents($filepath, $content);
|
||||
$_SESSION['alert'] = [
|
||||
'type' => 'success',
|
||||
'message' => 'File ' . ($_POST['action'] === 'create' ? 'created' : 'updated') . ' successfully.'
|
||||
];
|
||||
} elseif ($_POST['action'] === 'delete') {
|
||||
$filename = basename($_POST['filename']);
|
||||
$filepath = $directory . '/' . $filename;
|
||||
|
||||
if (strpos(realpath(dirname($filepath)), $directory) !== 0) {
|
||||
die("Invalid file path");
|
||||
}
|
||||
|
||||
if (is_file($filepath) && pathinfo($filepath, PATHINFO_EXTENSION) === 'md') {
|
||||
unlink($filepath);
|
||||
$_SESSION['alert'] = [
|
||||
'type' => 'warning',
|
||||
'message' => 'File deleted successfully.'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
header('Location: ' . $_SERVER['PHP_SELF']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$editFile = isset($_GET['edit']) ? basename($_GET['edit']) : null;
|
||||
if ($editFile !== null && strpos($editFile, '..') !== false) {
|
||||
$editFile = null;
|
||||
}
|
||||
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
// Display alert from session if it exists
|
||||
if (isset($_SESSION['alert'])) {
|
||||
$alertType = $_SESSION['alert']['type'];
|
||||
$alertMessage = $_SESSION['alert']['message'];
|
||||
$alert = "<div class='alert alert-{$alertType} alert-dismissible fade show' role='alert'>
|
||||
{$alertMessage}
|
||||
<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button>
|
||||
</div>";
|
||||
unset($_SESSION['alert']);
|
||||
}
|
||||
|
||||
function renderFileListItem($file) {
|
||||
$basename = basename($file);
|
||||
return "
|
||||
<li class='list-group-item d-flex justify-content-between align-items-center'>
|
||||
<div class='file-info' data-bs-toggle='modal' data-bs-target='#viewModal' data-file='" . htmlspecialchars($basename) . "'>
|
||||
<span class='file-name'>" . htmlspecialchars($basename) . "</span>
|
||||
<span class='file-date'>Last updated: " . date("F d, Y H:i", filemtime($file)) . "</span>
|
||||
</div>
|
||||
<div>
|
||||
<button class='btn btn-icon view-md' data-bs-toggle='modal' data-bs-target='#viewModal' data-file='" . htmlspecialchars($basename) . "' title='View'>
|
||||
<i class='fas fa-eye'></i>
|
||||
</button>
|
||||
<a href='?edit=" . urlencode($basename) . "' class='btn btn-icon' title='Edit'>
|
||||
<i class='fas fa-edit'></i>
|
||||
</a>
|
||||
<button class='btn btn-icon copy-url' data-url='" . htmlspecialchars('http://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']) . '/files/' . $basename) . "' title='Copy URL'>
|
||||
<i class='fas fa-copy'></i>
|
||||
</button>
|
||||
<a href='view_html.php?file=" . urlencode($basename) . "' class='btn btn-icon' title='View as HTML'>
|
||||
<i class='fas fa-code'></i>
|
||||
</a>
|
||||
<button class='btn btn-icon delete-file' data-bs-toggle='modal' data-bs-target='#deleteModal' data-file='" . htmlspecialchars($basename) . "' title='Delete'>
|
||||
<i class='fas fa-trash'></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>";
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vincent's MarkDown</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<header class="py-2 sticky-header">
|
||||
<div class="container">
|
||||
<a href="./" class="nav-brand">
|
||||
<strong>VINCENT</strong><span>MARKDOWN</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-grow-1">
|
||||
<div class="container my-4">
|
||||
<?php echo $alert; ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0"><?php echo $editFile ? 'Edit File' : 'Create New File'; ?></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="<?php echo $editFile ? 'edit' : 'create'; ?>">
|
||||
<div class="mb-3">
|
||||
<label for="filename" class="form-label">Filename:</label>
|
||||
<input type="text" class="form-control" id="filename" name="filename" value="<?php echo $editFile ? substr(pathinfo($editFile, PATHINFO_FILENAME), 9) : ''; ?>" required pattern="[a-zA-Z0-9_-]+">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<textarea id="editor" name="content"><?php echo $editFile ? htmlspecialchars(file_get_contents($directory . '/' . $editFile)) : ''; ?></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><?php echo $editFile ? 'Update File' : 'Create File'; ?></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="h5 mb-0">Markdown Files</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush mb-4">
|
||||
<?php
|
||||
foreach ($latestFiles as $file) {
|
||||
echo renderFileListItem($file);
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
|
||||
<?php if (!empty($olderFiles)) : ?>
|
||||
<div class="accordion" id="olderFilesAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="olderFilesHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#olderFilesCollapse" aria-expanded="false" aria-controls="olderFilesCollapse">
|
||||
Older Files
|
||||
</button>
|
||||
</h2>
|
||||
<div id="olderFilesCollapse" class="accordion-collapse collapse" aria-labelledby="olderFilesHeading" data-bs-parent="#olderFilesAccordion">
|
||||
<div class="accordion-body p-0">
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php
|
||||
foreach ($olderFiles as $file) {
|
||||
echo renderFileListItem($file);
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="bg-white py-3 mt-auto border-top">
|
||||
<div class="container text-left">
|
||||
<p class="mb-0 text-muted footer-text"><small><a href="https://vincentrozenberg.com"><strong>VINCENT</strong>ROZENBERG</a> ©<?php echo date('Y'); ?></small></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- View Modal -->
|
||||
<div class="modal fade" id="viewModal" tabindex="-1" aria-labelledby="viewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="viewModalLabel">View Markdown</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a href="#" id="modalEditBtn" class="btn btn-icon me-2" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button id="modalCopyUrlBtn" class="btn btn-icon me-2" title="Copy URL">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<a href="#" id="modalViewHtmlBtn" class="btn btn-icon me-2" title="View as HTML">
|
||||
<i class="fas fa-code"></i>
|
||||
</a>
|
||||
<button class="btn btn-icon delete-file" data-bs-toggle="modal" data-bs-target="#deleteModal" data-file="" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="viewModalBody">
|
||||
<!-- Rendered Markdown content will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete this file?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form method="post" id="deleteForm">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="filename" id="deleteFilename">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Copy URL Modal -->
|
||||
<div class="modal fade" id="copyUrlModal" tabindex="-1" aria-labelledby="copyUrlModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="copyUrlModalLabel">URL Copied</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
The URL has been copied to your clipboard.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"></script>
|
||||
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
|
||||
<script src="scripts.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Vincent Rozenberg
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Vincent's MarkDown
|
||||
|
||||
This is a quick and dirty PHP-based web application for managing and viewing Markdown files. I designed it as a straightforward solution for personal use or small-scale deployments, prioritising simplicity over advanced features.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
I want to be clear: this application is a basic, no-frills solution. I didn't design it for large-scale or high-security environments. Use it at your own risk and consider implementing additional security measures for sensitive deployments.
|
||||
|
||||
## Features
|
||||
|
||||
Here's what my app can do:
|
||||
- Create and edit Markdown files with an easy-to-use interface
|
||||
- Show a list of all Markdown files, sorted by last modified date
|
||||
- Render Markdown files as HTML for easy viewing
|
||||
- Delete unwanted files
|
||||
- Copy shareable URLs for each file
|
||||
- View files as raw HTML
|
||||
- Responsive design for both desktop and mobile devices
|
||||
|
||||
## Requirements
|
||||
|
||||
To run this, you'll need:
|
||||
- PHP 7.0 or higher
|
||||
- Web server (e.g., Apache, Nginx)
|
||||
- Modern web browser
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository to your web server's document root or a subdirectory.
|
||||
2. Make sure the `files` directory is writable by your web server.
|
||||
3. Configure your web server to serve PHP files.
|
||||
4. Access the application through your web browser.
|
||||
|
||||
## Usage
|
||||
|
||||
Here's how to use it:
|
||||
- Create a new file: Enter a filename and content in the form at the top of the page, then click "Create File".
|
||||
- Edit a file: Click the edit icon next to the file name in the list.
|
||||
- View a file: Click on the file name or the eye icon.
|
||||
- Delete a file: Click the trash icon and confirm the deletion.
|
||||
- Copy a shareable URL: Click the copy icon next to the file.
|
||||
- View a file as raw HTML: Click the code icon.
|
||||
|
||||
## Security
|
||||
|
||||
I've implemented some basic security measures:
|
||||
- CSRF protection for form submissions
|
||||
- Input sanitisation to prevent XSS attacks
|
||||
- Directory traversal prevention
|
||||
- Restriction on allowed file extensions
|
||||
|
||||
But remember, this is a quick and dirty solution. It may not be suitable for handling sensitive information or use in high-security environments. Use at your own discretion and implement additional security measures as needed.
|
||||
|
||||
## Customisation
|
||||
|
||||
Feel free to customise the app! You can modify the appearance by editing the `styles.css` file. If you want to extend or modify the JavaScript functionality, check out the `scripts.js` file.
|
||||
|
||||
## External Libraries
|
||||
|
||||
I've used several external libraries to enhance the functionality. I'm using the latest versions available via CDN for each:
|
||||
|
||||
- [Bootstrap](https://getbootstrap.com/) - For responsive design and UI components
|
||||
- [EasyMDE](https://easymde.tk/) - For the Markdown editor
|
||||
- [Font Awesome](https://fontawesome.com/) - For icons
|
||||
- [Marked](https://marked.js.org/) - For Markdown parsing
|
||||
- [Google Fonts](https://fonts.google.com/) (Roboto) - For typography
|
||||
|
||||
I'm really grateful to the maintainers and contributors of these libraries for their excellent work.
|
||||
|
||||
## License
|
||||
|
||||
I've licensed this project under the MIT License. Check out the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## About Me
|
||||
|
||||
[https://vincentrozenberg.com](https://vincentrozenberg.com).
|
||||
|
||||
## Contributing
|
||||
|
||||
Even though this is a simple, quick and dirty solution, I'm open to contributions! If you have improvements or bug fixes to suggest, please feel free to submit a Pull Request.
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const easyMDE = new EasyMDE({
|
||||
element: document.getElementById('editor'),
|
||||
spellChecker: false,
|
||||
autosave: {
|
||||
enabled: true,
|
||||
uniqueId: "mdEditor",
|
||||
delay: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
document.querySelectorAll('.copy-url').forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const url = this.getAttribute('data-url');
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
const copyUrlModal = new bootstrap.Modal(document.getElementById('copyUrlModal'));
|
||||
copyUrlModal.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.file-info, .view-md').forEach(element => {
|
||||
element.addEventListener('click', function() {
|
||||
const filename = this.getAttribute('data-file');
|
||||
const timestamp = new Date().getTime();
|
||||
fetch(`files/${encodeURIComponent(filename)}?t=${timestamp}`)
|
||||
.then(response => response.text())
|
||||
.then(markdown => {
|
||||
const html = marked(markdown);
|
||||
document.getElementById('viewModalBody').innerHTML = html;
|
||||
document.getElementById('viewModalLabel').textContent = `Viewing: ${filename}`;
|
||||
document.getElementById('modalEditBtn').href = `?edit=${encodeURIComponent(filename)}`;
|
||||
document.getElementById('modalCopyUrlBtn').setAttribute('data-url', `${window.location.origin}${window.location.pathname}files/${encodeURIComponent(filename)}`);
|
||||
document.getElementById('modalViewHtmlBtn').href = `view_html.php?file=${encodeURIComponent(filename)}`;
|
||||
document.querySelector('#viewModal .delete-file').setAttribute('data-file', filename);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('modalCopyUrlBtn').addEventListener('click', function() {
|
||||
const url = this.getAttribute('data-url');
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
const copyUrlModal = new bootstrap.Modal(document.getElementById('copyUrlModal'));
|
||||
copyUrlModal.show();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.delete-file').forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
const filename = this.getAttribute('data-file');
|
||||
document.getElementById('deleteFilename').value = filename;
|
||||
document.getElementById('deleteModalLabel').textContent = `Confirm Deletion: ${filename}`;
|
||||
});
|
||||
});
|
||||
|
||||
// Accordion functionality
|
||||
const accordion = document.getElementById('olderFilesAccordion');
|
||||
if (accordion) {
|
||||
accordion.addEventListener('show.bs.collapse', function () {
|
||||
this.querySelector('.accordion-button').classList.remove('collapsed');
|
||||
});
|
||||
|
||||
accordion.addEventListener('hide.bs.collapse', function () {
|
||||
this.querySelector('.accordion-button').classList.add('collapsed');
|
||||
});
|
||||
}
|
||||
});
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
font-size: 1.5rem;
|
||||
text-decoration: none;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.nav-brand strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-brand span {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: #333333;
|
||||
border: 1px solid #e0e0e0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
border: none;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.list-group-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.file-date {
|
||||
font-size: 0.75rem;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4b4b4b;
|
||||
border-color: #333333;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1a1a1a;
|
||||
border-color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Styles for view_html.php */
|
||||
.file-title {
|
||||
font-size: 1rem;
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.file-title:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.rendered-markdown {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.rendered-markdown h1,
|
||||
.rendered-markdown h2,
|
||||
.rendered-markdown h3 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rendered-markdown ul,
|
||||
.rendered-markdown ol {
|
||||
padding-left: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rendered-markdown li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.rendered-markdown p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rendered-markdown code {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* Styles for links in rendered Markdown and modal content */
|
||||
.rendered-markdown a,
|
||||
#viewModalBody a {
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.rendered-markdown a:hover,
|
||||
#viewModalBody a:hover {
|
||||
border-bottom: 1px dotted #757575;
|
||||
}
|
||||
|
||||
/* Accordion styles */
|
||||
.accordion-button {
|
||||
background-color: #f8f9fa;
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.accordion-button:not(.collapsed) {
|
||||
background-color: #e9ecef;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.accordion-button:focus {
|
||||
box-shadow: none;
|
||||
border-color: rgba(0,0,0,.125);
|
||||
}
|
||||
|
||||
.accordion-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 767px) {
|
||||
.sticky-header .container {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.file-info {
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.list-group-item > div:last-child {
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.accordion-body {
|
||||
padding: 0.5em !important;
|
||||
}
|
||||
|
||||
.footer-text a {text-decoration: none!important;
|
||||
color: #757575!important;}
|
||||
|
||||
* {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #333333 !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
$directory = __DIR__ . '/files';
|
||||
$file = isset($_GET['file']) ? basename($_GET['file']) : null;
|
||||
|
||||
if (!$file || !is_file($directory . '/' . $file) || pathinfo($file, PATHINFO_EXTENSION) !== 'md') {
|
||||
die("Invalid file");
|
||||
}
|
||||
|
||||
$filePath = $directory . '/' . $file;
|
||||
$content = file_get_contents($filePath);
|
||||
|
||||
function parseMarkdown($text) {
|
||||
// Normalize line breaks
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
||||
|
||||
// Split the text into lines
|
||||
$lines = explode("\n", $text);
|
||||
$parsed = [];
|
||||
$inList = false;
|
||||
$listBuffer = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Headers
|
||||
if (preg_match('/^(#{1,6})\s+(.+)$/', $line, $matches)) {
|
||||
$level = strlen($matches[1]);
|
||||
$parsed[] = "<h$level>" . trim($matches[2]) . "</h$level>";
|
||||
}
|
||||
// List items
|
||||
elseif (preg_match('/^(\s*[-*+])\s+(.+)$/', $line, $matches)) {
|
||||
if (!$inList) {
|
||||
$inList = true;
|
||||
$listBuffer[] = "<ul>";
|
||||
}
|
||||
$listBuffer[] = "<li>" . trim($matches[2]) . "</li>";
|
||||
}
|
||||
// End of list
|
||||
elseif ($inList && trim($line) === '') {
|
||||
$inList = false;
|
||||
$listBuffer[] = "</ul>";
|
||||
$parsed = array_merge($parsed, $listBuffer);
|
||||
$listBuffer = [];
|
||||
$parsed[] = ""; // Add an empty line after the list
|
||||
}
|
||||
// Paragraphs
|
||||
elseif (trim($line) !== '') {
|
||||
$parsed[] = "<p>" . $line . "</p>";
|
||||
}
|
||||
// Empty lines
|
||||
else {
|
||||
$parsed[] = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Close any open list
|
||||
if ($inList) {
|
||||
$listBuffer[] = "</ul>";
|
||||
$parsed = array_merge($parsed, $listBuffer);
|
||||
}
|
||||
|
||||
$text = implode("\n", $parsed);
|
||||
|
||||
// Inline formatting
|
||||
$text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
|
||||
$text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
|
||||
$text = preg_replace('/`(.+?)`/', '<code>$1</code>', $text);
|
||||
$text = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2">$1</a>', $text);
|
||||
|
||||
// Remove empty paragraphs
|
||||
$text = preg_replace('/<p>\s*<\/p>/', '', $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
$parsedContent = parseMarkdown($content);
|
||||
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http";
|
||||
$directUrl = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo htmlspecialchars($file); ?> - HTML View</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<header class="py-2 sticky-header">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<a href="./" class="nav-brand">
|
||||
<strong>VINCENT</strong><span>MARKDOWN</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="<?php echo $directUrl; ?>" class="file-title me-2"><?php echo htmlspecialchars($file); ?></a>
|
||||
<button class="btn btn-icon copy-url" data-url="<?php echo $directUrl; ?>" title="Copy URL">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-grow-1">
|
||||
<div class="container my-4">
|
||||
<div class="rendered-markdown">
|
||||
<?php echo $parsedContent; ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="bg-white py-3 mt-auto border-top">
|
||||
<div class="container text-left">
|
||||
<p class="mb-0 text-muted footer-text"><small><a href="https://vincentrozenberg.com"><strong>VINCENT</strong>ROZENBERG</a> ©<?php echo date('Y'); ?></small></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Copy URL Modal -->
|
||||
<div class="modal fade" id="copyUrlModal" tabindex="-1" aria-labelledby="copyUrlModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="copyUrlModalLabel">URL Copied</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
The URL has been copied to your clipboard.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.querySelector('.copy-url').addEventListener('click', function() {
|
||||
const url = this.getAttribute('data-url');
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
const copyUrlModal = new bootstrap.Modal(document.getElementById('copyUrlModal'));
|
||||
copyUrlModal.show();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user