分享:org-mode 中文行内格式标记(零宽空格方案)

org-mode 中,中文标记与单词内部的标记(比如首字母下划线),都涉及到怎样处理额外空格的问题。

这也是个老问题了,站内贴子很多:

最常见的解决方案是利用零宽空格(Zero Width Space,ZWS)。

当然也包括一些不用零宽空格的做法,比如

在尝试多种方案之后,出于对稳定性的偏好,我还是决定使用零宽空格,并且在导出、复制时自动清除零宽空格。

以下配置来自 Emacs China 的大家的智慧,再加 tecosaur 这篇旧文,再加我三个小时的试错整合。

优点:

  • 此配置不依赖额外的包,prettify-symbolsorg 都是内置的。
  • 非常稳定(指 robust),目前没有找到什么需要特别处理的例外情况。
;; 配置 prettify
(setq prettify-symbols-unprettify-at-point t)
(defun org-prettify-set ()
  (interactive)
  (setq prettify-symbols-alist
    (mapcan (lambda (x) (list x (cons (upcase (car x)) (cdr x)))) '(
          ; 这里还能再加更多 pretty symbol
          ("\u200b" . ?▾) ; 零宽空格
          ))) (prettify-symbols-mode 1))
(add-hook 'org-mode-hook 'org-prettify-set)

;; 快速插入零宽空格
(define-key org-mode-map (kbd "M-SPC M-SPC")
  (lambda () (interactive) (insert "\u200b")))

