Export current Ylilauta thread as a Markdown (.md) file
// ==UserScript==
// @name Ylilauta2Markdown
// @name:fi Ylilauta2Markdown
// @namespace https://greasyfork.org/en/users/11903-eonmc2
// @namespace https://greasyfork.org/en/users/1552401-chipfin
// @version 1.1.8
// @description Export current Ylilauta thread as a Markdown (.md) file
// @description:fi Vie Ylilauta-lanka Markdown-tiedostoksi (.md)
// @author Gemini 3 Flash Thinking + ChatGPT
// @icon https://ylilauta.org/static/img/seal_of_ylilauta-icon.svg
// @match https://ylilauta.org/*/*
// @exclude https://ylilauta.org/*/
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const thread = document.querySelector('.card.thread');
if (!thread) return;
/**
* Converts basic HTML to Markdown
* @param {string} html
* @returns {string}
*/
function htmlToMarkdown(html) {
let text = html;
text = text.replace(/<strong>(.*?)<\/strong>/gi, '**$1**');
text = text.replace(/<em>(.*?)<\/em>/gi, '*$1*');
text = text.replace(/<a [^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)');
text = text.replace(/<br\s*\/?>/gi, '\n');
text = text.replace(/<img [^>]*src="([^"]+)"[^>]*>/gi, '');
text = text.replace(/<[^>]+>/g, '');
const temp = document.createElement('textarea');
temp.innerHTML = text;
text = temp.value;
return text.trim();
}
/**
* Scrapes the thread and triggers file download
*/
function exportThread() {
const threadId = thread.getAttribute('data-thread-id');
const threadUrl = thread.getAttribute('data-url') || window.location.href;
let rawTitle = document.querySelector('title')?.textContent?.trim() ||
document.querySelector('meta[name="description"]')?.content?.trim() || '';
const safeTitle = rawTitle
.replace(/\s+/g, ' ')
.replace(/[\\/:*?"<>|]/g, '')
.replace(/[\u{1F3FB}-\u{1F3FF}\u{1F900}-\u{1F9FF}\u{1F300}-\u{1FAFF}]/gu, '')
.substring(0, 80)
.trim();
let md = `# ${rawTitle || 'Ylilauta Thread ' + threadId}\n\n[Original Thread](${threadUrl})\n\n`;
const posts = thread.querySelectorAll('.post.id-fixed');
posts.forEach(post => {
const userId = post.querySelector('.userid')?.textContent || '???';
const postId = post.querySelector('.post-id')?.textContent || '???';
const time = post.querySelector('.time')?.textContent?.trim() || '';
const messageHtml = post.querySelector('.post-message')?.innerHTML || '';
const message = htmlToMarkdown(messageHtml);
const media = post.querySelector('figure.file');
let mediaLine = '';
if (media) {
const src = media.getAttribute('data-file-src');
const type = media.getAttribute('data-file-type');
if (['jpg','jpeg','png','gif','webp','avif'].includes(type)) {
mediaLine = `\n\n`;
} else if (['mp4','webm'].includes(type)) {
mediaLine = `\n[▶️ Video](${src})\n`;
}
}
md += `---\n**#${userId}** | ID: ${postId} | ${time}\n\n${message}${mediaLine}\n`;
});
const filename = `ylilauta.org_${threadId}${safeTitle ? '_' + safeTitle : ''}.md`;
const blob = new Blob([md], {type: 'text/markdown'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
/**
* UI observer to inject the export button
*/
const observer = new MutationObserver(() => {
const dropdownNav = document.querySelector('.dropdown-container nav');
if (!dropdownNav) return;
if (dropdownNav.querySelector('.export-md-button')) return;
const exportButton = document.createElement('button');
exportButton.className = 'text-button export-md-button';
exportButton.innerHTML = '<span class="icon-download2"></span>Export .md';
exportButton.onclick = () => {
exportThread();
document.querySelector('.dropdown-container')?.remove();
};
const threadSection = Array.from(dropdownNav.querySelectorAll('h4')).find(h4 => /lanka/i.test(h4.textContent));
if (threadSection) {
const ref = threadSection.nextElementSibling;
ref?.parentNode.insertBefore(exportButton, ref);
} else {
dropdownNav.appendChild(exportButton);
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();