Last active
June 27, 2020 10:36
-
-
Save foriequal0/12794f45f5ef60249e8ba36f978dfaa4 to your computer and use it in GitHub Desktop.
Contents clamp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
텍스트가 너무 넓으면 body를 쪼여주는 Greasmonkey 스크립트입니다.
Wikipedia 처럼 텍스트 위주에 본문이 넓은 사이트에서 유용합니다.
Greasemonkey(Firefox), Tampermonkey(Chrome) 플러그인을 설치하시고 다음 링크를 클릭하시면 자동으로 설치됩니다.
https://gist.github.com/foriequal0/12794f45f5ef60249e8ba36f978dfaa4/raw/content-clamp.user.js