// ------------------------------------------------------- // FILE: proses-artikel.ts // Arsitektur Baru: Menggunakan Cheerio + Bun Native API // ------------------------------------------------------- import { readdirSync, existsSync } from "node:fs"; import { join } from "node:path"; import * as cheerio from "cheerio"; // FIX #11 — Jumlah file yang diproses secara paralel const CONCURRENCY = 5; const artikelDir = join(process.cwd(), "artikelx"); if (!existsSync(artikelDir)) { console.error(`❌ Folder tidak ditemukan: ${artikelDir}`); process.exit(1); } const files = readdirSync(artikelDir).filter((f) => f.endsWith(".html")); if (files.length === 0) { console.warn("⚠️ Tidak ada file .html di folder artikelx/"); process.exit(0); } // ------------------------------------------------------- // HELPERS // ------------------------------------------------------- // FIX #9 — Diperlukan untuk build single regex dari array pattern string function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // FIX #2 — Escape HTML entity sebelum text node masuk ke replaceWith() // Tanpa ini, karakter < > & di teks bisa diinterpretasi sebagai HTML tag function escapeHtml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } // FIX #8 — Validasi URL scheme sebelum diinjek ke href attribute function isSafeUrl(url: string): boolean { if (!url) return false; if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) return true; try { const { protocol } = new URL(url); return protocol === "https:" || protocol === "http:" || protocol === "mailto:"; } catch { return false; } } // ------------------------------------------------------- // PRE-PROCESSOR TEKS GLOBAL // ------------------------------------------------------- const brandingPatterns = [ " - Layar Kosong", " | Layar Kosong", " - layar kosong", " | layar kosong", " - LAYAR KOSONG", " | LAYAR KOSONG", " – Layar Kosong", " — Layar Kosong", " - dalam web id", " | dalam web id", " - Dalam web id", " | Dalam web id", " - Dalam Web Id", " | Dalam Web Id", " - DALAM WEB ID", " | DALAM WEB ID", " - dalam.web.id", " | dalam.web.id", " - Dalam.web.id", " | Dalam.web.id", " - Dalam.Web.Id", " | Dalam.Web.Id", " - DALAM.WEB.ID", " | DALAM.WEB.ID", " – dalam.web.id", " — dalam.web.id", "-Layar Kosong", "|Layar Kosong", "-dalam.web.id", "|dalam.web.id", ]; // FIX #9 — Satu regex pass gantikan 30+ loop replaceAll terpisah const brandingRegex = new RegExp(brandingPatterns.map(escapeRegex).join("|"), "g"); function preProcessRawText(html: string): string { let out = html; // FIX #9 — Single pass out = out.replace(brandingRegex, ""); out = out .replaceAll(``, `
`) .replaceAll("Dalam Web Artikel", "Jaga Data Pribadi Tetap Aman") .replaceAll("Dalam Web Id", "Jaga Data Pribadi Tetap Aman") .replaceAll("Dalam.Web.Id", "Jaga Data Pribadi Tetap Aman") .replaceAll("Redaksi Dalam Web", "Jaga Data Pribadi Tetap Aman") .replaceAll("Dalam.web.id", "Jaga Data Pribadi Tetap Aman") .replaceAll("DALAM.WEB.ID", "Jaga Data Pribadi Tetap Aman") .replaceAll("Dalam.Web.ID", "Jaga Data Pribadi Tetap Aman") .replace(/\b dalam\.web\.id /g, " Jaga Data Pribadi Tetap Aman ") .replace(/\b Dalam\.web\.id /g, " Jaga Data Pribadi Tetap Aman ") .replaceAll("twitter.com", "x.com") .replaceAll("©", "🄯") .replaceAll("©", "🄯") .replaceAll("All Rights Reserved", "Copyleft, All Rights Reversed") .replaceAll("Font Awesome 5 Free", "Font Awesome 7 Free") .replaceAll("Font Awesome 6 Free", "Font Awesome 7 Free") .replaceAll("Ditulis oleh", "Artikel dari"); return out; } // ------------------------------------------------------- // PARSER STRAY MARKDOWN INLINE // ------------------------------------------------------- function parseMarkdownInline(text: string): string { // Escape dulu sebelum regex markdown berjalan. // Karakter seperti < > & di konten asli tidak akan bocor jadi tag HTML. let out = escapeHtml(text); // --- Bold --- // Tambah underscore variant (__text__) selain asterisk (**text**) out = out.replace(/\*\*(.*?)\*\*/g, "$1"); out = out.replace(/(?$1"); // Catatan guard __bold__: // (?$1"); // Tambah underscore variant (_text_) dengan guard yang sama // Guard (?$1"); // --- Strikethrough & Inline Code --- out = out.replace(/~~(.*?)~~/g, "$1");
// --- Markdown Link ---
// FIX #8 — URL hanya diinjek jika scheme-nya aman (https/http/mailto/relative)
out = out.replace(/\[([^\]]+)\]\((.*?)\)/g, (_, linkText, href) => {
if (!isSafeUrl(href)) return `[${linkText}](${href})`;
return `${linkText}`;
});
return out;
}
// -------------------------------------------------------
// CORE PIPELINE PROCESSOR (CHEERIO POWERED)
// -------------------------------------------------------
function prosesHtmlDenganCheerio(rawHtml: string): string {
const cleanedTextHtml = preProcessRawText(rawHtml);
const $ = cheerio.load(cleanedTextHtml);
// === 1. MANIPULASI ATRIBUT & ELEMEN SPESIFIK ===
// FIX #1 — Hapus [.] dari string pencarian.
$('link[rel*="icon"]').each((_, el) => {
let href = $(el).attr("href");
if (href) {
href = href
.replace(
"https://dalam.web.id/assets/apple-touch-icon.png",
"https://dalam.web.id/ext/icons/apple-touch-icon.png"
)
.replace(
"https://dalam.web.id/apple-touch-icon.png",
"https://dalam.web.id/ext/icons/apple-touch-icon.png"
);
$(el).attr("href", href);
}
});
// FIX #4 — Hilangkan "Jaga Data Pribadi Tetap Aman" dari kondisi delete.
$("a").each((_, el) => {
const text = $(el).text().trim();
if (text === "Dalam Web") {
$(el).remove();
} else if (text === "dalam.web.id") {
$(el).text("Jaga Data Pribadi Tetap Aman");
}
});
// FIX #3 — Anchored ke awal string via startsWith + break setelah match pertama.
$('meta[name="description"], meta[property="og:description"], meta[name="twitter:description"]').each((_, el) => {
let content = $(el).attr("content");
if (content) {
const fluff = [
"Analisis kritis ", "Analisis lengkap ", "Analisis mendalam ", "Analisis tajam ",
"Artikel mendalam ", "Membedah ", "Bedah mendalam ", "Mengenal ",
"Mengupas ", "Mengupas tuntas ", "Panduan lengkap ", "Panduan mendalam ",
"Panduan santai ", "Mengapa ", "Penjelasan lengkap ","Penjelasan santai ",
"Penjelasan mendalam ","Simak ulasan mendalam ","Ulasan mendalam ", "Pelajari ",
"Simak ", "Temukan ",
];
for (const word of fluff) {
if (content.startsWith(word)) {
content = content.slice(word.length);
break;
}
}
$(el).attr("content", content);
}
});
// FIX #6 — Ganti if-if dengan else-if chain.
$('[class*="fa"]').each((_, el) => {
const $el = $(el);
if ($el.hasClass("far") && $el.hasClass("fa-copyright")) {
$el.removeClass("far fa-copyright").addClass("fa-brands fa-creative-commons-by");
} else if ($el.hasClass("fas") && $el.hasClass("fa-copyright")) {
$el.removeClass("fas fa-copyright").addClass("fa-brands fa-creative-commons-by");
} else if ($el.hasClass("fab")) {
$el.removeClass("fab").addClass("fa-brands");
} else if ($el.hasClass("far")) {
$el.removeClass("far").addClass("fa-regular");
} else if ($el.hasClass("fas")) {
$el.removeClass("fas").addClass("fa-solid");
}
});
// === 2. DETEKSI & PARSING STRAY MARKDOWN ===
// FIX MD-A — Tambah "span" ke selector.
// Sebelumnya text node di dalam tidak terproses sama sekali karena
// contents() tidak rekursif — dia hanya lihat direct children dari ,
dan tidak tumpang tindih.
const TEXT_ELEMENTS =
"p, li, h1, h2, h3, h4, h5, h6, td, th, blockquote, figcaption, dt, dd, caption, span";
$("body").find(TEXT_ELEMENTS).each((_, el) => {
const $el = $(el);
// FIX MD-B — Ganti $el.is() (dead code) dengan $el.closest() untuk ancestor check.
//
// Bug lama: $el.is("code, pre, script,...") tidak pernah true karena TEXT_ELEMENTS
// tidak mengandung tag-tag tersebut sama sekali. Jadi kondisi ini adalah dead code murni.
//
// Fix: .closest() memeriksa ke atas pohon DOM, bukan elemen itu sendiri.
// Ini yang memastikan teks di dalam ...
atau ...
// (HTML tidak valid tapi bisa ada di artikel lama) tidak ikut diproses.
if ($el.closest("pre, code, script, style, textarea, noscript, a").length > 0) return;
$el.contents().each((_, child) => {
if (child.type === "text") {
const oldText = child.data || "";
const parsedHtml = parseMarkdownInline(oldText);
// FIX #2 — Bandingkan dengan escapeHtml(oldText), bukan oldText mentah.
if (parsedHtml !== escapeHtml(oldText)) {
$(child).replaceWith(parsedHtml);
}
}
});
});
// === 3. INJEKSI ELEMEN & ASSETS BARU (DENGAN STRICT GUARD) ===
if ($('head link[href*="pemandu.css"]').length === 0) {
$("head").append(``);
}
// FIX #7 — Tambah rel="noopener noreferrer" ke semua target="_blank".
if ($("footer").length && $('footer a[href="/data-deletion"]').length === 0) {
const footerLinks = [
`☕`,
`🛡️`,
`⚠️`,
`📝`,
`📚`,
`🔰`,
`⚔️`,
].join(" ");
$("footer").append(` ${footerLinks}`);
}
if ($("#progress").length === 0) {
$("body").append(``);
}
if ($("#layar-kosong-header").length === 0) {
$("body").append(``);
}
if ($(".search-floating-container").length === 0) {
$("body").append(
`