← Files DefuddleARCHIVED FILE
skills/defuddle-web-content/references/source/src/constants.ts
27.8 KB · Oct 2, 2026 · 00:35 UTC
// Entry point elements
// These are the elements that will be used to find the main content
export const ENTRY_POINT_ELEMENTS = [
'#post',
'.post-content',
'.post-body',
'.article-content',
'#article-content',
'.js-article-content',
'.article_post',
'.article-wrapper',
'.entry-content',
'.content-article',
'.instapaper_body',
'.post',
'.markdown-body',
'article',
'[role="article"]',
'main',
'[role="main"]',
'.article-body',
'#content',
'body' // ensures there is always a match
];
export const MOBILE_WIDTH = 600;
export const BLOCK_ELEMENTS = ['div', 'section', 'article', 'main', 'aside', 'header', 'footer', 'nav', 'content'];
export const BLOCK_ELEMENTS_SELECTOR = BLOCK_ELEMENTS.join(',');
export const BLOCK_ELEMENTS_SET = new Set(BLOCK_ELEMENTS);
// All block-level HTML elements (includes BLOCK_ELEMENTS + semantic content blocks)
export const BLOCK_LEVEL_ELEMENTS = new Set([
...BLOCK_ELEMENTS,
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
'pre', 'blockquote', 'figure', 'figcaption',
'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
'details', 'summary', 'address', 'hr',
'form', 'fieldset'
]);
// Elements that should not be unwrapped
export const PRESERVE_ELEMENTS = new Set([
'pre', 'code', 'table', 'thead', 'tbody', 'tr', 'td', 'th',
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
'figure', 'figcaption', 'picture',
'details', 'summary',
'blockquote',
'form', 'fieldset'
]);
// Inline elements that should not be unwrapped
export const INLINE_ELEMENTS = new Set([
'a', 'span', 'strong', 'em', 'i', 'b', 'u', 'code', 'br', 'small',
'sub', 'sup', 'mark', 'date', 'del', 'ins', 'q', 'abbr', 'cite', 'relative-time', 'time',
'font'
]);
// Content element selectors — elements whose presence indicates real article
// content rather than navigation, promotional, or decorative material.
// Used to protect legitimate content from removal by scoring, content pattern
// detection, and other heuristics.
export const CONTENT_ELEMENT_SELECTOR = [
'math', '[data-mathml]',
'.katex', '.katex-mathml', '.katex-display',
'.MathJax', '.MathJax_Display', '.MathJax_SVG',
'mjx-container',
'pre', 'code',
'table',
'img', 'picture', 'video',
'blockquote',
'figure',
].join(', ');
// Selectors to be removed
const HIDDEN_EXACT_SKIP_SELECTORS = [
'[hidden]',
'[aria-hidden="true"]',
'.hidden',
'.invisible',
];
// Paywall sites (e.g. Future PLC) mark gated paragraphs with
// aria-hidden="true" + class="paywall". Preserve those so the
// article text isn't stripped, which would trigger a low-word-count
// retry with partial selectors disabled.
const HIDDEN_EXACT_SELECTORS = HIDDEN_EXACT_SKIP_SELECTORS.map(s =>
s === '[aria-hidden="true"]' ? '[aria-hidden="true"]:not([class*="math"]):not(svg):not([class*="paywall"])' : s
);
// Pre-joined selector strings (these arrays are constant)
export const HIDDEN_EXACT_SELECTOR = HIDDEN_EXACT_SELECTORS.join(',');
export const HIDDEN_EXACT_SKIP_SELECTOR = HIDDEN_EXACT_SKIP_SELECTORS.join(',');
export const EXACT_SELECTORS = [
// scripts, styles
'noscript',
'script:not([type^="math/"])',
'style',
'meta',
'link',
// empty media elements (src set by JS at runtime, not in raw HTML)
'audio:not([src]):not(:has(source))',
'video:not([src]):not(:has(source))',
// JW Player embeds (removes entire player including controls, shortcuts, overlays)
'.jwplayer',
// ads
'.ad:not([class*="gradient"])',
'[class^="ad-" i]',
'[class$="-ad" i]',
'[data-ad-wrapper]',
'[id^="ad-" i]',
'[id$="-ad" i]',
'[role="banner" i]',
'[alt*="advert" i]',
'.promo',
'.Promo',
'#barrier-page', // ft.com
'.alert',
'[rel="sponsored" i]',
'[href*="source=promotion" i]',
// comments
'[id="comments" i]',
'[id="comment" i]',
// cover images
'div[class*="cover-"]',
'div[id*="cover-"]',
// breadcrumbs (custom web component tag)
'ads-breadcrumbs',
// header, nav
// Exclude headers that contain multiple paragraphs — some sites (e.g. Webflow blogs)
// use <header> as the main content wrapper rather than a navigation container.
// A single <p> (subtitle/deck) is still a page header, not article content.
'header:not(:has(p + p)):not(:has(img))',
// Position-fixed/sticky headers are always site navigation (never content wrappers),
// so they don't need the :not(:has(img)) guard — their images are logos/icons.
'header[class~="fixed"]',
'header[class~="sticky"]',
'.header:not(.banner)',
'#header',
'#Header',
'#banner',
'#Banner',
'nav',
'.navigation',
'#navigation',
// '.hero', // see issue #132
'[role="navigation" i]',
'[role="dialog" i]',
'[role="alertdialog" i]',
'[role*="complementary" i]',
'[class*="pagination" i]',
'.menu',
// '#menu', // see issue #106
'#siteSub',
// '.fixed', see issue #44
'.previous',
// metadata
'.author',
'.Author',
'[class$="_bio"]',
'#categories',
'.contributor',
'.date',
'#date',
'[data-date]',
'.entry-meta',
'.meta',
'.tags',
'#tags',
'[rel="tag"]',
'.headline',
'#headline',
'#title',
'#Title',
'#articleTag',
// '[href*="/category"]', // see issue #131
// '[href*="/categories"]', // see issue #131
// '[href*="/tag/"]',
// '[href*="/tags/"]',
// '[href*="/topics"]', // see issue #131
'[href*="/author/"]',
'[href*="/author?"]',
'[href$="/author"]',
'a[href*="copyright.com"]',
'a[href*="google.com/preferences"]',
'[href="#top"]',
'[href="#Top"]',
'[href="#page-header"]',
'[href="#content"]',
'[href="#site-content"]',
'[href="#main-content"]',
'[href^="#main"]',
'[src*="author"]',
// table of contents
'.toc',
'.Toc',
'#toc',
'[href*="#toc"]',
// footer
'footer',
'ignore',
// inputs, forms, elements
'.aside',
'aside:not([class*="callout"])',
'button',
// '[role="button"]', Medium images
'canvas',
'date',
'dialog',
'fieldset',
'form',
'input:not([type="checkbox"])',
// UI toggle checkboxes (sidebar/menu/nav/drawer/hamburger patterns).
// Markdown task list checkboxes use class="task-list-item-checkbox", so
// none of these patterns affect them.
'input[type="checkbox"][class*="sidebar" i]',
'input[type="checkbox"][id*="sidebar" i]',
'input[type="checkbox"][class*="drawer" i]',
'input[type="checkbox"][id*="drawer" i]',
'input[type="checkbox"][class*="hamburger" i]',
'input[type="checkbox"][id*="hamburger" i]',
'input[type="checkbox"][class*="toggle" i]',
'input[type="checkbox"][id*="toggle" i]',
'input[type="checkbox"][class*="trigger" i]',
'input[type="checkbox"][id*="trigger" i]',
'label',
'option',
'select',
'[role="listbox"]',
'[role="option"]',
'textarea',
// 'time', // see issue #136
// 'relative-time', // see issue #136
// hidden
...HIDDEN_EXACT_SELECTORS,
// Note: [style*="display: none"] removed — substring match causes false positives
// with CSS custom properties like --footer-display: none. The removeHiddenElements
// step handles inline style detection with a proper regex.
// iframes
'instaread-player',
'iframe:not([src])',
'iframe[src*="blink.net"]',
'iframe[src*="giscus.app"]',
'iframe[src*="tinypass.com"]',
'iframe[src*="trinitymedia.ai"]',
// logos
'[class="logo" i]',
'#logo',
'#Logo',
// newsletter
'#newsletter',
'#Newsletter',
'.subscribe',
// Substack inline clutter
'[data-component-name="ButtonCreateButton"]',
'[data-component-name="DigestPostEmbed"]',
'[data-component-name="SubscribeWidgetToDOM"]',
'[class*="digestPostEmbed"]', // class has hash suffix in browser-rendered DOM
// hidden for print
'.noprint',
'[data-print-layout="hide" i]',
'[data-block="donotprint" i]',
// footnotes, citations
'[class*="clickable-icon" i]',
'li span[class*="ltx_tag" i][class*="ltx_tag_item" i]',
'a[href^="#"][class*="anchor" i]',
'a[href^="#"][class*="ref" i]:not(.ltx_ref):not(.footnote-backref)',
// link lists
'[data-container*="most-viewed" i]',
// sidebar
'.sidebar',
'.Sidebar',
'#sidebar',
'#Sidebar',
'#side-bar',
'#secondary',
'#sitesub',
// sitemap
'[href*="/sitemap/sitemap.xml"]', // Medium.com
// skip links
'[data-link-name*="skip" i]',
'[aria-label*="skip" i]',
// social
'[title^="Share on" i]',
// dismiss/close buttons
'[aria-label="Dismiss" i]',
'[aria-label="Close" i]',
// icon SVGs (Font Awesome)
'svg[data-icon]',
// other
'[data-testid="load-more-posts"] + div', // NY Times lazy loader
'.copyright',
'#copyright',
'.licensebox',
'#page-info',
'#rss',
'#feed',
'.gutter',
'#primaryaudio', // NPR
'#NYT_ABOVE_MAIN_CONTENT_REGION',
'[data-testid="photoviewer-children-figure"] > span', // NY Times
'table.infobox',
'[data-optimizely="related-articles-section" i]', // The Economist
'[data-orientation="vertical"]',
'.gh-header-sticky', // GitHub
'[data-testid="issue-metadata-sticky"]', // GitHub
];
export const EXACT_SELECTORS_JOINED = EXACT_SELECTORS.join(',');
// Attributes to test against for partial matches
export const TEST_ATTRIBUTES = [
'class',
'id',
'data-component',
'data-test',
'data-testid',
'data-test-id',
'data-qa',
'data-cy'
];
// Removal patterns tested against attributes above
// Case insensitive, partial matches allowed
export const PARTIAL_SELECTORS = [
'a-statement',
'(?<!main-)access-wall', // avoid matching data-test="main-access-wall" (content container)
'activitypub',
'actioncall',
'adcontainer',
'addcomment',
'addtoany',
'advert',
// '-ad-', howtogeek.com
'adlayout',
'ad-tldr',
'ad-placement',
'adplacehold',
'ads-container',
'_ad_',
'AdBlock_',
'AdUnit',
'after_content',
'after_main_article',
'afterpost',
'allterms',
'-alert-',
'alert-box',
// 'appendix',
'_archive',
'around-the-web',
'aroundpages',
'article-author',
'article-badges',
'article-banner',
'article-bottom-section',
'article-bottom',
'article-category',
'article-card',
'article-citation',
'article-continues',
'article__copy',
'article_date',
'article-date',
'article-end ',
'articleheader',
'article_header',
'article-header',
'article__header',
'article__hero',
'article__info',
'article-info',
'article-meta',
'article_meta',
'article__meta',
'articlename',
'article-subject',
'article_subject',
'article-snippet',
'article-separator',
'article--share',
'article-share',
'article--topics',
'article-tools',
'articletags',
'article-tags',
'article_tags',
'articletitle',
'article-title',
'article_title',
'articletopics',
'article-topics',
// 'article-type',
'article-actions',
'article--lede', // The Verge
'articlewell',
'associated-people',
'ambient-video__button',
'audio-card',
'beyondwords',
// 'author', Gwern
// '-author',
'about-author',
'author-bio',
'author-box',
'author-info',
'author_info',
'authorm',
'author-mini-bio',
'author-name',
'author-publish-info',
'authored-by',
'avatar',
'back-to-top',
'backlink_container',
'backlinks-section',
// 'banner',
'bio-block',
'biobox',
'blog-pager',
'bookmark-',
'-bookmark',
'bottominfo',
'bottomnav',
'bottom-of-article',
'bottom-wrapper',
'brand-bar',
'bcrumb',
'breadcrumb',
'brdcrumb',
'crumbs',
'bubblewrapper',
'button-wrapper',
'buttons-container',
'btn-',
'-btn',
'byline',
'captcha',
'card-text',
'card-media',
'card-post',
// 'carousel',
'carouselcontainer',
'carousel-container',
'cat_header',
'cat-overlay',
'catlinks',
'_categories',
'card-author',
'card-content',
'chapter-list', // The Economist
'collections',
'comments',
'-comment\\b', // word boundary avoids false matches like Wikipedia's 'rt-commentedText'. Comments in code blocks are skipped in removeBySelector
'commentbox',
'comment-button',
'commentcomp',
'comment-content',
'comment-count',
'comment-form',
'comment-number',
'comment-respond',
'comment-thread',
'comment-wrap',
'complementary',
'consent',
'contact-',
'contactus',
'cookie.law',
'content-card', // The Verge
'copycontent',
'copy-tooltip',
'content-topics',
'contentpromo',
'context-bar',
'context-widget', // Reuters
'core-collateral',
'cover-image',
'cover-photo',
'cover-wrap',
'created-date',
'creative-commons_',
'c-subscribe',
'_cta',
'-cta',
'cta-',
'cta_',
'current-issue', // The Nation
'custom-list-number',
'dateline',
'dateheader',
'date-header',
'date-pub',
// 'dialog',
'disclaimer',
'disclosure',
'discussion',
'discuss_',
'-dismiss',
'disqus',
'donate',
'donation',
'dropdown', // Ars Technica
'editorial_contact',
'editorial-contact',
'element-invisible',
'elementor-shortcode',
'eletters',
'emailsignup',
'emoji-bar',
'engagement-widget',
'enhancement-',
'entry-author-info',
'entry-categories',
'entry-date',
// 'entry-meta',
'entry-title',
'entry-utility',
'-error',
'error-',
'eyebrow',
'expand-reduce',
'external-anchor',
'externallinkembedwrapper', // The New Yorker
'extra-services',
'extra-title',
'facebook',
'fancy-box',
'favorite',
'featured-content',
'feature_feed',
'feedback',
'feed-links',
'field-site-sections',
'filed',
'fixheader',
'floating-vid',
// 'follow',
'follower',
'footer',
'footnote-back',
'footnoteback',
'form-group',
'for-you',
'frontmatter',
'further-reading',
'fullbleedheader',
'gallery-count',
'gated-popup',
'gh-feed',
'gist-meta',
// 'global',
// 'google',
'goog-',
'graph-view',
'hamburger',
'hawk-', // Future PLC affiliate deal widgets
'header-pattern', // The Verge
// 'headlines', Mercurynews
'hero[_\\-a-z]',
// '-hidden',
'hide-for-print',
'hide-print',
'hide-when-no-script',
'hidden-print',
'hidden-sidenote',
'hidden-accessibility',
'home-link',
'icon-sidebar',
'inarticle-ad',
'infoline',
'inline-topic',
'instacartIntegration',
'interlude',
'interaction',
'itemendrow',
'intro-date',
'invisible',
'jp-no-solution',
'jp-relatedposts',
'jswarning',
'js-warning',
'jumplink',
'jumpto',
'jump-to-',
'js-skip-to-content',
'keepreading',
'keep-reading',
'keep_reading',
// 'keyword', // used in syntax highlighting
'keyword_wrap',
'kicker',
'labstab', // Arxiv
'-labels',
'language-name',
'lastupdated',
'latest-content',
'-ledes-', // The Verge
'-license',
'license-',
'lightbox-popup',
'like-button',
'link-box',
'links-grid', // BBC
'links-title', // BBC
'listing-dynamic-terms', // Boston Review
'list-tags',
'live-blog-header-live-label', // NY Times
'listinks',
'loading',
'loa-info',
// Matches 'logo' only at a class-token edge ('site-logo', 'logo-wrapper').
'(?:(?<![\\w-])logo|logo(?![\\w-]))',
'ltx_role_refnum', // Arxiv
'ltx_tag_bibitem',
'ltx_error',
'masthead',
'marketing',
'media-card',
'media-inquiry',
'-menu',
'menu-',
// 'meta-', syntax highlighting
'metadata',
'meta-bottom',
'meta-date',
'meta-row',
'might-like',
'minibio',
'more-about',
'mod-paywall',
'_modal',
'-modal',
'more-',
'morenews',
'morestories',
'more_wrapper',
'most-read',
'move-helper',
'mw-editsection',
'mw-cite-backlink',
'mw-indicators',
'mw-jump-link',
'nav-',
'nav_',
// 'navbar',
// 'navigation',
'navcontainer',
'navigation-post',
'next-',
'next_prev',
'no-script',
'newsgallery',
'news-story-title',
// 'newsletter', used on Substack
'newsletter_',
'newsletterbanner',
'newslettercontainer',
'newsletter-form',
'newsletter-signup',
'newslettersignup',
'newsletterwidget',
'newsletterwrapper',
'not-found',
'notessection',
'nomobile',
'noprint',
'onward-journey', // FT.com
'open-slideshow',
'originally-published', // Mercury News
'osano-cm',
'other-blogs',
'outline-view',
// 'overlay',
'pagefoot',
'pagehead',
'page-header',
'page-title',
'paywall_message',
'-partners',
'permission-',
'plea',
'popular',
// 'popup', Gwern
'popup_links',
// 'popover',
'pop_stories',
'pop-up',
'post__author',
'post-author',
'post-bottom',
'post__category',
'postcomment',
'postdate',
'post-date',
'post_date',
'post-details',
'post-feeds',
'postinfo',
'post-info',
'post_info',
'post-inline-date',
'post-links',
'postlist',
'post_list',
'post_meta',
'post-meta',
'postmeta',
'post_more',
'postnavi',
'post-navigation',
'postpath',
'post-preview',
'postsnippet',
'post_snippet',
'post-snippet',
'post-subject',
'posttax',
'post-tax',
'post_tax',
'posttag',
// 'post_tag', https://opexnews.fr
'post-tag',
'post_time',
'posttitle',
'post-title',
'post_title',
'post__title',
'post-ufi-button',
// 'preview', used on Obsidian Publish
'prev-post',
'prevnext',
'prev_next',
'prev-next',
'previousnext',
'press-inquiries',
'print-none',
'print-header',
'print:hidden',
'privacy-notice',
'privacy-settings',
'profile',
// 'promo',
'promo_article',
'promo-bar',
'promo-box',
'pubdate',
'pub_date',
'pub-date',
'publish_date',
'publish-date',
'publication-date',
'publicationName', // Medium
'qr-code',
'qr_code',
'quick_up',
'_rail',
'ratingscontainer',
'ratingssection',
'reactions',
'read_also',
'readmore',
'read-next',
'read_next',
'read_time',
'read-time',
'reading_time',
'reading-time',
'reading-list',
'recent-',
'recent-articles',
'recentpost',
'recent_post',
'recent-post',
'recommend',
'redirectedfrom',
'recirc',
'register',
'(?<!h[1-6]-)related',
'-relance',
'relevant',
'relposts',
'reversefootnote',
'rightcol',
'\\bnocontent\\b',
'_rss',
'rss-link',
'rubricwrapper',
'screen-reader-text',
'scroll_to',
'scroll-to',
'_search',
'-search',
'section-nav',
'series-banner',
// 'share',
// '-share', scitechdaily.com
'share-box',
'sharedaddy',
'share-icons',
'sharelinks',
'share-links',
'share-post',
'share-print',
'share-section',
'share-text',
'sharing_',
'shariff-',
'shortcode-id',
'show-for-print',
'sidebartitle',
// 'sidebar_',
'sidebar-content',
'sidebar-element',
'sidebar-wrapper',
'sideitems',
'sidebar-author',
'sidebar-item',
'side-box',
'sign-in-gate',
'similar-',
'similar_',
'similars-',
'site-index',
'site-header',
'siteheader',
'site-name',
'site-wordpress',
// 'skip-',
'skip-content',
'skip-to-content',
'skip-link',
'c-skip-link',
'_skip-link',
'-slider',
'slug-wrap',
// 'social',
'social-author',
'social-button',
'social-shar',
'social-date',
'speechify-ignore',
'speedbump',
'sponsor',
'springercitation',
'sr-only',
// '-stats',
'_stats',
// 'sticky',
'sticky-social',
'story-date',
'story-navigation',
'storyreadtime', // Medium
'storysmall',
'storypublishdate', // Medium
'subject-label',
// 'subhead', #316
'submenu',
// 'subscribe',
'-subscribe-',
'subscriber-drive',
'subscription-',
'_tags',
'tags__item',
'tag_list',
'tag-list',
'tag-module',
'takeaways',
'taxonomy',
// 'table-content',
'table-of-contents',
'tblc',
'tabs-',
// 'teaser', Nature
'terminaltout',
'time-rubric',
'timestamp',
'time-read',
'time-to-read',
'tip_off',
'-ticker',
'tiptout',
'-tout-',
// '-toc',
'toc-container',
'toggle-caption',
// 'toolbar', prism.js
'tooltip-content',
'topbar',
'subnavbar',
'topic-authors',
'topic-footer',
'topic-list',
'topic-subnav',
// 'top-section',
'top-wrapper',
'tree-item',
'trending',
'trust-feat',
'trust-badge',
'trust-project',
'chakra-badge',
'twiblock',
'u-hide',
'upsell',
'vid_carousel',
'viewbottom',
'view-language',
'yarpp-related',
'visually-hidden',
'welcomebox',
'widget_pages',
'window__widget',
// 'widget-'
// Webflow form state messages — shown after form submit, never article content
'w-form-done',
'w-form-fail',
];
// Pre-compiled combined regex for PARTIAL_SELECTORS — avoids rebuilding on every parse
export const PARTIAL_SELECTORS_REGEX = new RegExp(PARTIAL_SELECTORS.join('|'), 'i');
// Anchored variant: the whole string must equal a selector token. Used only for
// delimiter-less ids, which are usually content anchors a publishing system
// concatenated from heading words (e.g. id "TheRoleOfThings" -> "theroleofthings").
// Substring matching those wrongly strips real sections — "theroleofthings"
// contains "herol" (matches 'hero[_\\-a-z]') and "loopsandfeedback" contains
// 'feedback'. Delimited ids keep substring matching.
export const PARTIAL_SELECTORS_ANCHORED_REGEX = new RegExp('^(?:' + PARTIAL_SELECTORS.join('|') + ')$', 'i');
// Attribute selector for elements we test partial matches against
export const TEST_ATTRIBUTES_SELECTOR = TEST_ATTRIBUTES.map(attr => `[${attr}]`).join(',');
// Selectors for footnotes and citations
export const FOOTNOTE_INLINE_REFERENCES = [
'sup.reference',
'cite.ltx_cite',
'sup[id^="fnr"]',
'span[id^="fnr"]',
'span[class*="footnote_ref"]',
'span[class*="footnote-ref"]',
'span.footnote-link',
'a.citation',
'a[id^="ref-link"]',
'a[href^="#fn"]',
'a[href^="#cite"]',
'a[href^="#reference"]',
'a[href^="#footnote"]',
'a[href^="#r"]', // Common in academic papers
'a[href^="#b"]', // Common for bibliography references
'a[href*="cite_note"]',
'a[href*="cite_ref"]',
'a.footnote-anchor', // Substack
'span.footnote-hovercard-target a', // Substack
'a[role="doc-biblioref"]', // Science.org
'a[id^="fnref"]',
'a[id^="ref-link"]', // Nature.com
'sup.footnoteref', // Wikidot
'sup.footnote-reference', // pulldown-cmark / mdBook / zola
'sup[data-fn] > a[href^="#"]', // WordPress block editor footnotes
'sup[id^="ftnt_ref"] a[href^="#ftnt"]', // Google Docs/Sites
'span.easy-footnote > a[href^="#easy-footnote-bottom-"]', // Easy Footnotes WP plugin
'a.footnote[href^="#"]', // GNU Texinfo / makeinfo inline markers
'a[data-type="noteref"]', // O'Reilly / HTMLBook
].join(',');
export const FOOTNOTE_LIST_SELECTORS = [
'div.footnote ol',
'div.footnotes ol',
'div[role="doc-endnotes"]',
'div[role="doc-footnotes"]',
'ol.footnotes-list',
'ol.footnotes',
'ol.references',
'ol[class*="article-references"]',
'section.footnotes ol',
'section[role="doc-endnotes"]',
'section[role="doc-footnotes"]',
'section[role="doc-bibliography"]',
'ul.footnotes-list',
'ul.ltx_biblist',
'div.footnote[data-component-name="FootnoteToDOM"]', // Substack
'div.footnotes-footer', // Wikidot
'div.footnote-definitions',
'div.footnote-definition', // pulldown-cmark / mdBook / zola (unwrapped)
'ol.wp-block-footnotes', // WordPress block editor footnotes
'ol.easy-footnotes-wrapper', // Easy Footnotes WP plugin
'div.footnotes-segment', // GNU Texinfo / makeinfo
'#footnotes' // standardizeFootnotes output container
].join(',');
// Elements that are allowed to be empty
// These are not removed even if they have no content
export const ALLOWED_EMPTY_ELEMENTS = new Set([
'area',
'audio',
'base',
'br',
'circle',
'col',
'defs',
'ellipse',
'embed',
'figure',
'g',
'hr',
'iframe',
'img',
'input',
'line',
'link',
'mask',
'meta',
'object',
'param',
'path',
'pattern',
'picture',
'polygon',
'polyline',
'rect',
'source',
'stop',
'svg',
'td',
'th',
'track',
'use',
'video',
'wbr'
]);
// Attributes to keep
export const ALLOWED_ATTRIBUTES = new Set([
'alt',
'allow',
'allowfullscreen',
'aria-label',
'checked',
'colspan',
'controls',
'data-latex',
'data-src',
'data-srcset',
'data-callout',
'data-callout-fold',
'data-callout-title',
'data-lang',
'dir',
'display',
'frameborder',
'headers',
'height',
'href',
'kind',
'label',
'lang',
'role',
'rowspan',
'sandbox',
'src',
'srclang',
'srcset',
'start',
'title',
'type',
'width',
// MathML attributes
'accent',
'accentunder',
'align',
'columnalign',
'columnlines',
'columnspacing',
'columnspan',
'data-mjx-texclass',
'depth',
'displaystyle',
'fence',
'frame',
'framespacing',
'linethickness',
'lspace',
'mathsize',
'mathvariant',
'maxsize',
'minsize',
'movablelimits',
'notation',
'rowalign',
'rowlines',
'rowspacing',
'rowspan',
'rspace',
'scriptlevel',
'separator',
'stretchy',
'symmetric',
'voffset',
'xmlns'
]);
export const ALLOWED_ATTRIBUTES_DEBUG = new Set([
'class',
'id',
]);
// Tailwind default color palette
export const TAILWIND_COLORS: Record<string, Record<string, string>> = {
slate: { '50':'#f8fafc','100':'#f1f5f9','200':'#e2e8f0','300':'#cbd5e1','400':'#94a3b8','500':'#64748b','600':'#475569','700':'#334155','800':'#1e293b','900':'#0f172a','950':'#020617' },
gray: { '50':'#f9fafb','100':'#f3f4f6','200':'#e5e7eb','300':'#d1d5db','400':'#9ca3af','500':'#6b7280','600':'#4b5563','700':'#374151','800':'#1f2937','900':'#111827','950':'#030712' },
zinc: { '50':'#fafafa','100':'#f4f4f5','200':'#e4e4e7','300':'#d4d4d8','400':'#a1a1aa','500':'#71717a','600':'#52525b','700':'#3f3f46','800':'#27272a','900':'#18181b','950':'#09090b' },
neutral: { '50':'#fafafa','100':'#f5f5f5','200':'#e5e5e5','300':'#d4d4d4','400':'#a3a3a3','500':'#737373','600':'#525252','700':'#404040','800':'#262626','900':'#171717','950':'#0a0a0a' },
stone: { '50':'#fafaf9','100':'#f5f5f4','200':'#e7e5e4','300':'#d6d3d1','400':'#a8a29e','500':'#78716c','600':'#57534e','700':'#44403c','800':'#292524','900':'#1c1917','950':'#0c0a09' },
red: { '50':'#fef2f2','100':'#fee2e2','200':'#fecaca','300':'#fca5a5','400':'#f87171','500':'#ef4444','600':'#dc2626','700':'#b91c1c','800':'#991b1b','900':'#7f1d1d','950':'#450a0a' },
orange: { '50':'#fff7ed','100':'#ffedd5','200':'#fed7aa','300':'#fdba74','400':'#fb923c','500':'#f97316','600':'#ea580c','700':'#c2410c','800':'#9a3412','900':'#7c2d12','950':'#431407' },
amber: { '50':'#fffbeb','100':'#fef3c7','200':'#fde68a','300':'#fcd34d','400':'#fbbf24','500':'#f59e0b','600':'#d97706','700':'#b45309','800':'#92400e','900':'#78350f','950':'#451a03' },
yellow: { '50':'#fefce8','100':'#fef9c3','200':'#fef08a','300':'#fde047','400':'#facc15','500':'#eab308','600':'#ca8a04','700':'#a16207','800':'#854d0e','900':'#713f12','950':'#422006' },
lime: { '50':'#f7fee7','100':'#ecfccb','200':'#d9f99d','300':'#bef264','400':'#a3e635','500':'#84cc16','600':'#65a30d','700':'#4d7c0f','800':'#3f6212','900':'#365314','950':'#1a2e05' },
green: { '50':'#f0fdf4','100':'#dcfce7','200':'#bbf7d0','300':'#86efac','400':'#4ade80','500':'#22c55e','600':'#16a34a','700':'#15803d','800':'#166534','900':'#14532d','950':'#052e16' },
emerald: { '50':'#ecfdf5','100':'#d1fae5','200':'#a7f3d0','300':'#6ee7b7','400':'#34d399','500':'#10b981','600':'#059669','700':'#047857','800':'#065f46','900':'#064e3b','950':'#022c22' },
teal: { '50':'#f0fdfa','100':'#ccfbf1','200':'#99f6e4','300':'#5eead4','400':'#2dd4bf','500':'#14b8a6','600':'#0d9488','700':'#0f766e','800':'#115e59','900':'#134e4a','950':'#042f2e' },
cyan: { '50':'#ecfeff','100':'#cffafe','200':'#a5f3fc','300':'#67e8f9','400':'#22d3ee','500':'#06b6d4','600':'#0891b2','700':'#0e7490','800':'#155e75','900':'#164e63','950':'#083344' },
sky: { '50':'#f0f9ff','100':'#e0f2fe','200':'#bae6fd','300':'#7dd3fc','400':'#38bdf8','500':'#0ea5e9','600':'#0284c7','700':'#0369a1','800':'#075985','900':'#0c4a6e','950':'#082f49' },
blue: { '50':'#eff6ff','100':'#dbeafe','200':'#bfdbfe','300':'#93c5fd','400':'#60a5fa','500':'#3b82f6','600':'#2563eb','700':'#1d4ed8','800':'#1e40af','900':'#1e3a8a','950':'#172554' },
indigo: { '50':'#eef2ff','100':'#e0e7ff','200':'#c7d2fe','300':'#a5b4fc','400':'#818cf8','500':'#6366f1','600':'#4f46e5','700':'#4338ca','800':'#3730a3','900':'#312e81','950':'#1e1b4b' },
violet: { '50':'#f5f3ff','100':'#ede9fe','200':'#ddd6fe','300':'#c4b5fd','400':'#a78bfa','500':'#8b5cf6','600':'#7c3aed','700':'#6d28d9','800':'#5b21b6','900':'#4c1d95','950':'#2e1065' },
purple: { '50':'#faf5ff','100':'#f3e8ff','200':'#e9d5ff','300':'#d8b4fe','400':'#c084fc','500':'#a855f7','600':'#9333ea','700':'#7e22ce','800':'#6b21a8','900':'#581c87','950':'#3b0764' },
fuchsia: { '50':'#fdf4ff','100':'#fae8ff','200':'#f5d0fe','300':'#f0abfc','400':'#e879f9','500':'#d946ef','600':'#c026d3','700':'#a21caf','800':'#86198f','900':'#701a75','950':'#4a044e' },
pink: { '50':'#fdf2f8','100':'#fce7f3','200':'#fbcfe8','300':'#f9a8d4','400':'#f472b6','500':'#ec4899','600':'#db2777','700':'#be185d','800':'#9d174d','900':'#831843','950':'#500724' },
rose: { '50':'#fff1f2','100':'#ffe4e6','200':'#fecdd3','300':'#fda4af','400':'#fb7185','500':'#f43f5e','600':'#e11d48','700':'#be123c','800':'#9f1239','900':'#881337','950':'#4c0519' },
};
export const TAILWIND_SPECIAL: Record<string, string> = {
black: '#000', white: '#fff', transparent: 'transparent', current: 'currentColor',
};
export const TW_COLOR_CLASS_RE = /^(fill|stroke)-([a-z]+)-(\d{2,3})(?:\/(\d+))?$/;
export const TW_SPECIAL_CLASS_RE = /^(fill|stroke)-(black|white|transparent|current)$/;
export const TW_ARBITRARY_RE = /^text-\[(.+)\]$/;
SHA-256: 247761db31fa76ad0604aaabf71fe5303e4e6e300cceedf41cc269258fc47ed4