;; 导出时(导出为 org 时除外),去除零宽空格
(defun +org-export-remove-zero-width-space (text _backend _info)
  "Remove zero width spaces from TEXT."
  (unless (org-export-derived-backend-p 'org)
    (replace-regexp-in-string "\u200b" "" text)))
(with-eval-after-load 'ox    ; 没有这一行的话,会因变量未定义而报错。
  (add-to-list 'org-export-filter-final-output-functions #'+org-export-remove-zero-width-space t))

使用方法也很简单,用 M-SPC M-SPC 插入零宽空格,显示为 ,导出 latex 等会自动去除。

如果不喜欢 ,也可以使用其他符号(C-X 8 RET 会弹出一个可插入的符号列表,自己找找看),比如 ˔

Linux 下复制时自动去除零宽空格的配置见 2 楼。

3 个赞

Linux wayland 下可以增加以下配置,在复制到系统剪贴板时去除零宽空格,在 Emacs 内部保留原样。

(修改自 Emacs Wiki

(setq wl-copy-process nil)
(defun wl-copy (text)
  (setq wl-copy-process (make-process :name "wl-copy"
                                      :buffer nil
                                      :command '("wl-copy" "-f" "-n")
                                      :connection-type 'pipe
                                      :noquery t))
  (setq cleaned-text (replace-regexp-in-string "\u200b" "" text))
  (process-send-string wl-copy-process cleaned-text)
  (process-send-eof wl-copy-process))
(defun wl-paste ()
  (if (and wl-copy-process (process-live-p wl-copy-process))
      nil ; should return nil if we're the current paste owner
      (shell-command-to-string "wl-paste -n | tr -d \r")))
(setq interprogram-cut-function 'wl-copy)
(setq interprogram-paste-function 'wl-paste)

x11 下应该也是类似的,无非是把 wl-copy -f -n 改为 xclip 之类的。

2 个赞

多按个M-SPC M-SPC不是跟多输入个空格差不多?只不过显示上减少了空格符那个空间。 我是类似electric-pair一样,输入*自动变**并加上零宽空格:

(add-hook
 'org-mode-hook
 (lambda ()
   ;; emphasis字符加上零宽空格
   (defmacro add_zero_space_char (ch)
     `(lambda ()
        (interactive)
        ;; r支持region!但注意有些键`easy-mark'会影响到,如=
        (tempel-insert (list "" "\x200B" ,ch 'r ,ch "\x200B"))))
   (local-set-key "*" (add_zero_space_char "*"))
   (local-set-key "/" (add_zero_space_char "/"))
   (local-set-key "_" (add_zero_space_char "_"))
   (local-set-key "=" (add_zero_space_char "="))
   (local-set-key "~" (add_zero_space_char "~"))
   (local-set-key "+" (add_zero_space_char "+"))
   (local-set-key "`" (add_zero_space_char "`"))))

用到了tempel的tempel-insert,这个支持对选中内容加上pair。

搜了下相关讨论,主要是为了中文不打空格实现org-mode的标记样式吗?

你说的“差不多”是指按键次数,对吧?确实如此,但我所关注与普通空格的差别,主要是导出与复制时,零宽空格因其特殊性,被简单稳定地去除,而不需要像普通空格那样对各种情况进行判定。

感谢分享。 这种做法或许也不错,但是有时我并不想它这么自动化,尤其是我其实会大量用到数学公式、其他 LaTeX 环境以及代码块,在这些环境下又要判别是否应当自动化的话——数学环境的判定其实就已经是个大难点了,比如如果用 texmathp,那么行内代码中如果出现奇数个 $,则其后的普通正文也会被当成行内公式。如果一种自动化需要对各种特殊情况分别打补丁,甚至有引发连锁反应的风险,我是宁可不要它带来的便利也要手动进行的。

总之不适合我吧。 大家按需来就好。

正是如此。

这个方案还有一个用途:保留 Org-mode 快捷地打出上下标的特性。

常规的解决下划线 _ 被识别为下标格式、^ 被识别为上标格式的方法是 #+options: ^:nil,但这会对全文生效,无法快捷地打出上下标。

如果也需要局部用到上下标的话,就可以利用零宽空格。

例:按照本贴方案,以下文本(注意 P3 和 P4 内含零宽空格)

\(\text{P}_0\) P_1 P_{2} P_​3 P​_4

在导出 LaTeX 后,不再含零宽空格,变成

\(\text{P}_0\) P\textsubscript{1} P\textsubscript{2} P\_3 P\_4

这样就通过零宽空格实现了局部禁用上下标的效果(P3 P4),同时也保留了上下标的特性(P1 P2)。

至于 P0 则展示了利用行内 LaTeX 数学公式打出上下标的方法。 这种方法在启用了 #+options: ^:nil 时,可能是使用上下标的唯二方法之一;另一种是 HTML。显然它麻烦得多,且实现的效果也不完全一致。

相关贴子:

是不是必须前后都加上零宽空格,导出时才能识别为粗体?

在 org-mode中,只要有后零宽空格,就有粗体渲染: 图片

导出html,必须前后都加上零宽空格,才能识别为粗体 图片

这是必须的么?还是可以有其他trick?

是的,我认为这是比较规范的用法,即前后都加上零宽空格。 至于你说的 org-mode 的 buffer 内渲染和导出,很可能因为有不同的实现,所以仅仅使用后零宽空格在 buffer 内渲染时才可识别为粗体。这种区别我猜测可能有性能等方面的考虑,或者单纯是 bug。总之,统一在前后都加零宽空格就可以了。

所以你是 输入零宽空格-输入格式标记和相关内容-再输入零宽空格?

那是不是把插入零宽空格的函数改为插入两个零宽空格,并把光标定位到两个零宽空格之间更方便?

(lambda ()
      (interactive)
      (insert-char #x200b)
      (insert-char #x200b)
      (backward-char))

或者有没有其他更简单的方法?例如选中文本后可以快速加上*,并加上零宽空格?

1 个赞

另外发现一个问题,零宽空格的可视化似乎不太稳定

逗号前,箭头处有零宽空格,但是没有可视化出来

这种用途就只需要一个零宽空格。

并且,即使是输入两个零宽空格,在整个部分都输入完毕之后,还要再 C-f 向右移动光标到零宽空格右边,以输入后续内容。总体的效率提升并没有多大。不过,也不失为一种思路,每个人都有自己的使用习惯。

顺便一提,用 yasnippet 来实现这方面的自动化,可能更灵活,也更方便。

如果能实现的话,我觉得很不错。

你说的这个问题我也注意到了,之前我没太在意,看来不是孤例。 可能是我对 prettify-symbols 的配置还有问题,也可能是它自身的 bug。有待进一步解决。

确实,考虑打出上下标之需要一个空格的话,你的方案更合适。

可以展开说明 yasnippet 的思路么?目前更多应用在更大层面的模版,例如特定类型的笔记模板。对于这么轻量级的功能我还没有很好的思路。

yasnippet 我也是随口一提,自己并没有实践过,只是很常规的用法而已。

这里就加粗来简单给个示例。为 org-mode 新建一个 yasnippet:

# -*- mode: snippet -*-
# name: org-bold
# key: ;;ob
# condition: (and t 'auto)
# --
<ZWS>*$1*<ZWS>$0

(记得把 <ZWS> 替换为零宽空格)

这里利用了自动展开,所以还需要如下配置:

;;允许snippet自动展开
(defvar yas-auto-expand-local-toggle t)
(defun my-yas-try-expanding-auto-snippets ()
  (when (and (boundp 'yas-minor-mode) yas-minor-mode)
    (let ((yas-buffer-local-condition ''(require-snippet-condition . auto)))
      (yas-expand))))
(add-hook 'post-command-hook #'my-yas-try-expanding-auto-snippets)

这样,在 org-mode 下输入 ;;ob 之后就会自动展开为 <ZWS>**<ZWS> 的形态,光标位于正中间,并且在输入完毕之后可用 yas-next-field(一般都会绑个键位的,默认好像是 TAB)来跳至末尾。