首页 > 解决方案 > HTML:防止在某些标点符号前面有空格之前换行

问题描述

在法语排版约定中,很少有标点符号;,如:?必须以空格开头。当标点位于其 HTML 容器的边界时,这会导致不需要的换行符。

Voulez-vous coucher avec moi ce soir ? Non, merci.

变成

Voulez-vous coucher avec moi ce soir 
? Non, merci.

目标是像这样得到它:

Voulez-vous coucher avec moi ce 
soir ? Oui, bien sûr.

在使用 CSS 的这些字符之前,我没有找到任何方法来避免换行符。而且:

我正在考虑使用运行 a 的全局 javascript 函数,str.replace(" ?", " ?");但我无法弄清楚一些事情:

有什么建议吗?

注意: 法语中一些标点符号前的空格:有没有 CSS 方法来避免断行?共享相同的目标,但有一个我在这里没有的纯 CSS 约束。

标签: javascripttypographypunctuation

解决方案


理想情况下,你会用硬空间服务器端而不是客户端来替换空间,因为如果你在客户端这样做,你会遇到更新内容时内容会重排的问题,这会引起明显的闪光/移动。

大多数情况下,您会在 Text 节点中遇到此问题,例如:

<p>Voulez-vous coucher avec moi ce soir ? Non, merci.</p>

那是一个 Element(p元素),里面有一个 Text 节点。

可以将空格放在一个 Text 节点中,将标点符号放在另一个节点中

<p><span>Voulez-vous coucher avec moi ce soir </span>? Non, merci.</p>

在那里,“Voulez-vous coucher avec moi ce soir”和空格在一个文本节点中(在 a 中span),但标点符号在另一个文本节点中(在 中p)。我假设这种情况非常罕见,我们不需要担心。

在 Text 节点中处理它相对容易:

fixSpaces(document.body);

function fixSpaces(node) {
    switch (node.nodeType) {
        case 1: // Element
            for (let child = node.firstChild;
                 child;
                 child = child.nextSibling) {
                fixSpaces(child);
            }
            break;

        case 3: // Text node
            node.nodeValue = node.nodeValue.replace(/ ([;?!])/g, "\u00a0$1");
            break;
    }
}

现场示例:

// Obviously, you wouldn't have this in a `setTimeout` in your
// real code, you'd call it directly right away
// as shown in the answer
console.log("Before...");
setTimeout(() => {
    fixSpaces(document.body);
    console.log("After");
}, 1000);

function fixSpaces(node) {
    switch (node.nodeType) {
        case 1: // Element
            for (let child = node.firstChild;
                 child;
                 child = child.nextSibling) {
                fixSpaces(child);
            }
            break;

        case 3: // Text node
            node.nodeValue = node.nodeValue.replace(/ ([;?!])/g, "\u00a0$1");
            break;
    }
}
p {
    font-size: 14px;
    display: inline-block;
    width: 220px;
    border: 1px solid #eee;
}
<p>Voulez-vous coucher avec moi ce soir ? Non, merci.</p>

您可以script在 结尾处的标签中body,就在结束</body>元素之前。

但是再说一遍:如果你可以在服务器端做这个,那会更好,以避免回流。


如果我们想尝试处理一个文本节点以空格结尾而下一个以标点符号开头的情况,如上所示:

<p><span>Voulez-vous coucher avec moi ce soir </span>? Non, merci.</p>

...一种方法是获取所有 Text 节点的数组,然后查看是否有一个以空格结尾,下一个以标点符号开头。这是不完美的,因为它不会尝试处理以空格结尾的 Text 节点位于块元素末尾的情况(例如,标点符号在视觉上总是在不同的行上),但它可能聊胜于无。唯一的缺点是你最终会得到带有额外硬空间的块元素。

fixSpaces(document.body);

function gatherText(node, array = []) {
    switch (node.nodeType) {
        case 1: // Element
            for (let child = node.firstChild;
                 child;
                 child = child.nextSibling) {
                array = gatherText(child, array);
            }
            break;

        case 3: // Text node
            array.push(node);
            break;
    }
    return array;
}

function fixSpaces(node) {
    const texts = gatherText(node);
    for (let i = 0, len = texts.length; i < len; ++i) {
        const text = texts[i];
        const str = text.nodeValue = text.nodeValue.replace(/ ([;?|])/g, "\u00A0$1");
        if (i < len - 1 && str[str.length - 1] === " " && /^[;?!]/.test(texts[i + 1].nodeValue)) {
            // This node ends with a space and the next starts with punctuation,
            // replace the space with a hard space
            text.nodeValue = str.substring(0, str.length - 1) + "\u00A0";
        }
    }
}

console.log("Before...");
setTimeout(() => {
    fixSpaces(document.body);
    console.log("After");
}, 1000);

function gatherText(node, array = []) {
    switch (node.nodeType) {
        case 1: // Element
            for (let child = node.firstChild;
                 child;
                 child = child.nextSibling) {
                array = gatherText(child, array);
            }
            break;

        case 3: // Text node
            array.push(node);
            break;
    }
    return array;
}

function fixSpaces(node) {
    const texts = gatherText(node);
    for (let i = 0, len = texts.length; i < len; ++i) {
        const text = texts[i];
        const str = text.nodeValue = text.nodeValue.replace(/ ([;?|])/g, "\u00A0$1");
        if (i < len - 1 && str[str.length - 1] === " " && /^[;?!]/.test(texts[i + 1].nodeValue)) {
            // This node ends with a space and the next starts with punctuation,
            // replace the space with a hard space
            text.nodeValue = str.substring(0, str.length - 1) + "\u00A0";
        }
    }
}
p {
    font-size: 14px;
    display: inline-block;
    width: 220px;
    border: 1px solid #eee;
}
<p><span>Voulez-vous coucher avec moi ce soir </span>? Non, merci.</p>


将您的问题映射到上述内容:

如何以及何时触发它

将它放在script元素末尾body会很早就触发它,但是在内容在 DOM 中并准备好我们对其进行操作之后。

如何选择合适的标签

我们忽略标签并在文本节点级别工作。

如何仅替换innerText(即不是属性内容)

通过工作文本节点级别。属性不是文本节点,所以我们不处理它们。


推荐阅读