Skip to content

Instantly share code, notes, and snippets.

@foriequal0
Last active June 27, 2020 10:36
Show Gist options
  • Save foriequal0/12794f45f5ef60249e8ba36f978dfaa4 to your computer and use it in GitHub Desktop.
Save foriequal0/12794f45f5ef60249e8ba36f978dfaa4 to your computer and use it in GitHub Desktop.
Contents clamp
// ==UserScript==
// @name Contents Clamp
// @updateURL https://gist.github.com/foriequal0/12794f45f5ef60249e8ba36f978dfaa4/raw/content-clamp.user.js
// @version 10
// @match http://*/*
// @match https://*/*
// @run-at document-idle
// ==/UserScript==
(function ContentsClamp(){
const THRESHOLD_EM = 50; // We targets 50em.
const MIN_COUNT = 5; // To prevent some exceptional elements such as header and footers accidentailly trigger this.
const MIN_TEXT_LENGTH = 120; // To filter some elements that are wide but only have large padding.
function* collectTextNodes(root, minLength) {
// Don't count 'pre', 'code'. They are sometimes wide but it is intentional.
// Usually stackoverflow's code snippets triggers this.
if (root.tagName === "PRE" || root.tagName === "CODE") {
return;
}
// We skip there is 'max-width', assuming it is intentional and well designed.
// Only count if it is applied on 'body' since 'max-width' is common.
const style = getComputedStyle(root);
if (style.maxWidth !== "none" && style.display === "block" && root.tagName !== "BODY") {
return;
}
if ((root.tagName === "SPAN" || root.tagName === "P" || root.tagName === "DIV" || root.tagName === "BLOCKQUOTE")) {
let childTextLen = 0;
// We want to count only when 'span', 'p', 'div' is used as a text container, not for a layout.
// So we measure text length of its immediate text nodes length.
for(const childNode of root.childNodes) {
if (childNode.nodeType === Node.TEXT_NODE) {
childTextLen += childNode.textContent.trim().length;
}
}
if (childTextLen >= minLength) {
yield {
node: root,
length: childTextLen,
};
return;
}
}
for(const child of root.children) {
yield* collectTextNodes(child, minLength);
}
}
function tooWide(root, threshold) {
const texts = collectTextNodes(root, MIN_TEXT_LENGTH);
let count = 0;
let widthSum = 0;
let lengthSum = 0;
for(const { node, length } of texts) {
const fontSize = parseFloat(getComputedStyle(node).fontSize);
function toEm(px) { return px / fontSize; }
for(const rect of node.getClientRects()) {
if (toEm(rect.width) > threshold) {
count += 1;
widthSum += toEm(rect.width);
lengthSum += length;
break;
}
}
// ex: MIN_COUNT = 3, MIN_TEXT_LENGTH=80
// 100, 100, 100 : There are 3 text elements that are relatively short but annoylingly exceeds 80 characters => clamp.
// 300 >= 3 * 80: There is no doubt. It is too wide.
if (count >= MIN_COUNT || lengthSum >= MIN_COUNT * MIN_TEXT_LENGTH) {
return widthSum / count;
}
}
return null;
}
const body = document.querySelector("body");
const fontSize = parseFloat(getComputedStyle(body).fontSize);
function toPx(em) { return em * fontSize; }
function clamp(root, threshold) {
const result = {
trial: 0,
width: 0,
};
let width = root.getBoundingClientRect().width;
const sheet = window.document.styleSheets[0];
let prevRuleIndex = null;
// Iteratively adjusts to match target width
for (let i = 0; i < 3; i++) {
const contentWidth = tooWide(root, threshold);
if (contentWidth === null || contentWidth <= threshold || (contentWidth - threshold) < 1) {
break;
}
width -= toPx(contentWidth - threshold) * 0.9;
if (prevRuleIndex !== null) {
sheet.deleteRule(prevRuleIndex);
}
prevRuleIndex = sheet.cssRules.length;
const rule = `
:root {
max-width: ${width}px;
position: absolute;
left: calc(50% - ${width}px / 2);
transform: translate(calc(${width}px / 2 - 50%));
}`;
sheet.insertRule(rule, sheet.cssRules.length);
result.trial++;
result.width = width;
}
return result;
}
var resizeObserver = new ResizeObserver(() => {
// Heuristic shortcut. It is assumed to be narrow enough, but there might be possible resize.
if (body.getBoundingClientRect().width < toPx(THRESHOLD_EM)) {
return;
}
const clamped = clamp(body, THRESHOLD_EM);
if (clamped.trial != 0) {
console.log(`content clamp: it is clamped to ${clamped.width}px (after ${clamped.trial} iterations)`);
resizeObserver.unobserve(body);
}
});
resizeObserver.observe(body);
})();
@foriequal0
Copy link
Author

foriequal0 commented Apr 26, 2020

텍스트가 너무 넓으면 body를 쪼여주는 Greasmonkey 스크립트입니다.
Wikipedia 처럼 텍스트 위주에 본문이 넓은 사이트에서 유용합니다.
Greasemonkey(Firefox), Tampermonkey(Chrome) 플러그인을 설치하시고 다음 링크를 클릭하시면 자동으로 설치됩니다.
https://gist.github.com/foriequal0/12794f45f5ef60249e8ba36f978dfaa4/raw/content-clamp.user.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment