Skip to content

Instantly share code, notes, and snippets.

@scriptify
Created January 18, 2020 18:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scriptify/d0e275a45a2d460022887b7e18e94c87 to your computer and use it in GitHub Desktop.
Save scriptify/d0e275a45a2d460022887b7e18e94c87 to your computer and use it in GitHub Desktop.
You know what it is
/** Play around and try to convert a website into re-usable React components */
// Code from https://github.com/scriptify/statikk-converter/
(() => {
const $ = document.querySelectorAll.bind(document);
function extractElements() {
const allElements = $('body *');
const allStatikkElements = [];
for (const elem of allElements) {
allStatikkElements.push(domToStatikk(elem));
}
return allStatikkElements;
}
/**
* Convert a DOM element to
* a data structure with which
* it's easy to work with later on.
* @example
* {
* tag: 'div',
* attributes: [['class', 'header'], ['id', 'title1']],
* children: StatikkElement
* }
*/
function domToStatikk(elem = document.createElement('div')) {
return {
id: Math.round(Math.random() * 10000),
tag: elem.tagName,
attributes: [...elem.attributes].reduce(
(obj, attr) => ({ [attr.name]: attr.value, ...obj }),
{},
),
children: [...elem.children].map(domToStatikk),
domElem: elem,
};
}
function haveSameHierarchalStructure(elem1, elem2) {
const stringify = element =>
`${element.tag}//${element.children.map(c => c.tag).join('//')}`;
const str1 = stringify(elem1);
const str2 = stringify(elem2);
return str1 === str2;
}
/**
* If elements have same hierarchal structure
* and at least one common class name *or*
* attribute value, they are considered
* *similar*.
*/
function areElementsSimilar(elem1, elem2) {
const elem1ClassNames = (elem1.attributes['class'] || '').split(' ');
const elem2ClassNames = (elem2.attributes['class'] || '').split(' ');
const sameStructure = haveSameHierarchalStructure(elem1, elem2);
if (!sameStructure) return false;
const sameAttribute = Object.keys(elem1.attributes).find(attrName => {
const isSame = elem1.attributes[attrName] === elem2.attributes[attrName];
return isSame;
});
if (sameAttribute) return true;
const sameClassName = elem1ClassNames.find(className =>
elem2ClassNames.includes(className),
);
return sameClassName;
}
/**
* Only use this function with elements
* where areElementsSimilar was true.
* Determines which parts of the specified elements
* stay the same and which parts are different.
* The different parts (attributes, classnames)
* need to be controlled by props later on.
*/
function elementDifferences(elements = []) {
if (elements.length < 2) throw new Error('At least 2 elements required.');
const count = (arr, val) => {
return arr.filter(elem => elem === val).length;
};
const deduplicate = (arr = []) => [...new Set(arr)];
const allClassNames = elements
.map(elem =>
elem.attributes['class'] ? elem.attributes['class'].split(' ') : [],
)
.flat();
// If a classname is exactly elements.length times in the array,
// it is a static value
const staticClassNames = allClassNames.filter(
className => count(allClassNames, className) >= elements.length,
);
// If the attribute name,value pair occurs element.length times in the array,
// it is a static value
const allAttributesNames = elements
.map(elem =>
Object.keys(elem.attributes).filter(attrName => attrName !== 'class'),
)
.flat();
const staticAttributeNames = allAttributesNames.filter(attrName => {
const isDifferent = elements.find(elem => {
return elem.attributes[attrName] !== elements[0].attributes[attrName];
});
return !isDifferent;
});
return {
staticClassNames: deduplicate(staticClassNames),
staticAttributeNames: deduplicate(staticAttributeNames),
};
}
function determineSimilarElements(element, allStatikkElements = []) {
const qualifiesAsPossibleComponent =
element.children.length > 0 || element.attributes.length > 0;
if (!qualifiesAsPossibleComponent) return [];
// Find elements with same hierachal structure and at least
// *one* same attribute or class name
const similarElements = allStatikkElements.filter(
elemToCompare =>
elemToCompare.id !== element.id &&
areElementsSimilar(element, elemToCompare),
);
return similarElements;
}
function toCamelCase(sentenceCase) {
let out = '';
const splitted = sentenceCase.split(/[^a-zA-Z0-9]/).filter(Boolean);
splitted.forEach(function(el, idx) {
var add = el.toLowerCase();
out += idx === 0 ? add : add[0].toUpperCase() + add.slice(1);
});
return out;
}
function cmpName(statikkElem) {
const joinedClassNames = statikkElem.static.staticClassNames.join(' ');
let retName = joinedClassNames ? toCamelCase(joinedClassNames) : '';
const [attrName] = statikkElem.static.staticAttributeNames;
const attrVal = statikkElem.attributes[attrName];
if (!retName) {
try {
const url = new URL(attrVal);
retName = `${url.hostname
.replace('www.', '')
.replace(/\..*$/, '')}Link`;
} catch {}
}
if (!retName) {
retName = attrName ? toCamelCase(attrVal) : '';
}
if (!retName) {
retName = statikkElem.tag;
}
return `${retName.charAt(0).toUpperCase()}${retName.slice(1)}`;
}
function componentify(statikkElem) {
const lowercaseTag = statikkElem.tag.toLowerCase();
const joinedClassNames = statikkElem.static.staticClassNames.join(' ');
const classNameString =
statikkElem.static.staticClassNames.length > 0
? ` className="${joinedClassNames}"`
: '';
const attributesStr = ` ${statikkElem.static.staticAttributeNames
.map(name => `${name}="${statikkElem.attributes[name]}"`)
.join(' ')}`;
const sanitizedAttrStr = attributesStr.slice(0, attributesStr.length - 1);
const render = `<${lowercaseTag}${classNameString}${sanitizedAttrStr}>{children}</${lowercaseTag}>`;
return {
...statikkElem,
component: {
name: cmpName(statikkElem),
render,
},
};
}
function main() {
const allStatikkElements = extractElements();
const similarized = allStatikkElements
.map(elem => ({
...elem,
similarElements: determineSimilarElements(elem, allStatikkElements),
}))
.filter(elem => elem.similarElements && elem.similarElements.length > 0);
const similiarWithStaticProps = similarized.map(elem => ({
...elem,
static: elementDifferences(elem.similarElements.concat([elem])),
}));
const allSameIds = similiarWithStaticProps
.map(el => el.id)
.map(id => {
const elementsWithSimilarElement = similiarWithStaticProps
.filter(el => el.similarElements.map(el => el.id).includes(id))
.map(el => el.id);
return [...elementsWithSimilarElement, id].sort();
});
const uniqueIdsArr = [];
for (const idsArr of allSameIds) {
if (!uniqueIdsArr.find(arr => arr.toString() === idsArr.toString())) {
uniqueIdsArr.push(idsArr);
}
}
const statikkElements = uniqueIdsArr
.map(arr => {
const allFoundElems = similiarWithStaticProps.filter(el =>
arr.includes(el.id),
);
return allFoundElems;
})
.flat();
const componentRepresentationsUnprepared = statikkElements.map(
componentify,
);
const components = componentRepresentationsUnprepared.map(
compRep => compRep.component,
);
const componentRepresentations = componentRepresentationsUnprepared.map(
comp => ({ ...comp, component: comp.component.name }),
);
const uniqueComponents = [
...new Set(components.map(cmp => cmp.name)),
].map(name => components.find(cmp => cmp.name === name));
console.log(componentRepresentations, uniqueComponents);
}
main();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment