mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-09-03 04:02:40 +00:00
a96ba665ac
* refactor(assets)!: rewrite content encryption with AES-256-GCM and PBKDF2
- Remove legacy Base64 obfuscation layer (content-encryption.html)
- Replace <cipher-text> with <template> for inert content storage
- Single-layer AES-256-GCM encryption via post-build script
- PBKDF2 key derivation (100k iterations) for encryption
- PBKDF2-protected password verification (data-verify-salt)
- Depth-tracking parser for nested shortcode support
- Dev mode: plaintext content used directly without encryption
- Remove crypto-js and xxhash-wasm vendored dependencies
* feat(post-encrypt): prepare package for npm publishing
- Add bin entry, build script, and dist output for npx usage
- Replace @hugo-fixit/shared with standalone implementations
- Add CLI shebang for direct execution
- Add package README with usage documentation
- Remove FIXIT_ENCRYPT_INPUT env var (redundant with --input)
- Add encryption detection warning in assets.html
- Remove Post-build Encryption sections from READMEs
* chore: integrate post-encrypt into build pipeline and use consola
- Replace console with consola for post-encrypt logging
- Improve verification messages (no templates, count, etc.)
- Simplify build scripts: encrypt runs as part of main build
- Update encryption detection warning to use npx command
* feat(assets): add encryption dev warning admonition
- Add danger admonition in single.html for full-page encryption (dev mode only)
- Add encryption detection warning in assets.html console
- Improve post-encrypt verification messages with template count
* feat(assets): improve encryption i18n, cache security, and error handling
- Add encryptionWarning and encryptionCommand i18n keys for all 16 languages
- Store PBKDF2 verification hash in localStorage cache for better security
- Show decryption errors via flashTooltip instead of console only
- Add FixItDecryptor type definitions in global.ts
- Move encryption detection to init/detection-encryption.html with batched warning
- Remove redundant per-page warning from assets.html
* feat(assets): redesign fixit-decryptor UI with card layout
- Page-level: card form with lock icon header, password input with key icon, primary-colored unlock button, circular re-encrypt button
- Shortcode-level: connected input+button design (search bar style) with focus ring sync
- Remove loading spinner, use display:none/flex toggle via .initialized class
- Move lock icon styling to UnoCSS (text-primary text-xl)
- Initialize CellTooltip on re-encrypt button
- Fix JSDoc @param warnings in global.ts
* refactor(assets): unify TOC template and decryptor animations
- TOC always rendered in `<template data-toc>`, containers populated by initToc()
- Moved TOC scroll/resize handling from events.ts to toc.ts (syncTocLayout, syncTocActiveState)
- Removed EventsModule toc dependency, updated public-api.ts constructor
- Removed visibility:hidden from #toc-auto
- Decryptor: form always visible, @starting-style for fade-in on init
- Decryptor: content expand/collapse animation using height + opacity
- Removed encrypted-hidden from TOC elements in single.html
* refactor(assets): add target to fixit:decrypted event and simplify handlers
- Add { target: Element } payload to fixit:decrypted in event-bus.ts
- Remove $content closure from fixit-decryptor init(), use detail.target
- Update content.ts and toc.ts to use detail.target from event
* fix(assets): use eventBus for TOC scroll/resize listeners and eliminate redundant template parsing
- Replace raw window scroll/resize listeners in TocModule with eventBus
subscriptions (fixit:scroll/fixit:resize), reusing EventsModule's
throttle and debounce instead of duplicating un-throttled handlers.
- Refactor hasUnencryptedTemplate in post-encrypt to accept the already-
computed matches array, avoiding a redundant findEncryptionTemplates
call on the same HTML content.
- Remove crypto-js and xxhash-wasm from README credits.
* style(assets): soften decryptor button background and add form hover shadow
* build(workflow): add post-encrypt step to build script
198 lines
6.7 KiB
TypeScript
198 lines
6.7 KiB
TypeScript
import type { CodeService, ContentService, CoreService } from '../core/tokens'
|
|
import { eventBus } from '../core/event-bus'
|
|
import { createCopyText, flashCopiedTooltip } from '../utils'
|
|
|
|
const CellTooltip = window.CellTooltip
|
|
const copyText = createCopyText()
|
|
|
|
/**
|
|
* Content module — details toggle, tooltips, footnotes, SVG icons, and link guard.
|
|
*
|
|
* Responsibilities:
|
|
* - Attach toggle behaviour to `.details` elements.
|
|
* - Initialize CellTooltip on action buttons, copy buttons, and footnotes.
|
|
* - Fetch and inline SVG icons from `data-svg-src` attributes.
|
|
* - Set up link guard dialog for external link confirmation.
|
|
* - Re-initialize components after encrypted content is decrypted.
|
|
*/
|
|
export class ContentModule implements ContentService {
|
|
constructor(
|
|
private readonly core: CoreService,
|
|
private readonly code: CodeService,
|
|
) {}
|
|
|
|
/** Fetch and inline SVG icons referenced by `data-svg-src` attributes. */
|
|
initSVGIcon() {
|
|
document.querySelectorAll<HTMLElement>('[data-svg-src]').forEach(($icon) => {
|
|
fetch($icon.dataset.svgSrc!)
|
|
.then(response => response.text())
|
|
.then((svg) => {
|
|
const $temp = document.createElement('div')
|
|
$temp.insertAdjacentHTML('afterbegin', svg)
|
|
const $svg = $temp.firstChild as SVGElement
|
|
$svg.dataset.svgSrc = $icon.dataset.svgSrc
|
|
$svg.classList.add('icon')
|
|
const $titleElements = $svg.getElementsByTagName('title')
|
|
$titleElements.length && $svg.removeChild($titleElements[0])
|
|
$icon.parentElement!.replaceChild($svg, $icon)
|
|
})
|
|
.catch((err) => {
|
|
console.error(err)
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Initialize the link-guard dialog and bind click handlers on guarded links.
|
|
* @param target - The root element to search for guarded links.
|
|
*/
|
|
initLinkGuardDialog(target: Element | Document = document) {
|
|
const dialog = document.getElementById('link-guard-dialog') as HTMLDialogElement
|
|
if (!dialog)
|
|
return
|
|
|
|
const $target = dialog.querySelector<HTMLElement>('.target')
|
|
const $copy = dialog.querySelector<HTMLElement>('.copy-icon-btn')
|
|
const $confirm = dialog.querySelector<HTMLElement>('.confirm-btn')
|
|
const $cancel = dialog.querySelector<HTMLElement>('.cancel-btn')
|
|
|
|
const _closeDialog = () => {
|
|
if (dialog.open)
|
|
dialog.close()
|
|
;(dialog as any)._target = null
|
|
if ($target) {
|
|
$target.textContent = '-'
|
|
}
|
|
}
|
|
|
|
if (!dialog.dataset.init) {
|
|
dialog.dataset.init = 'true'
|
|
|
|
$confirm!.addEventListener('click', () => {
|
|
if ((dialog as any)._target) {
|
|
window.open((dialog as any)._target, '_blank', 'noopener,noreferrer')
|
|
}
|
|
_closeDialog()
|
|
})
|
|
|
|
$cancel!.addEventListener('click', _closeDialog)
|
|
|
|
$copy!.addEventListener('click', () => {
|
|
const textToCopy = (dialog as any)._target || ''
|
|
if (!textToCopy)
|
|
return
|
|
copyText(textToCopy).then(() => {
|
|
flashCopiedTooltip($copy!)
|
|
})
|
|
})
|
|
}
|
|
|
|
target.querySelectorAll<HTMLAnchorElement>('a[target="_blank"][data-guard="modal"]:not([data-init])').forEach(($link) => {
|
|
$link.dataset.init = 'true'
|
|
$link.addEventListener('click', (e) => {
|
|
e.preventDefault()
|
|
let targetUrl = $link.href
|
|
try {
|
|
const guardUrl = new URL($link.href)
|
|
targetUrl = guardUrl.searchParams.get('target') || targetUrl
|
|
}
|
|
catch {
|
|
// Ignore malformed URLs and fall back to the original href.
|
|
}
|
|
|
|
;(dialog as any)._target = targetUrl
|
|
if ($target) {
|
|
$target.textContent = targetUrl
|
|
}
|
|
dialog.showModal()
|
|
;(document.activeElement as HTMLElement)?.blur()
|
|
}, false)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Attach toggle behaviour to `.details` elements.
|
|
* @param target - The root element to search within.
|
|
*/
|
|
initDetails(target: Element | Document = document) {
|
|
target.querySelectorAll<HTMLElement>('.details:not(.disabled)').forEach(($details) => {
|
|
const $summary = $details.querySelector<HTMLElement>('.details-summary')!
|
|
$summary?.addEventListener('click', () => {
|
|
$details.classList.toggle('open')
|
|
}, false)
|
|
})
|
|
}
|
|
|
|
/** Convert footnote refs into tooltip-enabled elements. */
|
|
#initFootnotes() {
|
|
const $footnoteRefs = document.querySelectorAll<HTMLElement>('#content sup[id^="fnref:"]')
|
|
const $footnotes = document.querySelector<HTMLElement>('.footnotes[role="doc-endnotes"]')
|
|
if (!$footnoteRefs.length || !$footnotes)
|
|
return
|
|
const footnoteMap = new Map<HTMLElement, HTMLElement>()
|
|
$footnoteRefs.forEach(($ref) => {
|
|
if (this.core.config.tooltip) {
|
|
const $link = $ref.querySelector<HTMLAnchorElement>('a.footnote-ref')
|
|
if ($link) {
|
|
$link.addEventListener('click', (e) => {
|
|
e.preventDefault()
|
|
}, false)
|
|
}
|
|
}
|
|
const id = $ref.id.replace('fnref:', '')
|
|
const $footnoteContent = $footnotes.querySelector<HTMLElement>(`[id="fn:${id}"]`)
|
|
if ($footnoteContent) {
|
|
const $clonedContent = $footnoteContent.cloneNode(true) as HTMLElement
|
|
const $backref = $clonedContent.querySelector('.footnote-backref')
|
|
if ($backref)
|
|
$backref.remove()
|
|
footnoteMap.set($ref, $clonedContent)
|
|
}
|
|
})
|
|
footnoteMap.forEach(($content, $ref) => {
|
|
if ($ref.hasAttribute('title'))
|
|
return
|
|
$ref.setAttribute('title', $content.textContent!.trim())
|
|
if (this.core.config.tooltip) {
|
|
CellTooltip.getOrCreateInstance($ref)
|
|
}
|
|
})
|
|
}
|
|
|
|
/** Initialize CellTooltip on action buttons, copy buttons, and footnotes. */
|
|
initTooltip() {
|
|
if (!this.core.config.tooltip)
|
|
return
|
|
CellTooltip.initAll('li[data-task] > span[title]', { placement: 'right' })
|
|
CellTooltip.initAll('.action-btn[title]', { placement: 'bottom' })
|
|
CellTooltip.initAll('.copy-icon-btn[title]', { placement: 'top' })
|
|
CellTooltip.initAll('.fixit-encryptor-btn[title]')
|
|
this.#initFootnotes()
|
|
}
|
|
|
|
/**
|
|
* Re-initialize content components within a target element.
|
|
* Useful after AJAX/pjax loads or dynamic content injection.
|
|
* @param target - The root element to initialize components within.
|
|
*/
|
|
initContent(target: Element | Document = document) {
|
|
this.initDetails(target)
|
|
this.code.initCodeWrapper()
|
|
this.code.initCodeTabs()
|
|
this.code.initDiagramCopyBtn()
|
|
this.initTooltip()
|
|
this.initLinkGuardDialog(target)
|
|
}
|
|
|
|
setup() {
|
|
this.initContent()
|
|
this.initSVGIcon()
|
|
eventBus.on('fixit:decrypted', ({ detail }) => {
|
|
this.initContent(detail.target)
|
|
})
|
|
eventBus.on('fixit:partial-decrypted', ({ detail }) => {
|
|
this.initContent(detail.target)
|
|
})
|
|
}
|
|
}
|