243 lines
8.5 KiB
EmacsLisp
243 lines
8.5 KiB
EmacsLisp
;;; my-org-unfill-aware.el --- Org-aware hard line break remover -*- lexical-binding: t; -*-
|
|
|
|
;; Author: Takahiro Ohkubo / ChatGPT
|
|
;; Version: 0.1
|
|
;; Keywords: org, text, clipboard
|
|
;;
|
|
;; This file haves Org-aware functions to remove hard line breaks
|
|
;; from normal paragraphs while preserving Org structures such as
|
|
;; headings, lists, tables, source/example/export blocks, drawers,
|
|
;; comments, horizontal rules, and display math lines.
|
|
|
|
(require 'cl-lib)
|
|
(require 'subr-x)
|
|
|
|
(defgroup my-org-unfill-aware nil
|
|
"Org-aware hard line break removal."
|
|
:group 'org
|
|
:prefix "my-org-unfill-aware-")
|
|
|
|
(defcustom my-org-unfill-aware-preserve-list-items t
|
|
"If non-nil, preserve list item lines as structural lines.
|
|
Continuation lines after a list item are still treated as normal paragraph
|
|
text unless they also match a preserved Org structure."
|
|
:type 'boolean
|
|
:group 'my-org-unfill-aware)
|
|
|
|
(defun my-org-unfill-aware--region-or-buffer ()
|
|
"Return region bounds if active, otherwise whole buffer bounds."
|
|
(if (use-region-p)
|
|
(list (region-beginning) (region-end))
|
|
(list (point-min) (point-max))))
|
|
|
|
(defun my-org-unfill-aware--japanese-p (text)
|
|
"Return non-nil if TEXT contains Japanese/CJK characters."
|
|
(string-match-p "[ぁ-んァ-ン一-龯々〆〤ー]" text))
|
|
|
|
(defun my-org-unfill-aware--ascii-normalize-space (s)
|
|
"Normalize whitespace in ASCII paragraph S."
|
|
(string-trim
|
|
(replace-regexp-in-string "[ \t\n]+" " " s)))
|
|
|
|
(defun my-org-unfill-aware--join-lines (lines joiner ascii-p)
|
|
"Join paragraph LINES using JOINER.
|
|
If ASCII-P is non-nil, normalize multiple spaces into one."
|
|
(let ((s (mapconcat #'string-trim lines joiner)))
|
|
(if ascii-p
|
|
(my-org-unfill-aware--ascii-normalize-space s)
|
|
(string-trim s))))
|
|
|
|
(defun my-org-unfill-aware--blank-line-p (line)
|
|
"Return non-nil if LINE is blank."
|
|
(string-match-p "\\`[ \t]*\\'" line))
|
|
|
|
(defun my-org-unfill-aware--heading-p (line)
|
|
"Return non-nil if LINE is an Org heading."
|
|
(string-match-p "\\`[ \t]*\\*+\\s-+" line))
|
|
|
|
(defun my-org-unfill-aware--keyword-p (line)
|
|
"Return non-nil if LINE is an Org keyword line."
|
|
(string-match-p "\\`[ \t]*#\\+[-A-Za-z0-9_]+:" line))
|
|
|
|
(defun my-org-unfill-aware--comment-p (line)
|
|
"Return non-nil if LINE is an Org comment line."
|
|
(string-match-p "\\`[ \t]*#\\($\\|[^+]\\)" line))
|
|
|
|
(defun my-org-unfill-aware--table-p (line)
|
|
"Return non-nil if LINE is an Org table line."
|
|
(string-match-p "\\`[ \t]*|" line))
|
|
|
|
(defun my-org-unfill-aware--hline-p (line)
|
|
"Return non-nil if LINE is an Org horizontal rule."
|
|
(string-match-p "\\`[ \t]*-----+[ \t]*\\'" line))
|
|
|
|
(defun my-org-unfill-aware--drawer-begin-p (line)
|
|
"Return non-nil if LINE begins an Org drawer."
|
|
(string-match-p "\\`[ \t]*:[A-Za-z0-9_-]+:[ \t]*\\'" line))
|
|
|
|
(defun my-org-unfill-aware--drawer-end-p (line)
|
|
"Return non-nil if LINE ends an Org drawer."
|
|
(string-match-p "\\`[ \t]*:END:[ \t]*\\'" line))
|
|
|
|
(defun my-org-unfill-aware--list-item-p (line)
|
|
"Return non-nil if LINE looks like an Org list item."
|
|
(and my-org-unfill-aware-preserve-list-items
|
|
(string-match-p
|
|
"\\`[ \t]*\\(?:[-+]\\|[0-9]+[.)]\\|[A-Za-z][.)]\\)\\s-+"
|
|
line)))
|
|
|
|
(defun my-org-unfill-aware--display-math-p (line)
|
|
"Return non-nil if LINE looks like display math delimiter/content."
|
|
(string-match-p
|
|
"\\`[ \t]*\\(?:\\\\\\[\\|\\\\\\]\\|\\\\begin{\\|\\\\end{\\|\\$\\$\\)"
|
|
line))
|
|
|
|
(defun my-org-unfill-aware--block-begin-p (line)
|
|
"Return non-nil if LINE begins an Org block."
|
|
(string-match-p "\\`[ \t]*#\\+BEGIN_" (upcase line)))
|
|
|
|
(defun my-org-unfill-aware--block-end-p (line)
|
|
"Return non-nil if LINE ends an Org block."
|
|
(string-match-p "\\`[ \t]*#\\+END_" (upcase line)))
|
|
|
|
(defun my-org-unfill-aware--structural-line-p (line)
|
|
"Return non-nil if LINE should not be joined with surrounding text."
|
|
(or (my-org-unfill-aware--blank-line-p line)
|
|
(my-org-unfill-aware--heading-p line)
|
|
(my-org-unfill-aware--keyword-p line)
|
|
(my-org-unfill-aware--comment-p line)
|
|
(my-org-unfill-aware--table-p line)
|
|
(my-org-unfill-aware--hline-p line)
|
|
(my-org-unfill-aware--drawer-begin-p line)
|
|
(my-org-unfill-aware--drawer-end-p line)
|
|
(my-org-unfill-aware--list-item-p line)
|
|
(my-org-unfill-aware--display-math-p line)))
|
|
|
|
(defun my-org-unfill-aware--process-text (text joiner ascii-p)
|
|
"Return processed TEXT using JOINER.
|
|
If ASCII-P is non-nil, normalize spaces in joined paragraphs."
|
|
(let ((lines (split-string text "\n"))
|
|
(out nil)
|
|
(para nil)
|
|
(in-block nil)
|
|
(in-drawer nil))
|
|
(cl-labels
|
|
((flush-para
|
|
()
|
|
(when para
|
|
(push (my-org-unfill-aware--join-lines
|
|
(nreverse para) joiner ascii-p)
|
|
out)
|
|
(setq para nil))))
|
|
(dolist (line lines)
|
|
(cond
|
|
;; Inside source/example/export/special block: preserve literally.
|
|
(in-block
|
|
(flush-para)
|
|
(push line out)
|
|
(when (my-org-unfill-aware--block-end-p line)
|
|
(setq in-block nil)))
|
|
|
|
;; Inside drawer/properties: preserve literally.
|
|
(in-drawer
|
|
(flush-para)
|
|
(push line out)
|
|
(when (my-org-unfill-aware--drawer-end-p line)
|
|
(setq in-drawer nil)))
|
|
|
|
;; Begin block.
|
|
((my-org-unfill-aware--block-begin-p line)
|
|
(flush-para)
|
|
(push line out)
|
|
(setq in-block t))
|
|
|
|
;; Begin drawer.
|
|
((my-org-unfill-aware--drawer-begin-p line)
|
|
(flush-para)
|
|
(push line out)
|
|
(unless (my-org-unfill-aware--drawer-end-p line)
|
|
(setq in-drawer t)))
|
|
|
|
;; Other structural line.
|
|
((my-org-unfill-aware--structural-line-p line)
|
|
(flush-para)
|
|
(push line out))
|
|
|
|
;; Normal paragraph line.
|
|
(t
|
|
(push line para))))
|
|
(flush-para))
|
|
(mapconcat #'identity (nreverse out) "\n")))
|
|
|
|
(defun my-org-unfill-aware--replace-region (beg end joiner ascii-p)
|
|
"Replace region BEG END with Org-aware unfilled text."
|
|
(let ((new-text
|
|
(my-org-unfill-aware--process-text
|
|
(buffer-substring-no-properties beg end)
|
|
joiner ascii-p)))
|
|
(delete-region beg end)
|
|
(insert new-text)))
|
|
|
|
;;;###autoload
|
|
(defun my-org-unfill-aware-ascii (beg end)
|
|
"Org-aware ASCII mode.
|
|
Remove hard line breaks in normal paragraphs by replacing them with one space.
|
|
Org structures are preserved."
|
|
(interactive (my-org-unfill-aware--region-or-buffer))
|
|
(my-org-unfill-aware--replace-region beg end " " t))
|
|
|
|
;;;###autoload
|
|
(defun my-org-unfill-aware-japanese (beg end)
|
|
"Org-aware Japanese mode.
|
|
Remove hard line breaks in normal paragraphs without inserting spaces.
|
|
Org structures are preserved."
|
|
(interactive (my-org-unfill-aware--region-or-buffer))
|
|
(my-org-unfill-aware--replace-region beg end "" nil))
|
|
|
|
;;;###autoload
|
|
(defun my-org-unfill-aware-auto (beg end)
|
|
"Org-aware auto mode.
|
|
If the selected text contains Japanese/CJK characters, use Japanese mode.
|
|
Otherwise use ASCII mode."
|
|
(interactive (my-org-unfill-aware--region-or-buffer))
|
|
(let ((text (buffer-substring-no-properties beg end)))
|
|
(if (my-org-unfill-aware--japanese-p text)
|
|
(my-org-unfill-aware-japanese beg end)
|
|
(my-org-unfill-aware-ascii beg end))))
|
|
|
|
(defun my-org-unfill-aware--copy-only (beg end fn)
|
|
"Copy processed text from BEG END using FN without modifying current buffer."
|
|
(let* ((text (buffer-substring-no-properties beg end))
|
|
(buf (generate-new-buffer " *my-org-unfill-aware-copy*")))
|
|
(unwind-protect
|
|
(with-current-buffer buf
|
|
(insert text)
|
|
(funcall fn (point-min) (point-max))
|
|
(kill-new (buffer-string)))
|
|
(kill-buffer buf))
|
|
(message "Org-aware unfilled text copied to kill-ring.")))
|
|
|
|
;;;###autoload
|
|
(defun my-org-unfill-aware-ascii-copy (beg end)
|
|
"Org-aware ASCII mode, copy only.
|
|
Do not modify current buffer."
|
|
(interactive (my-org-unfill-aware--region-or-buffer))
|
|
(my-org-unfill-aware--copy-only beg end #'my-org-unfill-aware-ascii))
|
|
|
|
;;;###autoload
|
|
(defun my-org-unfill-aware-japanese-copy (beg end)
|
|
"Org-aware Japanese mode, copy only.
|
|
Do not modify current buffer."
|
|
(interactive (my-org-unfill-aware--region-or-buffer))
|
|
(my-org-unfill-aware--copy-only beg end #'my-org-unfill-aware-japanese))
|
|
|
|
;;;###autoload
|
|
(defun my-org-unfill-aware-auto-copy (beg end)
|
|
"Org-aware auto mode, copy only.
|
|
Do not modify current buffer."
|
|
(interactive (my-org-unfill-aware--region-or-buffer))
|
|
(my-org-unfill-aware--copy-only beg end #'my-org-unfill-aware-auto))
|
|
|
|
(provide 'my-org-unfill-aware)
|
|
;;; my-org-unfill-aware.el ends here
|