@@ 34,6 34,7 @@
(require 'subr-x)
(require 'pcase))
(require 'flymake)
+(require 'xdg)
(defgroup flymake-proselint ()
"Flymake backend for proselint."
@@ 50,6 51,197 @@ The following %-sequences are replaced:
%c - the error code"
:type 'string)
+(defconst flymake-proselint--options
+ ;; Regenerate using:
+ ;;
+ ;; (with-temp-buffer
+ ;; (call-process "proselint" nil t nil "--dump-config")
+ ;; (goto-char (point-min))
+ ;; (let ((config (json-parse-buffer :object-type 'alist)))
+ ;; (mapcar #'car (alist-get 'checks config))))
+ '(airlinese.misc
+ annotations.misc
+ archaism.misc
+ cliches.hell
+ cliches.misc
+ consistency.spacing
+ consistency.spelling
+ corporate_speak.misc
+ cursing.filth
+ cursing.nfl
+ cursing.nword
+ dates_times.am_pm
+ dates_times.dates
+ hedging.misc
+ hyperbole.misc
+ jargon.misc
+ lexical_illusions.misc
+ lgbtq.offensive_terms
+ lgbtq.terms
+ links.broken
+ malapropisms.misc
+ misc.apologizing
+ misc.back_formations
+ misc.bureaucratese
+ misc.but
+ misc.capitalization
+ misc.chatspeak
+ misc.commercialese
+ misc.composition
+ misc.currency
+ misc.debased
+ misc.false_plurals
+ misc.illogic
+ misc.inferior_superior
+ misc.institution_name
+ misc.latin
+ misc.many_a
+ misc.metaconcepts
+ misc.metadiscourse
+ misc.narcissism
+ misc.not_guilty
+ misc.phrasal_adjectives
+ misc.preferred_forms
+ misc.pretension
+ misc.professions
+ misc.punctuation
+ misc.scare_quotes
+ misc.suddenly
+ misc.tense_present
+ misc.waxed
+ misc.whence
+ mixed_metaphors.misc
+ mondegreens.misc
+ needless_variants.misc
+ nonwords.misc
+ oxymorons.misc
+ psychology.misc
+ redundancy.misc
+ redundancy.ras_syndrome
+ security.credit_card
+ security.password
+ sexism.misc
+ skunked_terms.misc
+ spelling.able_atable
+ spelling.able_ible
+ spelling.athletes
+ spelling.em_im_en_in
+ spelling.er_or
+ spelling.in_un
+ spelling.misc
+ terms.animal_adjectives
+ terms.denizen_labels
+ terms.eponymous_adjectives
+ terms.venery
+ typography.diacritical_marks
+ typography.exclamation
+ typography.symbols
+ uncomparables.misc
+ weasel_words.misc
+ weasel_words.very)
+ "List of Proselint options.")
+
+(defconst flymake-proselint--custom-type
+ `(set :greedy t
+ ,@(mapcar
+ (lambda (opt)
+ ;; TODO: Add a :tag
+ `(const ,opt))
+ flymake-proselint--options))
+ "Custom option type for proselint configurations.")
+
+(defun flymake-proselint-safe-option-p (val)
+ "Check if VAL is a safe (and valid) local value."
+ (and (listp val)
+ (catch 'fail
+ (dolist (elem val)
+ (unless (memq elem flymake-proselint--options)
+ (throw 'fail nil)))
+ t)))
+
+(defcustom flymake-proselint-max-errors 1000
+ "After how many errors should Proselint give up?
+For this option to work, the user option
+`flymake-proselint-config-directory' must be non-nil."
+ :safe #'natnump
+ :type 'natnum)
+
+(defcustom flymake-proselint-enable '()
+ "List of Proselint options to enable.
+See `flymake-proselint--options' for a list of possible options.
+For this option to work, the user option
+`flymake-proselint-config-directory' must be non-nil."
+ :safe #'flymake-proselint-safe-option-p
+ :type flymake-proselint--custom-type)
+
+(defcustom flymake-proselint-disable '()
+ "List of Proselint options to disable.
+See `flymake-proselint--options' for a list of possible options.
+For this option to work, the user option
+`flymake-proselint-config-directory' must be non-nil."
+ :safe #'flymake-proselint-safe-option-p
+ :type flymake-proselint--custom-type)
+
+(defcustom flymake-proselint-config-directory
+ (when-let* ((parent (or (xdg-cache-home) (temporary-file-directory)))
+ (dir (expand-file-name "flymake-proselint" parent)))
+ (condition-case nil
+ (progn
+ (unless (file-exists-p dir)
+ (make-directory dir))
+ dir)
+ ;; In case there was an issue while creating the directory, this
+ ;; option will just be disabled.
+ (permission-denied nil)))
+ "Directory to use for temporary configuration files.
+If nil, `flymake-proselint' will not generate temporary
+configuration files. This means that the user options
+`flymake-proselint-max-errors', `flymake-proselint-enable' and
+`flymake-proselint-disable' will have no effect.."
+ :type 'boolean)
+
+(defun flymake-proselint-generate-configuration ()
+ "Generate a configuration and return path.
+This function uses the buffer local values of
+`flymake-proselint-max-errors', `flymake-proselint-enable' and
+`flymake-proselint-disable'. If no configuration was
+generated (either because it wasn't necessary or because disabled
+by `flymake-proselint-inhibit-configuration'), the return value
+will be nil."
+ (unless (and flymake-proselint-config-directory
+ (= flymake-proselint-max-errors 1000) ;default
+ (null flymake-proselint-enable)
+ (null flymake-proselint-disable))
+ (let ((output (expand-file-name
+ ;; As done by `make-backup-file-name-1'
+ (secure-hash
+ 'sha256
+ (prin1-to-string
+ (list (file-truename (buffer-file-name))
+ flymake-proselint-max-errors
+ flymake-proselint-enable
+ flymake-proselint-disable)))
+ flymake-proselint-config-directory)))
+ (unless (file-exists-p output)
+ (let* ((config (with-temp-buffer
+ (call-process "proselint" nil t nil "--dump-config")
+ (goto-char (point-min))
+ (json-parse-buffer :object-type 'alist)))
+ (checks (alist-get 'checks config)))
+ (setf (alist-get 'checks config)
+ (mapcar
+ (lambda (opt)
+ (cons opt
+ (cond
+ ((memq opt flymake-proselint-disable) :false)
+ ((memq opt flymake-proselint-enable) t)
+ ((alist-get opt checks)))))
+ flymake-proselint--options)
+ (alist-get 'max_errors config)
+ flymake-proselint-max-errors)
+ (with-temp-file output (json-insert config))))
+ output)))
+
(defun flymake-proselint-sentinel-1 (source data)
"Handle a successfully parsed DATA from SOURCE.
DATA is a list of error diagnostics that are converted into
@@ 133,7 325,10 @@ node (flymake) Backend functions for more details."
(let ((proc (make-process
:name "proselint-flymake" :noquery t :connection-type 'pipe
:buffer (generate-new-buffer " *proselint-flymake*")
- :command '("proselint" "--json" "-")
+ :command
+ (if-let* ((conf (flymake-proselint-generate-configuration)))
+ (list "proselint" "--config" conf "--json" "-")
+ '("proselint" "--json" "-"))
:sentinel #'flymake-proselint-sentinel)))
(process-put proc 'source (current-buffer))
(process-put proc 'report-fn report-fn)