first commit

This commit is contained in:
2026-07-03 09:04:08 +09:00
commit 00a581c2ea
6 changed files with 2247 additions and 0 deletions
+242
View File
@@ -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