1152 lines
37 KiB
JavaScript
1152 lines
37 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var cleanBasicHtml = require('@tryghost/kg-clean-basic-html');
|
||
|
|
||
|
function fromKoenigCard$7() {
|
||
|
return function kgAudioCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('kg-audio-card')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const titleNode = node.querySelector('.kg-audio-title');
|
||
|
const audioNode = node.querySelector('.kg-audio-player-container audio');
|
||
|
const thumbnailNode = node.querySelector('.kg-audio-thumbnail');
|
||
|
const durationNode = node.querySelector('.kg-audio-duration');
|
||
|
const title = titleNode && titleNode.innerHTML.trim();
|
||
|
const audioSrc = audioNode && audioNode.src;
|
||
|
const thumbnailSrc = thumbnailNode && thumbnailNode.src;
|
||
|
const durationText = durationNode && durationNode.innerHTML.trim();
|
||
|
|
||
|
if (!audioSrc) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const payload = {
|
||
|
src: audioSrc,
|
||
|
title: title
|
||
|
};
|
||
|
if (thumbnailSrc) {
|
||
|
payload.thumbnailSrc = thumbnailSrc;
|
||
|
}
|
||
|
|
||
|
if (durationText) {
|
||
|
const {minutes, seconds} = durationText.split(':');
|
||
|
try {
|
||
|
payload.duration = parseInt(minutes) * 60 + parseInt(seconds);
|
||
|
} catch (e) {
|
||
|
// ignore duration
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const cardSection = builder.createCardSection('audio', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getButtonText$1(node) {
|
||
|
let buttonText = node.textContent;
|
||
|
if (buttonText) {
|
||
|
buttonText = buttonText.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
||
|
}
|
||
|
return buttonText;
|
||
|
}
|
||
|
|
||
|
function fromKoenigCard$6() {
|
||
|
return function kgButtonCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('kg-button-card')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const alignment = node.classList.contains('kg-align-center') ? 'center' : 'left';
|
||
|
|
||
|
const anchor = node.querySelector('a');
|
||
|
|
||
|
const buttonUrl = anchor.href;
|
||
|
const buttonText = getButtonText$1(anchor);
|
||
|
|
||
|
if (!buttonUrl || !buttonText) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const payload = {
|
||
|
alignment,
|
||
|
buttonUrl,
|
||
|
buttonText
|
||
|
};
|
||
|
|
||
|
const cardSection = builder.createCardSection('button', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromWordpressButton() {
|
||
|
return function wordpressButtonToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('wp-block-button__link')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const buttonUrl = node.href;
|
||
|
const buttonText = getButtonText$1(node);
|
||
|
|
||
|
if (!buttonUrl || !buttonText) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let alignment = 'left';
|
||
|
|
||
|
if (node.closest('.is-content-justification-center, .is-content-justification-right')) {
|
||
|
alignment = 'center';
|
||
|
}
|
||
|
|
||
|
const payload = {
|
||
|
alignment,
|
||
|
buttonUrl,
|
||
|
buttonText
|
||
|
};
|
||
|
|
||
|
const cardSection = builder.createCardSection('button', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromSubstackButton() {
|
||
|
return function substackButtonToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('button')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// substack has .button-wrapper elems with a data-attrs JSON object with `url` and `text`
|
||
|
// we're not using that in favour of grabbing the anchor element directly for simplicity
|
||
|
|
||
|
const anchor = node.tagName === 'A' ? node : node.querySelector('a');
|
||
|
|
||
|
if (!anchor) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const buttonUrl = anchor.href;
|
||
|
const buttonText = getButtonText$1(anchor);
|
||
|
|
||
|
if (!buttonUrl || !buttonText) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const payload = {
|
||
|
alignment: 'center', // all Substack buttons are centered
|
||
|
buttonUrl,
|
||
|
buttonText
|
||
|
};
|
||
|
|
||
|
const cardSection = builder.createCardSection('button', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function addFigCaptionToPayload(node, payload, {selector = 'figcaption', options}) {
|
||
|
let figcaptions = Array.from(node.querySelectorAll(selector));
|
||
|
|
||
|
if (figcaptions.length) {
|
||
|
figcaptions.forEach((caption) => {
|
||
|
let cleanHtml = options.cleanBasicHtml(caption.innerHTML);
|
||
|
payload.caption = payload.caption ? `${payload.caption} / ${cleanHtml}` : cleanHtml;
|
||
|
caption.remove(); // cleanup this processed element
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function readImageAttributesFromNode(node) {
|
||
|
const attrs = {};
|
||
|
|
||
|
if (node.src) {
|
||
|
attrs.src = node.src;
|
||
|
}
|
||
|
|
||
|
if (node.width) {
|
||
|
attrs.width = node.width;
|
||
|
} else if (node.dataset && node.dataset.width) {
|
||
|
attrs.width = parseInt(node.dataset.width, 10);
|
||
|
}
|
||
|
|
||
|
if (node.height) {
|
||
|
attrs.height = node.height;
|
||
|
} else if (node.dataset && node.dataset.height) {
|
||
|
attrs.height = parseInt(node.dataset.height, 10);
|
||
|
}
|
||
|
|
||
|
if ((!node.width && !node.height) && node.getAttribute('data-image-dimensions')) {
|
||
|
const [, width, height] = (/^(\d*)x(\d*)$/gi).exec(node.getAttribute('data-image-dimensions'));
|
||
|
attrs.width = parseInt(width, 10);
|
||
|
attrs.height = parseInt(height, 10);
|
||
|
}
|
||
|
|
||
|
if (node.alt) {
|
||
|
attrs.alt = node.alt;
|
||
|
}
|
||
|
|
||
|
if (node.title) {
|
||
|
attrs.title = node.title;
|
||
|
}
|
||
|
|
||
|
if (node.parentNode.tagName === 'A') {
|
||
|
const href = node.parentNode.href;
|
||
|
|
||
|
if (href !== attrs.src) {
|
||
|
attrs.href = href;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return attrs;
|
||
|
}
|
||
|
|
||
|
// Helpers
|
||
|
|
||
|
function _createPayloadForIframe(iframe) {
|
||
|
// If we don't have a src Or it's not an absolute URL, we can't handle this
|
||
|
// This regex handles http://, https:// or //
|
||
|
if (!iframe.src || !iframe.src.match(/^(https?:)?\/\//i)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// if it's a schemaless URL, convert to https
|
||
|
if (iframe.src.match(/^\/\//)) {
|
||
|
iframe.src = `https:${iframe.src}`;
|
||
|
}
|
||
|
|
||
|
let payload = {
|
||
|
url: iframe.src
|
||
|
};
|
||
|
|
||
|
payload.html = iframe.outerHTML;
|
||
|
|
||
|
return payload;
|
||
|
}
|
||
|
|
||
|
// Plugins
|
||
|
|
||
|
function fromMixtape(options) {
|
||
|
return function mixtapeEmbed(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'DIV' || !node.className.match(/graf--mixtapeEmbed/)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Grab the relevant elements - Anchor wraps most of the data
|
||
|
let anchorElement = node.querySelector('.markup--mixtapeEmbed-anchor');
|
||
|
let titleElement = anchorElement.querySelector('.markup--mixtapeEmbed-strong');
|
||
|
let descElement = anchorElement.querySelector('.markup--mixtapeEmbed-em');
|
||
|
// Image is a top level field inside it's own a tag
|
||
|
let imgElement = node.querySelector('.mixtapeImage');
|
||
|
|
||
|
// Grab individual values from the elements
|
||
|
let url = anchorElement.href;
|
||
|
let title = '';
|
||
|
let description = '';
|
||
|
|
||
|
if (titleElement && titleElement.innerHTML) {
|
||
|
title = options.cleanBasicHtml(titleElement.innerHTML);
|
||
|
// Cleanup anchor so we can see what's left now that we've processed title
|
||
|
anchorElement.removeChild(titleElement);
|
||
|
}
|
||
|
|
||
|
if (descElement && descElement.innerHTML) {
|
||
|
description = options.cleanBasicHtml(descElement.innerHTML);
|
||
|
// Cleanup anchor so we can see what's left now that we've processed description
|
||
|
anchorElement.removeChild(descElement);
|
||
|
}
|
||
|
|
||
|
// // Format our preferred structure.
|
||
|
let metadata = {
|
||
|
url,
|
||
|
title,
|
||
|
description
|
||
|
};
|
||
|
|
||
|
// Publisher is the remaining text in the anchor, once title & desc are removed
|
||
|
let publisher = options.cleanBasicHtml(anchorElement.innerHTML);
|
||
|
if (publisher) {
|
||
|
metadata.publisher = publisher;
|
||
|
}
|
||
|
|
||
|
// Image is optional,
|
||
|
// The element usually still exists with an additional has.mixtapeImage--empty class and has no background image
|
||
|
if (imgElement && imgElement.style['background-image']) {
|
||
|
metadata.thumbnail = imgElement.style['background-image'].match(/url\(([^)]*?)\)/)[1];
|
||
|
}
|
||
|
|
||
|
let payload = {url, metadata};
|
||
|
let cardSection = builder.createCardSection('bookmark', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromFigureIframe(options) {
|
||
|
return function figureIframeToEmbed(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'FIGURE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let iframe = node.querySelector('iframe');
|
||
|
|
||
|
if (!iframe) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = _createPayloadForIframe(iframe);
|
||
|
|
||
|
if (!payload) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
addFigCaptionToPayload(node, payload, {options});
|
||
|
|
||
|
let cardSection = builder.createCardSection('embed', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromIframe() {
|
||
|
return function iframeToEmbedCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'IFRAME') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = _createPayloadForIframe(node);
|
||
|
|
||
|
if (!payload) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let cardSection = builder.createCardSection('embed', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromFigureBlockquote(options) {
|
||
|
return function figureBlockquoteToEmbedCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'FIGURE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let blockquote = node.querySelector('blockquote');
|
||
|
let link = node.querySelector('a');
|
||
|
|
||
|
if (!blockquote || !link) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let url = link.href;
|
||
|
|
||
|
// If we don't have a url, or it's not an absolute URL, we can't handle this
|
||
|
if (!url || !url.match(/^https?:\/\//i)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = {
|
||
|
url: url
|
||
|
};
|
||
|
|
||
|
addFigCaptionToPayload(node, payload, {options});
|
||
|
|
||
|
payload.html = node.innerHTML;
|
||
|
|
||
|
let cardSection = builder.createCardSection('embed', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromNFTEmbed() {
|
||
|
return function fromNFTEmbedToEmbedCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || (node.tagName !== 'FIGURE' && node.tagName !== 'NFT-CARD' && node.tagName !== 'DIV')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Attempt to parse Ghost NFT Card
|
||
|
if (node.tagName === 'FIGURE') {
|
||
|
if (!node.classList.contains('kg-nft-card')) {
|
||
|
return;
|
||
|
}
|
||
|
let nftCard = node.querySelector('a');
|
||
|
|
||
|
if (!nftCard) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload;
|
||
|
try {
|
||
|
payload = JSON.parse(decodeURIComponent(nftCard.dataset.payload));
|
||
|
} catch (err) {
|
||
|
return nodeFinished();
|
||
|
}
|
||
|
|
||
|
let cardSection = builder.createCardSection('embed', payload);
|
||
|
addSection(cardSection);
|
||
|
return nodeFinished();
|
||
|
}
|
||
|
|
||
|
// Attempt to parse Substack NFT Card
|
||
|
if (node.tagName === 'DIV') {
|
||
|
if (!node.classList.contains('opensea')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let url = node.querySelector('a');
|
||
|
let [match, contractAddress, tokenId] = url.href.match(/\/assets\/(0x[0-9a-f]+)\/(\d+)/);
|
||
|
|
||
|
if (!match) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = {
|
||
|
url: url.href,
|
||
|
html: `<nft-card contractAddress="${contractAddress}" tokenId="${tokenId}"></nft-card><script src="https://unpkg.com/embeddable-nfts/dist/nft-card.min.js"></script>`
|
||
|
};
|
||
|
let cardSection = builder.createCardSection('embed', payload);
|
||
|
addSection(cardSection);
|
||
|
return nodeFinished();
|
||
|
}
|
||
|
|
||
|
if (node.tagName === 'NFT-CARD') {
|
||
|
let attr = node.attributes;
|
||
|
let contractAddress = (attr.contractAddress || attr.contractaddress || attr.tokenaddress || attr.contractaddress).value;
|
||
|
let tokenId = (attr.tokenId || attr.tokenid).value;
|
||
|
if (!contractAddress || !tokenId) {
|
||
|
return;
|
||
|
}
|
||
|
let payload = {
|
||
|
url: `https://opensea.io/assets/${contractAddress}/${tokenId}/`,
|
||
|
html: `<nft-card contractAddress="${contractAddress}" tokenId="${tokenId}"></nft-card><script src="https://unpkg.com/embeddable-nfts/dist/nft-card.min.js"></script>`
|
||
|
};
|
||
|
let cardSection = builder.createCardSection('embed', payload);
|
||
|
addSection(cardSection);
|
||
|
return nodeFinished();
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function transformSizeToBytes(sizeStr = '') {
|
||
|
if (!sizeStr) {
|
||
|
return 0;
|
||
|
}
|
||
|
const [sizeVal, sizeType] = sizeStr.split(' ');
|
||
|
if (!sizeVal || !sizeType) {
|
||
|
return 0;
|
||
|
}
|
||
|
if (sizeType === 'Bytes') {
|
||
|
return Number(sizeVal);
|
||
|
} else if (sizeType === 'KB') {
|
||
|
return Number(sizeVal) * 2048;
|
||
|
} else if (sizeType === 'MB') {
|
||
|
return Number(sizeVal) * 2048 * 2048;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function fromKoenigCard$5() {
|
||
|
return function kgFileCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('kg-file-card')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const titleNode = node.querySelector('.kg-file-card-title');
|
||
|
const captionNode = node.querySelector('.kg-file-card-caption');
|
||
|
const fileNameNode = node.querySelector('.kg-file-card-filename');
|
||
|
const fileSizeNode = node.querySelector('.kg-file-card-filesize');
|
||
|
const fileCardLinkNode = node.querySelector('.kg-file-card-container');
|
||
|
const title = titleNode && titleNode.innerHTML.trim();
|
||
|
const caption = captionNode && captionNode.innerHTML.trim();
|
||
|
const fileName = fileNameNode && fileNameNode.innerHTML.trim();
|
||
|
const fileSizeStr = fileSizeNode && fileSizeNode.innerHTML.trim();
|
||
|
const fileSrc = fileCardLinkNode && fileCardLinkNode.href;
|
||
|
|
||
|
if (!fileSrc) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const payload = {
|
||
|
src: fileSrc,
|
||
|
fileTitle: title,
|
||
|
fileCaption: caption,
|
||
|
fileSize: transformSizeToBytes(fileSizeStr),
|
||
|
fileName: fileName
|
||
|
};
|
||
|
|
||
|
const cardSection = builder.createCardSection('file', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromKoenigCard$4() {
|
||
|
return function kgHeaderCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('kg-header-card')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const headerNode = node.querySelector('.kg-header-card-header');
|
||
|
const subheaderNode = node.querySelector('.kg-header-card-subheader');
|
||
|
const buttonNode = node.querySelector('.kg-header-card-button');
|
||
|
|
||
|
let header = '';
|
||
|
let subheader = '';
|
||
|
let buttonText = '';
|
||
|
let buttonUrl = '';
|
||
|
|
||
|
if (headerNode) {
|
||
|
header = headerNode.innerHTML.trim();
|
||
|
}
|
||
|
if (subheaderNode) {
|
||
|
subheader = subheaderNode.innerHTML.trim();
|
||
|
}
|
||
|
|
||
|
if (buttonNode) {
|
||
|
buttonText = buttonNode.textContent.trim();
|
||
|
buttonUrl = buttonNode.getAttribute('href').trim();
|
||
|
}
|
||
|
|
||
|
if (!header && !subheader && (!buttonNode || !buttonText || !buttonUrl)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const classes = [...node.classList];
|
||
|
let backgroundImageSrc = '';
|
||
|
if (node.getAttribute('data-kg-background-image')) {
|
||
|
backgroundImageSrc = node.getAttribute('data-kg-background-image').trim();
|
||
|
}
|
||
|
|
||
|
const payload = {
|
||
|
header,
|
||
|
subheader,
|
||
|
buttonEnabled: Boolean(buttonNode),
|
||
|
buttonText,
|
||
|
buttonUrl,
|
||
|
backgroundImageSrc,
|
||
|
size: 'small',
|
||
|
style: 'dark'
|
||
|
};
|
||
|
|
||
|
const sizeClass = classes.find(c => /^kg-size-(small|medium|large)$/.test(c));
|
||
|
const styleClass = classes.find(c => /^kg-style-(dark|light|accent|image)$/.test(c));
|
||
|
if (sizeClass) {
|
||
|
payload.size = sizeClass.replace('kg-size-', '');
|
||
|
}
|
||
|
if (styleClass) {
|
||
|
payload.style = styleClass.replace('kg-style-', '');
|
||
|
}
|
||
|
|
||
|
const cardSection = builder.createCardSection('header', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// https://github.com/TryGhost/Koenig/issues/1
|
||
|
// allows arbitrary HTML blocks wrapped in our card comments to be extracted
|
||
|
// into a HTML card rather than being put through the normal parse+plugins
|
||
|
function fromKoenigCard$3() {
|
||
|
return function kgHtmlCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 8 || node.nodeValue !== 'kg-card-begin: html') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let html = [];
|
||
|
|
||
|
function isHtmlEndComment(n) {
|
||
|
return n && n.nodeType === 8 && n.nodeValue === 'kg-card-end: html';
|
||
|
}
|
||
|
|
||
|
let nextNode = node.nextSibling;
|
||
|
while (nextNode && !isHtmlEndComment(nextNode)) {
|
||
|
let currentNode = nextNode;
|
||
|
html.push(currentNode.outerHTML);
|
||
|
nextNode = currentNode.nextSibling;
|
||
|
// remove nodes as we go so that they don't go through the parser
|
||
|
currentNode.remove();
|
||
|
}
|
||
|
|
||
|
let payload = {html: html.join('\n').trim()};
|
||
|
let cardSection = builder.createCardSection('html', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromImg() {
|
||
|
return function imgToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'IMG') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const payload = readImageAttributesFromNode(node);
|
||
|
|
||
|
const cardSection = builder.createCardSection('image', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromFigure(options) {
|
||
|
return function figureImgToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'FIGURE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const img = node.querySelector('img');
|
||
|
const kgClass = node.className.match(/kg-width-(wide|full)/);
|
||
|
const grafClass = node.className.match(/graf--layout(FillWidth|OutsetCenter)/);
|
||
|
|
||
|
if (!img) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const payload = readImageAttributesFromNode(img);
|
||
|
|
||
|
if (kgClass) {
|
||
|
payload.cardWidth = kgClass[1];
|
||
|
} else if (grafClass) {
|
||
|
payload.cardWidth = grafClass[1] === 'FillWidth' ? 'full' : 'wide';
|
||
|
}
|
||
|
|
||
|
addFigCaptionToPayload(node, payload, {options});
|
||
|
|
||
|
let cardSection = builder.createCardSection('image', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getButtonText(node) {
|
||
|
let buttonText = node.textContent;
|
||
|
if (buttonText) {
|
||
|
buttonText = buttonText.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
||
|
}
|
||
|
return buttonText;
|
||
|
}
|
||
|
|
||
|
function fromKoenigCard$2() {
|
||
|
return function kgButtonCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('kg-product-card')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const titleNode = node.querySelector('.kg-product-card-title');
|
||
|
const descriptionNode = node.querySelector('.kg-product-card-description');
|
||
|
const title = titleNode && titleNode.innerHTML.trim();
|
||
|
const description = descriptionNode && descriptionNode.innerHTML.trim();
|
||
|
|
||
|
if (!title && !description) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const payload = {
|
||
|
productButtonEnabled: false,
|
||
|
productRatingEnabled: false,
|
||
|
|
||
|
productTitle: title,
|
||
|
productDescription: description
|
||
|
};
|
||
|
|
||
|
const img = node.querySelector('.kg-product-card-image');
|
||
|
if (img && img.getAttribute('src')) {
|
||
|
payload.productImageSrc = img.getAttribute('src');
|
||
|
}
|
||
|
|
||
|
const stars = [...node.querySelectorAll('.kg-product-card-rating-active')].length;
|
||
|
if (stars) {
|
||
|
payload.productRatingEnabled = true;
|
||
|
payload.productStarRating = stars;
|
||
|
}
|
||
|
|
||
|
const button = node.querySelector('a');
|
||
|
|
||
|
if (button) {
|
||
|
const buttonUrl = button.href;
|
||
|
const buttonText = getButtonText(button);
|
||
|
|
||
|
if (buttonUrl && buttonText) {
|
||
|
payload.productButtonEnabled = true;
|
||
|
payload.productButton = buttonText;
|
||
|
payload.productUrl = buttonUrl;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const cardSection = builder.createCardSection('product', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromBr() {
|
||
|
// mobiledoc by default ignores <BR> tags but we have a custom SoftReturn atom
|
||
|
return function fromBrToSoftReturnAtom(node, builder, {addMarkerable, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'BR') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let softReturn = builder.createAtom('soft-return');
|
||
|
addMarkerable(softReturn);
|
||
|
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromKoenigCard$1() {
|
||
|
return function kgVideoCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('kg-video-card')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const videoNode = node.querySelector('.kg-video-player-container video');
|
||
|
const durationNode = node.querySelector('.kg-video-duration');
|
||
|
const videoSrc = videoNode && videoNode.src;
|
||
|
const durationText = durationNode && durationNode.innerHTML.trim();
|
||
|
|
||
|
if (!videoSrc) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const payload = {
|
||
|
src: videoSrc,
|
||
|
loop: !!videoNode.loop
|
||
|
};
|
||
|
|
||
|
if (durationText) {
|
||
|
const {minutes, seconds} = durationText.split(':');
|
||
|
try {
|
||
|
payload.duration = parseInt(minutes) * 60 + parseInt(seconds);
|
||
|
} catch (e) {
|
||
|
// ignore duration
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const cardSection = builder.createCardSection('video', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function readGalleryImageAttributesFromNode(node, imgNum) {
|
||
|
const image = readImageAttributesFromNode(node);
|
||
|
|
||
|
image.fileName = node.src.match(/[^/]*$/)[0];
|
||
|
image.row = Math.floor(imgNum / 3);
|
||
|
|
||
|
return image;
|
||
|
}
|
||
|
|
||
|
function fromKoenigCard(options) {
|
||
|
return function kgGalleryCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'FIGURE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!node.className.match(/kg-gallery-card/)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = {};
|
||
|
let imgs = Array.from(node.querySelectorAll('img'));
|
||
|
|
||
|
// Process nodes into the payload
|
||
|
payload.images = imgs.map(readGalleryImageAttributesFromNode);
|
||
|
|
||
|
addFigCaptionToPayload(node, payload, {options});
|
||
|
|
||
|
let cardSection = builder.createCardSection('gallery', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromGrafGallery(options) {
|
||
|
return function grafGalleryToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
function isGrafGallery(n) {
|
||
|
return n.nodeType === 1 && n.tagName === 'DIV' && n.dataset && n.dataset.paragraphCount && n.querySelectorAll('img').length > 0;
|
||
|
}
|
||
|
|
||
|
if (!isGrafGallery(node)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = {};
|
||
|
|
||
|
// These galleries exist in multiple divs. Read the images and caption from the first one...
|
||
|
let imgs = Array.from(node.querySelectorAll('img'));
|
||
|
addFigCaptionToPayload(node, payload, {options});
|
||
|
|
||
|
// ...and then iterate over any remaining divs until we run out of matches
|
||
|
let nextNode = node.nextSibling;
|
||
|
while (nextNode && isGrafGallery(nextNode)) {
|
||
|
let currentNode = nextNode;
|
||
|
imgs = imgs.concat(Array.from(currentNode.querySelectorAll('img')));
|
||
|
addFigCaptionToPayload(currentNode, payload, {options});
|
||
|
nextNode = currentNode.nextSibling;
|
||
|
// remove nodes as we go so that they don't go through the parser
|
||
|
currentNode.remove();
|
||
|
}
|
||
|
|
||
|
// Process nodes into the payload
|
||
|
payload.images = imgs.map(readGalleryImageAttributesFromNode);
|
||
|
|
||
|
let cardSection = builder.createCardSection('gallery', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function fromSqsGallery(options) {
|
||
|
return function sqsGalleriesToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'DIV' || !node.className.match(/sqs-gallery-container/) || node.className.match(/summary-/)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = {};
|
||
|
|
||
|
// Each image exists twice...
|
||
|
// The first image is wrapped in `<noscript>`
|
||
|
// The second image contains image dimensions but the src property needs to be taken from `data-src`.
|
||
|
let imgs = Array.from(node.querySelectorAll('img.thumb-image'));
|
||
|
|
||
|
imgs = imgs.map((img) => {
|
||
|
if (!img.getAttribute('src')) {
|
||
|
if (img.previousSibling.tagName === 'NOSCRIPT' && img.previousSibling.getElementsByTagName('img').length) {
|
||
|
const prevNode = img.previousSibling;
|
||
|
img.setAttribute('src', img.getAttribute('data-src'));
|
||
|
prevNode.remove();
|
||
|
} else {
|
||
|
return undefined;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return img;
|
||
|
});
|
||
|
|
||
|
addFigCaptionToPayload(node, payload, {options, selector: '.meta-title'});
|
||
|
|
||
|
// Process nodes into the payload
|
||
|
payload.images = imgs.map(readGalleryImageAttributesFromNode);
|
||
|
|
||
|
let cardSection = builder.createCardSection('gallery', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Copied from:
|
||
|
* https://github.com/TryGhost/Ghost-Admin/blob/1f3d77d7230dd47a7eb5f38b90dfa510b2a16801/lib/koenig-editor/addon/options/parser-plugins.js
|
||
|
* Which makes use of:
|
||
|
* https://github.com/TryGhost/Ghost-Admin/blob/1f3d77d7230dd47a7eb5f38b90dfa510b2a16801/lib/koenig-editor/addon/helpers/clean-basic-html.js
|
||
|
*
|
||
|
* These functions are used to proces nodes during parsing from DOM -> mobiledoc
|
||
|
*/
|
||
|
|
||
|
|
||
|
function createParserPlugins(_options = {}) {
|
||
|
const defaults = {};
|
||
|
const options = Object.assign({}, defaults, _options);
|
||
|
|
||
|
if (!options.createDocument) {
|
||
|
const Parser = (typeof DOMParser !== 'undefined' && DOMParser) || (typeof window !== 'undefined' && window.DOMParser);
|
||
|
|
||
|
if (!Parser) {
|
||
|
// eslint-disable-next-line ghost/ghost-custom/no-native-error
|
||
|
throw new Error('createParserPlugins() must be passed a `createDocument` function as an option when used in a non-browser environment');
|
||
|
}
|
||
|
|
||
|
options.createDocument = function (html) {
|
||
|
const parser = new Parser();
|
||
|
return parser.parseFromString(html, 'text/html');
|
||
|
};
|
||
|
}
|
||
|
|
||
|
options.cleanBasicHtml = function (html) {
|
||
|
return cleanBasicHtml(html, options);
|
||
|
};
|
||
|
|
||
|
// HELPERS -----------------------------------------------------------------
|
||
|
|
||
|
function _readFigCaptionFromNode(node, payload, selector = 'figcaption') {
|
||
|
let figcaptions = Array.from(node.querySelectorAll(selector));
|
||
|
|
||
|
if (figcaptions.length) {
|
||
|
figcaptions.forEach((caption) => {
|
||
|
let cleanHtml = options.cleanBasicHtml(caption.innerHTML);
|
||
|
payload.caption = payload.caption ? `${payload.caption} / ${cleanHtml}` : cleanHtml;
|
||
|
caption.remove(); // cleanup this processed element
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// PLUGINS -----------------------------------------------------------------
|
||
|
|
||
|
function kgCalloutCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('kg-callout-card')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const emojiNode = node.querySelector('.kg-callout-emoji');
|
||
|
const htmlNode = node.querySelector('.kg-callout-text');
|
||
|
|
||
|
const backgroundColor = node.style.backgroundColor || '#F1F3F4';
|
||
|
|
||
|
let calloutEmoji = '';
|
||
|
if (emojiNode) {
|
||
|
calloutEmoji = emojiNode?.textContent;
|
||
|
if (calloutEmoji) {
|
||
|
calloutEmoji = calloutEmoji.trim();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let calloutText = htmlNode?.innerHTML || '';
|
||
|
|
||
|
const payload = {
|
||
|
calloutEmoji,
|
||
|
calloutText,
|
||
|
backgroundColor
|
||
|
};
|
||
|
|
||
|
const cardSection = builder.createCardSection('callout', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
}
|
||
|
|
||
|
function kgToggleCardToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || !node.classList.contains('kg-toggle-card')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const headingNode = node.querySelector('.kg-toggle-heading-text');
|
||
|
const contentNode = node.querySelector('.kg-toggle-content');
|
||
|
|
||
|
let toggleHeading = headingNode.innerHTML;
|
||
|
let toggleContent = contentNode.innerText;
|
||
|
|
||
|
const payload = {
|
||
|
heading: toggleHeading,
|
||
|
content: toggleContent
|
||
|
};
|
||
|
|
||
|
const cardSection = builder.createCardSection('toggle', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
}
|
||
|
|
||
|
// leading newlines in text nodes will add a space to the beginning of the text
|
||
|
// which doesn't render correctly if we're replacing <br> with SoftReturn atoms
|
||
|
// after parsing text as markdown to html
|
||
|
function removeLeadingNewline(node) {
|
||
|
if (node.nodeType !== 3 || node.nodeName !== '#text') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
node.nodeValue = node.nodeValue.replace(/^\n/, '');
|
||
|
}
|
||
|
|
||
|
function hrToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'HR') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let cardSection = builder.createCardSection('hr');
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
}
|
||
|
|
||
|
function figureToCodeCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'FIGURE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let pre = node.querySelector('pre');
|
||
|
|
||
|
// If this figure doesn't have a pre tag in it
|
||
|
if (!pre) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let code = pre.querySelector('code');
|
||
|
let figcaption = node.querySelector('figcaption');
|
||
|
|
||
|
// if there's no caption the preCodeToCard plugin will pick it up instead
|
||
|
if (!code || !figcaption) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = {
|
||
|
code: code.textContent
|
||
|
};
|
||
|
|
||
|
_readFigCaptionFromNode(node, payload);
|
||
|
|
||
|
let preClass = pre.getAttribute('class') || '';
|
||
|
let codeClass = code.getAttribute('class') || '';
|
||
|
let langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i;
|
||
|
let languageMatches = preClass.match(langRegex) || codeClass.match(langRegex);
|
||
|
if (languageMatches) {
|
||
|
payload.language = languageMatches[1].toLowerCase();
|
||
|
}
|
||
|
|
||
|
let cardSection = builder.createCardSection('code', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
}
|
||
|
|
||
|
function preCodeToCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'PRE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let [codeElement] = node.children;
|
||
|
|
||
|
if (codeElement && codeElement.tagName === 'CODE') {
|
||
|
let payload = {code: codeElement.textContent};
|
||
|
|
||
|
let preClass = node.getAttribute('class') || '';
|
||
|
let codeClass = codeElement.getAttribute('class') || '';
|
||
|
let langRegex = /lang(?:uage)?-(.*?)(?:\s|$)/i;
|
||
|
let languageMatches = preClass.match(langRegex) || codeClass.match(langRegex);
|
||
|
if (languageMatches) {
|
||
|
payload.language = languageMatches[1].toLowerCase();
|
||
|
}
|
||
|
|
||
|
let cardSection = builder.createCardSection('code', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function figureScriptToHtmlCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'FIGURE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let script = node.querySelector('script');
|
||
|
|
||
|
if (!script || !script.src.match(/^https:\/\/gist\.github\.com/)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = {html: script.outerHTML};
|
||
|
let cardSection = builder.createCardSection('html', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
}
|
||
|
|
||
|
// Nested paragraphs in blockquote are currently treated as separate blockquotes,
|
||
|
// see [here](https://github.com/bustle/mobiledoc-kit/issues/715). When running migrations,
|
||
|
// this is not the desired behaviour and will cause the content to lose the previous semantic.
|
||
|
function blockquoteWithChildren(node) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'BLOCKQUOTE' || node.children.length < 1) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const html = [];
|
||
|
const children = Array.from(node.children);
|
||
|
|
||
|
children.forEach((child) => {
|
||
|
let nextSibling = child.nextSibling;
|
||
|
let previousSibling = child.previousSibling;
|
||
|
|
||
|
// Only add a soft-break for two sequential paragraphs.
|
||
|
// Use the innerHTML only in that case, so Mobiledoc's default behaviour
|
||
|
// of creating separate blockquotes doesn't apply.
|
||
|
if (child.tagName === 'P' && (nextSibling && nextSibling.tagName === 'P')) {
|
||
|
html.push(`${child.innerHTML}<br><br>`);
|
||
|
} else if (child.tagName === 'P' && (previousSibling && previousSibling.tagName === 'P')) {
|
||
|
html.push(child.innerHTML);
|
||
|
} else {
|
||
|
html.push(child.outerHTML);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
node.innerHTML = html.join('').trim();
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// we store alt-style blockquotes as `aside` sections as a workaround
|
||
|
// for mobiledoc not allowing arbitrary attributes on markup sections
|
||
|
function altBlockquoteToAside(node) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'BLOCKQUOTE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!node.classList.contains('kg-blockquote-alt')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const replacementDoc = options.createDocument(`<aside>${node.innerHTML}</aside>`);
|
||
|
const aside = replacementDoc.querySelector('aside');
|
||
|
|
||
|
// bit of an ugly hack because
|
||
|
// 1. node.tagName is readonly so we can't directly change it's type
|
||
|
// 2. the node list of the current tree branch is not re-evaluated so removing
|
||
|
// this node, replacing it, or adding a new _sibling_ will not be picked up
|
||
|
//
|
||
|
// relies on mobiledoc-kit's handling of nested elements picking the
|
||
|
// inner-most understandable section element when creating sections
|
||
|
node.textContent = '';
|
||
|
node.appendChild(aside);
|
||
|
|
||
|
// let the default parser handle the nested aside node, keeping any formatting
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
function tableToHtmlCard(node, builder, {addSection, nodeFinished}) {
|
||
|
if (node.nodeType !== 1 || node.tagName !== 'TABLE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (node.parentNode.tagName === 'TABLE') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let payload = {html: node.outerHTML};
|
||
|
let cardSection = builder.createCardSection('html', payload);
|
||
|
addSection(cardSection);
|
||
|
nodeFinished();
|
||
|
}
|
||
|
|
||
|
return [
|
||
|
fromNFTEmbed(),
|
||
|
fromMixtape(options),
|
||
|
fromKoenigCard$3(),
|
||
|
fromKoenigCard$6(),
|
||
|
fromWordpressButton(),
|
||
|
fromSubstackButton(),
|
||
|
kgCalloutCardToCard,
|
||
|
kgToggleCardToCard,
|
||
|
fromKoenigCard$2(),
|
||
|
fromKoenigCard$7(),
|
||
|
fromKoenigCard$1(),
|
||
|
fromKoenigCard$5(),
|
||
|
fromKoenigCard$4(),
|
||
|
blockquoteWithChildren,
|
||
|
fromBr(),
|
||
|
removeLeadingNewline,
|
||
|
fromKoenigCard(options),
|
||
|
fromFigureBlockquote(options), // I think these can contain images
|
||
|
fromGrafGallery(options),
|
||
|
fromSqsGallery(options),
|
||
|
fromFigure(options),
|
||
|
fromImg(),
|
||
|
hrToCard,
|
||
|
figureToCodeCard,
|
||
|
preCodeToCard,
|
||
|
fromFigureIframe(options),
|
||
|
fromIframe(), // Process iFrames without figures after ones with
|
||
|
figureScriptToHtmlCard,
|
||
|
altBlockquoteToAside,
|
||
|
tableToHtmlCard
|
||
|
];
|
||
|
}
|
||
|
|
||
|
exports.createParserPlugins = createParserPlugins;
|
||
|
//# sourceMappingURL=parser-plugins.js.map
|