2025-09-15 21:35:41 +00:00
|
|
|
---
|
|
|
|
|
layout: default
|
|
|
|
|
---
|
|
|
|
|
|
2025-09-15 23:56:46 +01:00
|
|
|
<!-- Theme Toggle Button -->
|
|
|
|
|
<div class="theme-toggle-container" style="position: fixed; top: 20px; right: 20px; z-index: 1000;">
|
|
|
|
|
<button id="theme-toggle" class="btn btn-sm" style="background-color: var(--primary-btnbg-color); color: white; border: none; border-radius: 20px; padding: 8px 16px;">
|
|
|
|
|
<i class="fas fa-moon" id="theme-icon"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-15 21:35:41 +00:00
|
|
|
<article class="lab-content">
|
2025-09-26 16:12:28 +01:00
|
|
|
<div class="hidden-when-embedded">
|
|
|
|
|
<h1>{{ page.title }}</h1>
|
|
|
|
|
{% if page.description %}
|
|
|
|
|
<p class="lab-description">{{ page.description }}</p>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{% if page.overview %}
|
|
|
|
|
<h2>Lab Overview</h2>
|
|
|
|
|
<div class="overview-content">{{ page.overview | markdownify }}</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
</div>
|
2025-09-23 12:50:22 +01:00
|
|
|
|
2025-09-30 10:46:29 +01:00
|
|
|
<div class="lab-metadata hidden-when-embedded">
|
2025-09-23 12:50:22 +01:00
|
|
|
{% if page.author %}
|
|
|
|
|
<div class="metadata-item">
|
2025-09-25 12:34:07 +01:00
|
|
|
<strong>{% if page.author.first %}Authors:{% else %}Author:{% endif %}</strong>
|
|
|
|
|
{% if page.author.first %}
|
|
|
|
|
{% assign author_count = page.author.size %}
|
|
|
|
|
{% for author in page.author %}
|
|
|
|
|
{% if forloop.last and author_count > 1 %}and {% endif %}{{ author }}{% unless forloop.last %}, {% endunless %}
|
|
|
|
|
{% endfor %}
|
|
|
|
|
{% else %}
|
|
|
|
|
{{ page.author }}
|
|
|
|
|
{% endif %}
|
2025-09-23 12:50:22 +01:00
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if page.license %}
|
|
|
|
|
<div class="metadata-item">
|
|
|
|
|
<strong>License:</strong> {{ page.license }}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
2025-09-15 21:35:41 +00:00
|
|
|
{% if page.difficulty %}
|
|
|
|
|
<div class="metadata-item">
|
|
|
|
|
<strong>Difficulty:</strong> {{ page.difficulty }}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if page.duration %}
|
|
|
|
|
<div class="metadata-item">
|
|
|
|
|
<strong>Estimated Duration:</strong> {{ page.duration }}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if page.prerequisites %}
|
|
|
|
|
<div class="metadata-item">
|
|
|
|
|
<strong>Prerequisites:</strong> {{ page.prerequisites }}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
2025-09-26 16:12:28 +01:00
|
|
|
<div class="hidden-when-embedded">
|
2025-09-23 12:50:22 +01:00
|
|
|
{% if page.cybok %}
|
2025-09-26 16:12:28 +01:00
|
|
|
<div class="cybok">
|
|
|
|
|
<strong>CyBOK Knowledge Areas:</strong>
|
|
|
|
|
{% for cybok_item in page.cybok %}
|
|
|
|
|
<span class="cybok-ka">{{ cybok_item.ka }}: {{ cybok_item.topic }}</span>
|
|
|
|
|
{% for keyword in cybok_item.keywords %}
|
|
|
|
|
<span class="cybok-keyword">{{ keyword }}</span>
|
|
|
|
|
{% endfor %}
|
2025-09-15 21:35:41 +00:00
|
|
|
{% endfor %}
|
2025-09-26 16:12:28 +01:00
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
{% if page.tags %}
|
|
|
|
|
<div class="tags">
|
2025-09-29 11:27:48 +01:00
|
|
|
<strong>Tags:</strong>
|
2025-09-26 16:12:28 +01:00
|
|
|
{% for tag in page.tags %}
|
|
|
|
|
<span class="tag">{{ tag }}</span>
|
|
|
|
|
{% endfor %}
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
2025-09-23 12:50:22 +01:00
|
|
|
</div>
|
|
|
|
|
|
2025-09-15 21:35:41 +00:00
|
|
|
</div>
|
2025-09-23 12:50:22 +01:00
|
|
|
<br />
|
2025-09-15 21:35:41 +00:00
|
|
|
|
|
|
|
|
<div class="lab-content-body">
|
2025-09-23 12:50:22 +01:00
|
|
|
<div id="toc-container" class="toc-container">
|
2025-09-30 10:46:29 +01:00
|
|
|
<h2>Contents</h2>
|
2025-09-23 12:50:22 +01:00
|
|
|
<ul id="toc-list"></ul>
|
|
|
|
|
</div>
|
2025-09-15 21:35:41 +00:00
|
|
|
{{ content }}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<footer class="lab-footer">
|
2025-09-23 12:50:22 +01:00
|
|
|
<a href="https://hacktivity.co.uk" class="back-link">← Back to Hacktivity</a>
|
2025-09-15 21:35:41 +00:00
|
|
|
</footer>
|
|
|
|
|
</article>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.lab-content {
|
|
|
|
|
max-width: 800px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-27 00:44:55 +01:00
|
|
|
.embedded-mode .lab-content {
|
2025-09-25 14:17:11 +01:00
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
2025-09-26 16:12:28 +01:00
|
|
|
.embedded-mode .hidden-when-embedded {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
2025-09-25 14:17:11 +01:00
|
|
|
|
2025-09-15 21:35:41 +00:00
|
|
|
.lab-header {
|
2025-09-15 23:30:20 +01:00
|
|
|
border-bottom: 2px solid var(--panelborder-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
padding-bottom: 1.5rem;
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lab-header h1 {
|
|
|
|
|
margin-bottom: 0.5rem;
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--fg-color);
|
2025-09-16 00:42:46 +01:00
|
|
|
font-weight: normal;
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lab-description {
|
|
|
|
|
font-size: 1.125rem;
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--fg-color);
|
|
|
|
|
opacity: 0.8;
|
2025-09-15 21:35:41 +00:00
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-23 12:50:22 +01:00
|
|
|
|
|
|
|
|
.overview-content {
|
|
|
|
|
color: var(--fg-color);
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-09-15 21:35:41 +00:00
|
|
|
.lab-metadata {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 0.5rem;
|
2025-09-15 23:30:20 +01:00
|
|
|
background-color: var(--panelbg-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
padding: 1rem;
|
|
|
|
|
border-radius: 6px;
|
2025-09-15 23:30:20 +01:00
|
|
|
border: 1px solid var(--panelborder-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.metadata-item {
|
|
|
|
|
font-size: 0.875rem;
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--fg-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.metadata-item strong {
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--fg-color);
|
|
|
|
|
font-weight: 600;
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.lab-content-body {
|
|
|
|
|
line-height: 1.6;
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--fg-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lab-content-body h2 {
|
2025-09-15 23:30:20 +01:00
|
|
|
border-bottom: 1px solid var(--panelborder-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
padding-bottom: 0.3rem;
|
|
|
|
|
margin-top: 2rem;
|
|
|
|
|
margin-bottom: 1rem;
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--fg-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lab-content-body h3 {
|
|
|
|
|
margin-top: 1.5rem;
|
|
|
|
|
margin-bottom: 0.75rem;
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--fg-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lab-footer {
|
|
|
|
|
margin-top: 3rem;
|
|
|
|
|
padding-top: 1.5rem;
|
2025-09-15 23:30:20 +01:00
|
|
|
border-top: 1px solid var(--panelborder-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.back-link {
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--link-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
text-decoration: none;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.back-link:hover {
|
|
|
|
|
text-decoration: underline;
|
2025-09-15 23:30:20 +01:00
|
|
|
color: var(--primary-btnhov-color);
|
2025-09-15 21:35:41 +00:00
|
|
|
}
|
2025-09-23 12:50:22 +01:00
|
|
|
|
|
|
|
|
.toc-container {
|
|
|
|
|
background-color: var(--panelbg-color);
|
|
|
|
|
border: 1px solid var(--panelborder-color);
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.toc-container h3 {
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
color: var(--fg-color);
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
border-bottom: 1px solid var(--panelborder-color);
|
|
|
|
|
padding-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toc-list {
|
|
|
|
|
list-style: none;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toc-list li {
|
|
|
|
|
margin: 0.25rem 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toc-list a {
|
|
|
|
|
color: var(--link-color);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
display: block;
|
|
|
|
|
padding: 0.25rem 0;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toc-list a:hover {
|
|
|
|
|
background-color: var(--highlight-color);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Indentation based on heading level */
|
|
|
|
|
#toc-list li[data-level="1"] { padding-left: 0; }
|
|
|
|
|
#toc-list li[data-level="2"] { padding-left: 1rem; }
|
|
|
|
|
#toc-list li[data-level="3"] { padding-left: 2rem; }
|
|
|
|
|
#toc-list li[data-level="4"] { padding-left: 3rem; }
|
|
|
|
|
#toc-list li[data-level="5"] { padding-left: 4rem; }
|
|
|
|
|
#toc-list li[data-level="6"] { padding-left: 5rem; }
|
2025-09-15 23:56:46 +01:00
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// Theme toggle functionality
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
|
|
|
const themeIcon = document.getElementById('theme-icon');
|
|
|
|
|
const body = document.body;
|
|
|
|
|
|
|
|
|
|
// Check for saved theme preference or default to dark mode
|
|
|
|
|
const currentTheme = localStorage.getItem('theme') || 'dark';
|
|
|
|
|
body.setAttribute('data-theme', currentTheme);
|
|
|
|
|
updateThemeIcon(currentTheme);
|
|
|
|
|
|
|
|
|
|
themeToggle.addEventListener('click', function() {
|
|
|
|
|
const currentTheme = body.getAttribute('data-theme');
|
|
|
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
|
|
|
|
|
|
|
|
body.setAttribute('data-theme', newTheme);
|
|
|
|
|
localStorage.setItem('theme', newTheme);
|
|
|
|
|
updateThemeIcon(newTheme);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function updateThemeIcon(theme) {
|
|
|
|
|
if (theme === 'dark') {
|
|
|
|
|
themeIcon.className = 'fas fa-sun';
|
|
|
|
|
} else {
|
|
|
|
|
themeIcon.className = 'fas fa-moon';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-16 00:42:46 +01:00
|
|
|
|
2025-09-23 12:50:22 +01:00
|
|
|
// Process ==highlight== syntax and > TIP: patterns
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
const contentBody = document.querySelector('.lab-content-body');
|
|
|
|
|
if (contentBody) {
|
|
|
|
|
// Replace specific highlight types first
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==action:\s*([^=]+)==/gi, '<span class="action-highlight">⚡ $1</span>');
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==tip:\s*([^=]+)==/gi, '<span class="tip-highlight">💡 $1</span>');
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==hint:\s*([^=]+)==/gi, '<span class="hint-highlight">💭 $1</span>');
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==note:\s*([^=]+)==/gi, '<span class="note-highlight">📝 $1</span>');
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==warning:\s*([^=]+)==/gi, '<span class="warning-highlight">⚠️ $1</span>');
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==VM:\s*([^=]+)==/gi, '<span class="vm-highlight">🖥️ $1</span>');
|
2025-09-26 16:12:28 +01:00
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==question:\s*([^=]+)==/gi, '<span class="question-highlight">❓ $1</span>');
|
|
|
|
|
|
|
|
|
|
// Process edit highlights BEFORE other processing to avoid syntax highlighting interference
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==edit:\s*([^=]+)==/gi, '<span class="edit-highlight">✏️ $1</span>');
|
2025-09-23 12:50:22 +01:00
|
|
|
|
2025-09-26 16:12:28 +01:00
|
|
|
// Replace generic ==text== with <mark>text</mark> (but not edit highlights)
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(/==(?!edit:)([^=]+)==/g, '<mark>$1</mark>');
|
2025-09-23 12:50:22 +01:00
|
|
|
|
|
|
|
|
// Replace > TIP: patterns with tip-item divs
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*<em>Tip:<\/em>\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="tip-item">$1</div>'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Handle > *Tip: ANYTHINGHERE* (entire content in italics)
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*<em>Tip:\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/em>\s*<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="tip-item">$1</div>'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Also handle > TIP: without italics
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*Tip:\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="tip-item">$1</div>'
|
|
|
|
|
);
|
|
|
|
|
|
2025-09-26 16:12:28 +01:00
|
|
|
// Handle block-level action, warning, note, hint, flag patterns
|
2025-09-23 12:50:22 +01:00
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*Action:\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="action-item">$1</div>'
|
|
|
|
|
);
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*Warning:\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="warning-item">$1</div>'
|
|
|
|
|
);
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*Note:\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="note-item">$1</div>'
|
|
|
|
|
);
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*Hint:\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="hint-item">Hint: $1</div>'
|
|
|
|
|
);
|
2025-09-26 16:12:28 +01:00
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*Question:\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="question-item">$1</div>'
|
|
|
|
|
);
|
|
|
|
|
contentBody.innerHTML = contentBody.innerHTML.replace(
|
|
|
|
|
/<blockquote>\s*<p>\s*Flag:\s*([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/p>\s*<\/blockquote>/gi,
|
|
|
|
|
'<div class="flag-item">$1</div>'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Add copy functionality to code blocks
|
|
|
|
|
const codeBlocks = contentBody.querySelectorAll('pre code');
|
|
|
|
|
codeBlocks.forEach(block => {
|
|
|
|
|
const pre = block.parentElement;
|
|
|
|
|
if (pre.classList.contains('highlight') || pre.closest('.highlight')) {
|
|
|
|
|
pre.style.position = 'relative';
|
|
|
|
|
const copyBtn = document.createElement('button');
|
|
|
|
|
copyBtn.textContent = 'Copy';
|
|
|
|
|
copyBtn.className = 'copy-btn';
|
|
|
|
|
copyBtn.style.cssText = `
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0.5rem;
|
|
|
|
|
right: 0.5rem;
|
|
|
|
|
background: #007bff;
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
`;
|
|
|
|
|
copyBtn.addEventListener('click', () => {
|
|
|
|
|
navigator.clipboard.writeText(block.textContent).then(() => {
|
|
|
|
|
copyBtn.textContent = 'Copied!';
|
|
|
|
|
setTimeout(() => copyBtn.textContent = 'Copy', 2000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
pre.addEventListener('mouseenter', () => copyBtn.style.opacity = '1');
|
|
|
|
|
pre.addEventListener('mouseleave', () => copyBtn.style.opacity = '0');
|
|
|
|
|
pre.appendChild(copyBtn);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Process edit highlights that got broken up by syntax highlighting
|
|
|
|
|
const codeElements = contentBody.querySelectorAll('code, pre code, .highlight code');
|
|
|
|
|
codeElements.forEach(codeElement => {
|
|
|
|
|
// Look for the pattern: <span class="o">==</span>edit:content<span class="o">==</span>
|
|
|
|
|
const html = codeElement.innerHTML;
|
|
|
|
|
if (html.includes('==') && html.includes('edit:')) {
|
|
|
|
|
// Handle the broken pattern from syntax highlighting
|
|
|
|
|
codeElement.innerHTML = html.replace(
|
|
|
|
|
/<span[^>]*class="[^"]*o[^"]*"[^>]*>==<\/span>edit:\s*([^<]+)<span[^>]*class="[^"]*o[^"]*"[^>]*>==<\/span>/gi,
|
|
|
|
|
'<span class="edit-highlight">✏️$1</span>'
|
|
|
|
|
);
|
|
|
|
|
// Handle patterns with mark tags in the middle
|
|
|
|
|
codeElement.innerHTML = codeElement.innerHTML.replace(
|
|
|
|
|
/<span[^>]*class="[^"]*o[^"]*"[^>]*>==<\/span>edit:\s*([^<]+(?:<[^>]+>[^<]*)*?)<span[^>]*class="[^"]*o[^"]*"[^>]*><mark><\/mark><\/span>/gi,
|
|
|
|
|
'<span class="edit-highlight">✏️$1</span>'
|
|
|
|
|
);
|
|
|
|
|
// Handle the specific pattern: ==edit:content<span class="o"><mark></mark></span>
|
|
|
|
|
codeElement.innerHTML = codeElement.innerHTML.replace(
|
|
|
|
|
/<span[^>]*class="[^"]*o[^"]*"[^>]*>==<\/span>edit:\s*([^<]+)<span[^>]*class="[^"]*o[^"]*"[^>]*><mark><\/mark><\/span>/gi,
|
|
|
|
|
'<span class="edit-highlight">✏️$1</span>'
|
|
|
|
|
);
|
|
|
|
|
// Handle pattern with variable highlighting in the middle: ==edit:content <span class="nv">VAR</span>==
|
|
|
|
|
codeElement.innerHTML = codeElement.innerHTML.replace(
|
|
|
|
|
/<span[^>]*class="[^"]*o[^"]*"[^>]*>==<\/span>edit:\s*([^<]+)\s*<span[^>]*class="[^"]*nv[^"]*"[^>]*>([^<]+)<\/span><span[^>]*class="[^"]*o[^"]*"[^>]*>==<\/span>/gi,
|
|
|
|
|
'<span class="edit-highlight">✏️$1 $2</span>'
|
|
|
|
|
);
|
|
|
|
|
// Also handle any remaining unprocessed edit highlights
|
|
|
|
|
codeElement.innerHTML = codeElement.innerHTML.replace(
|
|
|
|
|
/==edit:\s*([^=]+)==/gi,
|
|
|
|
|
'<span class="edit-highlight">✏️$1</span>'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-30 10:34:41 +01:00
|
|
|
|
|
|
|
|
// Convert YouTube links to embedded videos
|
|
|
|
|
const youtubeLinks = contentBody.querySelectorAll('a[href*="youtu.be"], a[href*="youtube.com/watch"]');
|
|
|
|
|
youtubeLinks.forEach(link => {
|
|
|
|
|
const href = link.href;
|
|
|
|
|
let videoId = '';
|
|
|
|
|
|
|
|
|
|
// Extract video ID from different YouTube URL formats
|
|
|
|
|
if (href.includes('youtu.be/')) {
|
|
|
|
|
videoId = href.split('youtu.be/')[1].split('?')[0];
|
|
|
|
|
} else if (href.includes('youtube.com/watch?v=')) {
|
|
|
|
|
videoId = href.split('v=')[1].split('&')[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (videoId) {
|
|
|
|
|
// Create embedded iframe
|
|
|
|
|
const iframe = document.createElement('iframe');
|
|
|
|
|
iframe.src = `https://www.youtube.com/embed/${videoId}`;
|
|
|
|
|
iframe.width = '560';
|
|
|
|
|
iframe.height = '315';
|
|
|
|
|
iframe.frameBorder = '0';
|
|
|
|
|
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
|
|
|
|
|
iframe.allowFullscreen = true;
|
|
|
|
|
iframe.style.cssText = 'max-width: 100%; height: auto; border-radius: 8px; margin: 1rem 0; transition: all 0.3s ease;';
|
|
|
|
|
|
|
|
|
|
// Create container div
|
|
|
|
|
const container = document.createElement('div');
|
|
|
|
|
container.className = 'youtube-embed';
|
|
|
|
|
container.style.cssText = 'text-align: center; margin: 1rem 0; cursor: pointer;';
|
|
|
|
|
|
|
|
|
|
// Add hover effects with delay
|
|
|
|
|
let hoverTimeout = null;
|
|
|
|
|
|
|
|
|
|
container.addEventListener('mouseenter', function() {
|
|
|
|
|
// Clear any existing timeout
|
|
|
|
|
if (hoverTimeout) {
|
|
|
|
|
clearTimeout(hoverTimeout);
|
|
|
|
|
hoverTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iframe.style.transform = 'scale(1.05)';
|
|
|
|
|
iframe.style.height = '400px'; // Make it taller on hover
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
container.addEventListener('mouseleave', function() {
|
|
|
|
|
// Set a timeout to return to normal size after 30 seconds
|
|
|
|
|
hoverTimeout = setTimeout(function() {
|
|
|
|
|
iframe.style.transform = 'scale(1)';
|
|
|
|
|
iframe.style.height = 'auto'; // Return to original height
|
|
|
|
|
hoverTimeout = null;
|
|
|
|
|
}, 30000); // 30 seconds delay
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add the link text as a caption
|
|
|
|
|
const caption = document.createElement('p');
|
|
|
|
|
caption.textContent = link.textContent;
|
|
|
|
|
caption.style.cssText = 'font-style: italic; color: var(--fg-color); opacity: 0.8; margin-top: 0.5rem;';
|
|
|
|
|
|
|
|
|
|
container.appendChild(iframe);
|
|
|
|
|
container.appendChild(caption);
|
|
|
|
|
|
|
|
|
|
// Replace the link with the embedded video
|
|
|
|
|
link.parentNode.replaceChild(container, link);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-23 12:50:22 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Auto-generate Table of Contents
|
2025-09-16 00:42:46 +01:00
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
2025-09-23 12:50:22 +01:00
|
|
|
const tocList = document.getElementById('toc-list');
|
2025-09-16 00:42:46 +01:00
|
|
|
const contentBody = document.querySelector('.lab-content-body');
|
2025-09-23 12:50:22 +01:00
|
|
|
|
|
|
|
|
if (tocList && contentBody) {
|
|
|
|
|
// Find all headings in the content
|
|
|
|
|
const headings = contentBody.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
|
|
|
|
|
|
|
|
if (headings.length === 0) {
|
|
|
|
|
// Hide TOC if no headings found
|
|
|
|
|
document.getElementById('toc-container').style.display = 'none';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate TOC items
|
|
|
|
|
headings.forEach((heading, index) => {
|
2025-09-30 10:46:29 +01:00
|
|
|
// Skip "Contents" heading
|
|
|
|
|
if (heading.textContent.trim() === 'Contents') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-23 12:50:22 +01:00
|
|
|
// Create ID if it doesn't exist
|
|
|
|
|
if (!heading.id) {
|
|
|
|
|
heading.id = heading.textContent
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
|
|
|
.replace(/\s+/g, '-')
|
|
|
|
|
.replace(/^-+|-+$/g, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create TOC item
|
|
|
|
|
const li = document.createElement('li');
|
|
|
|
|
const level = parseInt(heading.tagName.charAt(1));
|
|
|
|
|
li.setAttribute('data-level', level);
|
|
|
|
|
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = '#' + heading.id;
|
|
|
|
|
a.textContent = heading.textContent;
|
|
|
|
|
|
|
|
|
|
// Add click handler for smooth scrolling
|
|
|
|
|
a.addEventListener('click', function(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
heading.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
li.appendChild(a);
|
|
|
|
|
tocList.appendChild(li);
|
|
|
|
|
});
|
2025-09-16 00:42:46 +01:00
|
|
|
}
|
|
|
|
|
});
|
2025-09-15 23:56:46 +01:00
|
|
|
</script>
|