first commit
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
;;; 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 provides 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
|
||||
Reference in New Issue
Block a user