Added q scraping helper. Added Perfect Gonzo scraper.
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 788 B |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 23 KiB |
|
@ -113,6 +113,12 @@ const networks = [
|
||||||
url: 'https://www.naughtyamerica.com',
|
url: 'https://www.naughtyamerica.com',
|
||||||
description: 'The best porn movies daily at Naughty America! Experience the most seductive porn stars in stunning virtual reality, 4K and HD porn videos!',
|
description: 'The best porn movies daily at Naughty America! Experience the most seductive porn stars in stunning virtual reality, 4K and HD porn videos!',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
slug: 'perfectgonzo',
|
||||||
|
name: 'Perfect Gonzo',
|
||||||
|
url: 'https://www.perfectgonzo.com',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
slug: 'pervcity',
|
slug: 'pervcity',
|
||||||
name: 'Perv City',
|
name: 'Perv City',
|
||||||
|
|
|
@ -2078,6 +2078,67 @@ function getSites(networksMap) {
|
||||||
url: 'https://www.naughtyamerica.com/site/live-naughty-nurse',
|
url: 'https://www.naughtyamerica.com/site/live-naughty-nurse',
|
||||||
network_id: networksMap.naughtyamerica,
|
network_id: networksMap.naughtyamerica,
|
||||||
},
|
},
|
||||||
|
// PERFECT GONZO
|
||||||
|
{
|
||||||
|
slug: 'allinternal',
|
||||||
|
name: 'All Internal',
|
||||||
|
url: 'https://allinternal.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'asstraffic',
|
||||||
|
name: 'Ass Traffic',
|
||||||
|
url: 'https://asstraffic.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'cumforcover',
|
||||||
|
name: 'Cum For Cover',
|
||||||
|
url: 'https://cumforcover.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'fistflush',
|
||||||
|
name: 'Fist Flush',
|
||||||
|
url: 'https://fistflush.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'givemepink',
|
||||||
|
name: 'Give Me Pink',
|
||||||
|
url: 'https://givemepink.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'milfthing',
|
||||||
|
name: 'MILF Thing',
|
||||||
|
url: 'https://milfthing.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'primecups',
|
||||||
|
name: 'Prime Cups',
|
||||||
|
url: 'https://primecups.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'purepov',
|
||||||
|
name: 'Pure POV',
|
||||||
|
url: 'https://purepov.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'spermswap',
|
||||||
|
name: 'Sperm Swap',
|
||||||
|
url: 'https://spermswap.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'tamedteens',
|
||||||
|
name: 'Tamed Teens',
|
||||||
|
url: 'https://tamedteens.com',
|
||||||
|
network_id: networksMap.perfectgonzo,
|
||||||
|
},
|
||||||
// PERVCITY
|
// PERVCITY
|
||||||
{
|
{
|
||||||
slug: 'analoverdose',
|
slug: 'analoverdose',
|
||||||
|
|
|
@ -296,6 +296,11 @@ function getTags(groupsMap) {
|
||||||
slug: 'cum-on-boobs',
|
slug: 'cum-on-boobs',
|
||||||
alias_for: null,
|
alias_for: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'cum swapping',
|
||||||
|
slug: 'cum-swapping',
|
||||||
|
alias_for: null,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'cumshot',
|
name: 'cumshot',
|
||||||
slug: 'cumshot',
|
slug: 'cumshot',
|
||||||
|
@ -756,6 +761,11 @@ function getTags(groupsMap) {
|
||||||
alias_for: null,
|
alias_for: null,
|
||||||
group_id: groupsMap.clothing,
|
group_id: groupsMap.clothing,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'solo',
|
||||||
|
slug: 'solo',
|
||||||
|
alias_for: null,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'spanking',
|
name: 'spanking',
|
||||||
slug: 'spanking',
|
slug: 'spanking',
|
||||||
|
@ -1120,6 +1130,10 @@ function getTagAliases(tagsMap) {
|
||||||
name: 'creampies',
|
name: 'creampies',
|
||||||
alias_for: tagsMap.creampie,
|
alias_for: tagsMap.creampie,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'creampie - anal',
|
||||||
|
alias_for: tagsMap['anal-creampie'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'crop', // a type of whip, not [sic] short for corporal
|
name: 'crop', // a type of whip, not [sic] short for corporal
|
||||||
alias_for: tagsMap['corporal-punishment'],
|
alias_for: tagsMap['corporal-punishment'],
|
||||||
|
@ -1188,6 +1202,10 @@ function getTagAliases(tagsMap) {
|
||||||
name: 'doggystyle - standing',
|
name: 'doggystyle - standing',
|
||||||
alias_for: tagsMap['standing-doggy-style'],
|
alias_for: tagsMap['standing-doggy-style'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'doggystyle regular',
|
||||||
|
alias_for: tagsMap['doggy-style'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'dom',
|
name: 'dom',
|
||||||
alias_for: tagsMap.bdsm,
|
alias_for: tagsMap.bdsm,
|
||||||
|
@ -1536,6 +1554,10 @@ function getTagAliases(tagsMap) {
|
||||||
name: 'teens',
|
name: 'teens',
|
||||||
alias_for: tagsMap.teen,
|
alias_for: tagsMap.teen,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'throat fucking',
|
||||||
|
alias_for: tagsMap.facefucking,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'tiny boobs',
|
name: 'tiny boobs',
|
||||||
alias_for: tagsMap['small-boobs'],
|
alias_for: tagsMap['small-boobs'],
|
||||||
|
@ -1598,12 +1620,14 @@ function getSiteTags() {
|
||||||
dpparodies: ['parody'],
|
dpparodies: ['parody'],
|
||||||
eighteenyearsold: ['teen'],
|
eighteenyearsold: ['teen'],
|
||||||
exotic4k: ['4k'],
|
exotic4k: ['4k'],
|
||||||
|
givemepink: ['solo', 'masturbation'],
|
||||||
lubed: ['oil'],
|
lubed: ['oil'],
|
||||||
familystrokes: ['family'],
|
familystrokes: ['family'],
|
||||||
massagecreep: ['massage'],
|
massagecreep: ['massage'],
|
||||||
menonedge: ['gay'],
|
menonedge: ['gay'],
|
||||||
povd: ['pov'],
|
povd: ['pov'],
|
||||||
puremature: ['milf'],
|
puremature: ['milf'],
|
||||||
|
spermswap: ['cum-swapping'],
|
||||||
spyfam: ['family'],
|
spyfam: ['family'],
|
||||||
submissived: ['bdsm'],
|
submissived: ['bdsm'],
|
||||||
swallowed: ['blowjob', 'deepthroat', 'facefucking'],
|
swallowed: ['blowjob', 'deepthroat', 'facefucking'],
|
||||||
|
|
|
@ -52,8 +52,10 @@ async function scrapeScene(scene, site, tokens) {
|
||||||
entryId: scene.id,
|
entryId: scene.id,
|
||||||
title: scene.title,
|
title: scene.title,
|
||||||
duration: scene.length,
|
duration: scene.length,
|
||||||
tokens, // attach tokens to reduce number of requests required for deep fetching
|
|
||||||
site,
|
site,
|
||||||
|
meta: {
|
||||||
|
tokens, // attach tokens to reduce number of requests required for deep fetching
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
release.url = `${site.url}/scene/${release.entryId}/${slugify(release.title, true)}`;
|
release.url = `${site.url}/scene/${release.entryId}/${slugify(release.title, true)}`;
|
||||||
|
@ -93,7 +95,7 @@ async function fetchLatest(site, page = 1) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchScene(url, site, release) {
|
async function fetchScene(url, site, release) {
|
||||||
const { time, token } = release?.tokens || await fetchToken(site); // use attached tokens when deep fetching
|
const { time, token } = release?.meta.tokens || await fetchToken(site); // use attached tokens when deep fetching
|
||||||
const { pathname } = new URL(url);
|
const { pathname } = new URL(url);
|
||||||
const entryId = pathname.split('/')[2];
|
const entryId = pathname.split('/')[2];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const bhttp = require('bhttp');
|
||||||
|
const blake2 = require('blake2');
|
||||||
|
const knex = require('../knex');
|
||||||
|
|
||||||
|
const { ex, ctxa } = require('../utils/q');
|
||||||
|
|
||||||
|
async function getSiteSlugs() {
|
||||||
|
return knex('sites')
|
||||||
|
.pluck('sites.slug')
|
||||||
|
.join('networks', 'networks.id', 'sites.network_id')
|
||||||
|
.where('networks.slug', 'perfectgonzo');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHash(identifier) {
|
||||||
|
const hash = blake2.createHash('blake2b', { digestLength: 8 });
|
||||||
|
|
||||||
|
hash.update(Buffer.from(identifier));
|
||||||
|
|
||||||
|
return hash.digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMaleModelsFromTags(tagContainer) {
|
||||||
|
if (!tagContainer) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagEls = Array.from(tagContainer.childNodes, node => ({ type: node.nodeType, text: node.textContent.trim() })).filter(node => node.text.length > 0);
|
||||||
|
const modelLabelIndex = tagEls.findIndex(node => node.text === 'Male Models');
|
||||||
|
|
||||||
|
if (modelLabelIndex > -1) {
|
||||||
|
const nextLabelIndex = tagEls.findIndex((node, index) => index > modelLabelIndex && node.type === 3);
|
||||||
|
const maleModels = tagEls.slice(modelLabelIndex + 1, nextLabelIndex);
|
||||||
|
|
||||||
|
return maleModels.map(model => model.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractChannelFromPhoto(photo, metaSiteSlugs) {
|
||||||
|
const siteSlugs = metaSiteSlugs || await getSiteSlugs();
|
||||||
|
const channelMatch = photo.match(new RegExp(siteSlugs.join('|')));
|
||||||
|
|
||||||
|
if (channelMatch) {
|
||||||
|
return channelMatch[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrapeLatest(html, site) {
|
||||||
|
const siteSlugs = await getSiteSlugs();
|
||||||
|
const { element } = ex(html);
|
||||||
|
|
||||||
|
return ctxa(element, '#content-main .itemm').map(({
|
||||||
|
q, qa, qlength, qdate, qimages,
|
||||||
|
}) => {
|
||||||
|
const release = {
|
||||||
|
site,
|
||||||
|
meta: {
|
||||||
|
siteSlugs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sceneLink = q('a');
|
||||||
|
|
||||||
|
release.title = sceneLink.title;
|
||||||
|
release.url = `${site.url}${sceneLink.href}`;
|
||||||
|
release.date = qdate('.nm-date', 'MM/DD/YYYY');
|
||||||
|
|
||||||
|
const slug = new URL(release.url).pathname.split('/')[2];
|
||||||
|
release.entryId = getHash(`${site.slug}${slug}${release.date.toISOString()}`);
|
||||||
|
|
||||||
|
release.actors = release.title.split('&').map(actor => actor.trim());
|
||||||
|
|
||||||
|
[release.poster, ...release.photos] = qimages('.bloc-link img');
|
||||||
|
|
||||||
|
release.tags = qa('.dropdown ul a', true).slice(1);
|
||||||
|
release.duration = qlength('.dropdown p:first-child');
|
||||||
|
|
||||||
|
return release;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrapeScene(html, site, url, metaSiteSlugs) {
|
||||||
|
const {
|
||||||
|
q, qa, qlength, qdate, qposter, qtrailer,
|
||||||
|
} = ex(html);
|
||||||
|
|
||||||
|
const release = { url, site };
|
||||||
|
|
||||||
|
release.title = q('#movie-header h2', true);
|
||||||
|
release.date = qdate('#movie-header div span', 'MMMM DD, YYYY', /\w+ \d{1,2}, \d{4}/);
|
||||||
|
|
||||||
|
release.description = q('.container .mg-md', true);
|
||||||
|
release.duration = qlength('#video-ribbon .container > div > span:nth-child(3)');
|
||||||
|
|
||||||
|
release.actors = qa('#video-info a', true).concat(extractMaleModelsFromTags(q('.tag-container')));
|
||||||
|
release.tags = qa('.tag-container a', true);
|
||||||
|
|
||||||
|
const uhd = q('#video-ribbon .container > div > span:nth-child(2)', true);
|
||||||
|
if (/4K/.test(uhd)) release.tags = release.tags.concat('4k');
|
||||||
|
|
||||||
|
release.photos = qa('.bxslider_pics img').map(el => el.dataset.original || el.src);
|
||||||
|
release.poster = qposter();
|
||||||
|
|
||||||
|
const trailer = qtrailer();
|
||||||
|
if (trailer) release.trailer = { src: trailer };
|
||||||
|
|
||||||
|
if (release.photos.length > 0) release.channel = await extractChannelFromPhoto(release.photos[0], metaSiteSlugs);
|
||||||
|
|
||||||
|
if (release.channel) {
|
||||||
|
const { pathname } = new URL(url);
|
||||||
|
release.url = `https://${release.channel}.com${pathname}`;
|
||||||
|
|
||||||
|
const slug = pathname.split('/')[2];
|
||||||
|
release.entryId = getHash(`${release.channel}${slug}${release.date.toISOString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLatest(site, page = 1) {
|
||||||
|
const url = `${site.url}/movies/page-${page}`;
|
||||||
|
const res = await bhttp.get(url);
|
||||||
|
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
return scrapeLatest(res.body.toString(), site);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchScene(url, site, release) {
|
||||||
|
const res = await bhttp.get(url);
|
||||||
|
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
return scrapeScene(res.body.toString(), site, url, release?.meta.siteSlugs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fetchLatest,
|
||||||
|
fetchScene,
|
||||||
|
};
|
|
@ -3,13 +3,12 @@
|
||||||
const bhttp = require('bhttp');
|
const bhttp = require('bhttp');
|
||||||
const { JSDOM } = require('jsdom');
|
const { JSDOM } = require('jsdom');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const ex = require('../utils/ex');
|
|
||||||
|
|
||||||
function scrapeLatest(html, site) {
|
function scrapeLatest(html, site) {
|
||||||
const s = ex(html);
|
const { document } = new JSDOM(html).window;
|
||||||
const { origin } = new URL(site.url);
|
const { origin } = new URL(site.url);
|
||||||
|
|
||||||
const videos = s.qa('.video-releases-list').slice(-1)[0];
|
const videos = document.querySelectorAll('.video-releases-list').slice(-1)[0];
|
||||||
|
|
||||||
return Array.from(videos.querySelectorAll('.card'), (scene) => {
|
return Array.from(videos.querySelectorAll('.card'), (scene) => {
|
||||||
const release = { site };
|
const release = { site };
|
||||||
|
|
|
@ -14,6 +14,7 @@ const jayrock = require('./jayrock');
|
||||||
const kink = require('./kink');
|
const kink = require('./kink');
|
||||||
const mikeadriano = require('./mikeadriano');
|
const mikeadriano = require('./mikeadriano');
|
||||||
const mofos = require('./mofos');
|
const mofos = require('./mofos');
|
||||||
|
const perfectgonzo = require('./perfectgonzo');
|
||||||
const pervcity = require('./pervcity');
|
const pervcity = require('./pervcity');
|
||||||
const pornpros = require('./pornpros');
|
const pornpros = require('./pornpros');
|
||||||
const privateNetwork = require('./private'); // reserved keyword
|
const privateNetwork = require('./private'); // reserved keyword
|
||||||
|
@ -56,6 +57,7 @@ module.exports = {
|
||||||
legalporno,
|
legalporno,
|
||||||
mikeadriano,
|
mikeadriano,
|
||||||
mofos,
|
mofos,
|
||||||
|
perfectgonzo,
|
||||||
pervcity,
|
pervcity,
|
||||||
pornpros,
|
pornpros,
|
||||||
private: privateNetwork,
|
private: privateNetwork,
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const { JSDOM } = require('jsdom');
|
|
||||||
|
|
||||||
function q(context, selector) {
|
|
||||||
return context.querySelector(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
function qa(context, selector) {
|
|
||||||
return Array.from(context.querySelectorAll(selector));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ex(html) {
|
|
||||||
const { document } = new JSDOM(html).window;
|
|
||||||
|
|
||||||
return {
|
|
||||||
document,
|
|
||||||
q: selector => q(document, selector),
|
|
||||||
qa: selector => qa(document, selector),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ex;
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { JSDOM } = require('jsdom');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
function q(context, selector, attrArg, trim = true) {
|
||||||
|
const attr = attrArg === true ? 'textContent' : attrArg;
|
||||||
|
|
||||||
|
if (attr) {
|
||||||
|
const value = context.querySelector(selector)[attr];
|
||||||
|
|
||||||
|
return trim ? value.trim() : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
function qall(context, selector, attrArg, trim = true) {
|
||||||
|
const attr = attrArg === true ? 'textContent' : attrArg;
|
||||||
|
|
||||||
|
if (attr) {
|
||||||
|
return Array.from(context.querySelectorAll(selector), el => (trim ? el[attr]?.trim() : el[attr]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(context.querySelectorAll(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
function qdate(context, selector, format, match, attr = 'textContent') {
|
||||||
|
const dateString = context.querySelector(selector)[attr];
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const dateStamp = dateString.match(match);
|
||||||
|
|
||||||
|
if (dateStamp) return moment.utc(dateStamp[0], format).toDate();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return moment.utc(dateString.trim(), format).toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function qimages(context, selector = 'img', attr = 'src') {
|
||||||
|
return qall(context, selector, attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function qposter(context, selector = 'video', attr = 'poster') {
|
||||||
|
return q(context, selector, attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function qtrailer(context, selector = 'source', attr = 'src') {
|
||||||
|
return q(context, selector, attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function qlength(context, selector, attr = 'textContent') {
|
||||||
|
const durationString = q(context, selector, attr);
|
||||||
|
const duration = durationString.match(/(\d+:)?\d+:\d+/);
|
||||||
|
|
||||||
|
if (duration) {
|
||||||
|
const segments = ['00'].concat(duration[0].split(':')).slice(-3);
|
||||||
|
|
||||||
|
return moment.duration(segments.join(':')).asSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const funcs = {
|
||||||
|
q,
|
||||||
|
qall,
|
||||||
|
qdate,
|
||||||
|
qimages,
|
||||||
|
qposter,
|
||||||
|
qlength,
|
||||||
|
qtrailer,
|
||||||
|
qa: qall,
|
||||||
|
qd: qdate,
|
||||||
|
qi: qimages,
|
||||||
|
qp: qposter,
|
||||||
|
ql: qlength,
|
||||||
|
qt: qtrailer,
|
||||||
|
};
|
||||||
|
|
||||||
|
function ctx(element) {
|
||||||
|
const contextFuncs = Object.entries(funcs)
|
||||||
|
.reduce((acc, [key, func]) => ({ ...acc, [key]: (...args) => func(element, ...args) }), {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
element,
|
||||||
|
...contextFuncs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ctxa(context, selector) {
|
||||||
|
return Array.from(context.querySelectorAll(selector)).map(element => ctx(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ex(html) {
|
||||||
|
const { document } = new JSDOM(html).window;
|
||||||
|
|
||||||
|
return ctx(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ex,
|
||||||
|
ctx,
|
||||||
|
ctxa,
|
||||||
|
...funcs,
|
||||||
|
};
|