;;; 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