diff --git a/reverso.el b/reverso.el index c5567eb..2c4e14f 100644 --- a/reverso.el +++ b/reverso.el @@ -1,4 +1,4 @@ -;;; reverso.el --- Client for the https://reverso.net translation service -*- lexical-binding: t -*- +;;; reverso.el --- Translation, grammar checking and bilingual concordances -*- lexical-binding: t -*- ;; Copyright (C) 2022 Korytov Pavel @@ -31,11 +31,12 @@ (require 'request) (require 'transient) (require 'url-util) +(require 'dom) (declare-function evil-define-key* "evil-core") (defgroup reverso nil - "Client for the https://reverso.net translation service." + "Translation, grammar checking and bilingual concordances." :group 'applications) (defface reverso-highlight-face @@ -50,12 +51,17 @@ (defface reverso-heading-face '((t (:inherit transient-heading))) - "Face for heading in reverso buffers." + "Face for headings in reverso buffers." :group 'reverso) -(defface reverso-language-face +(defface reverso-keyword-face '((t (:inherit transient-value))) - "Face for language names in reverso buffers." + "Face for various keywords in reverso buffers." + :group 'reverso) + +(defface reverso-definition-face + '((t (:inherit italic))) + "Face for word definitions in reverso buffer." :group 'reverso) (defcustom reverso-max-display-lines-in-input 5 @@ -105,9 +111,9 @@ This one is used for the translation queries.") (portuguese . pt) (romanian . ro) (russian . ru)) - "Mapping from long language names to short ones. + "Another mapping from long language names to short ones. -This one is used for the synonyms queries.") +This one is used for the synonym queries.") (defconst reverso--right-to-left-languages '(arabic hebrew) @@ -131,7 +137,6 @@ This one is used for the synonyms queries.") russian))) "Available languages for diferent operations.") - (defconst reverso--languages-compatible `((context . ((arabic . (english german spanish french hebrew italian @@ -205,10 +210,10 @@ This one is used for the synonyms queries.") portuguese romanian ukrainian)) (ukrainian . (english)) (chinese . (english french spanish ukrainian))))) - "Which languages are compatible with which.") + "Compatibility of languages for different operations.") (defun reverso-verify-settings () - "Check if all the languages are set correctly." + "Check if the settings are correct." (interactive) (let ((languages (mapcar #'car reverso--language-mapping))) (dolist (cell reverso--languages) @@ -256,7 +261,7 @@ A random one is be picked at package initialization.") (defvar reverso--user-agent (nth (random (length reverso--user-agents)) reverso--user-agents) - "User-Agent to use for reverse.el requests.") + "User-Agent to use for reverso.el requests.") (defun reverso--translate (text source target cb) "Translate TEXT from language SOURCE to TARGET. @@ -265,11 +270,10 @@ SOURCE and TARGET are keys of `reverso--languages'. CB is called with the result. The result is an alist with the following keys: -- `:corrected-text': set when the text has been corrected +- `:corrected-text': corrected version of SOURCE (if available) - `:language-from': the source language - `:language-to': the target language -- `:detected-language': set when the detected target language is - different from the source language +- `:detected-language': the detected source language - `:translation': a string with translated text - `:context-results': a list with found contexts. An item of the list is an alist with the keys: @@ -315,7 +319,7 @@ The result is an alist with the following keys: (message "Error!: %S" error-thrown))))) (defun reverso--convert-string (dom) - "Convert html DOM from the reverso API to fontified string. + "Convert DOM from the reverso API to fontified string. reverso.net uses tags to highlight relevant works, e.g. for the selected word in the context search. This function fontifies words @@ -393,14 +397,14 @@ ALIST is an alist, LOOKUP-VALUE is a value to look in `cdr'." (:context-results . ,context-results)))) (defun reverso--get-context (text source target cb) - "Do a context translation for TEXT from SOURCE to TARGET. + "Find bilingual concordances for TEXT. SOURCE and TARGET are keys of `reverso--languages'. CB is called with the result. The result is a list of alists with the keys: -- `:source': string in the source language -- `:target': string in the target language" +- `:source': a string in the source language +- `:target': the same string in the target language" (unless (and (alist-get source reverso--language-mapping) (member source (alist-get 'context reverso--languages))) @@ -432,7 +436,7 @@ The result is a list of alists with the keys: (message "Error!: %S" error-thrown))))) (defun reverso--get-context-parse (data) - "Parse response from reverso context API into an alist. + "Parse response from reverso context API. DATA is an html string." (let ((html (with-temp-buffer @@ -456,7 +460,7 @@ The result is a list of alists with the following keys: - `:kind': part of speech - `:synonyms': list of alists: - `:synonym': word - - `:relevant': if t considered a \"good match\" by the service + - `:relevant': if t, considered a \"good match\" by the service - `:examples': list of strings with examples - `:antonyms': list of alists - `:antonym': word" @@ -521,6 +525,27 @@ HTML is a string." `((:antonym . ,text)))))))) (defun reverso--get-grammar (text language cb) + "Check TEXT in LANGUAGE for errors. + +CB is called with the result. + +The result is an alist with the keys: +`:corrected-text': corrected version of TEXT +`:source-text-hl': TEXT with highlighted errors +`:corrections': a list of alists with keys: + - `:type': error type + - `:short-description' + - `:long-description' + - `:mistake-text': part of TEXT with the error + - `:correction-text': corrected version of `:mistake-text' + - `:correction-defition': definiton of `:correction-text', if + available + - `:start-index': start index in TEXT + - `:end-index': end index TEXT + - `:suggestions': a list of alists with keys: + - `:text': the suggested text + - `:definition': definition of `:text', if available + - `:category': category of the correction, if available" (unless (member language (alist-get 'grammar reverso--languages)) (error "Wrong language: %s" language)) (request (concat (alist-get 'grammar reverso--urls) @@ -543,34 +568,39 @@ HTML is a string." (message "Error!: %S" error-thrown))))) (defun reverso--get-grammar-parse (source-text data) + "Parse the reverso grammar API response. + +SOURCE-TEXT is the text sent for checking. DATA is the JSON reply." (let* ((corrected-text (alist-get 'text data)) (source-text-hl (with-temp-buffer (insert source-text) (cl-loop for corr across (alist-get 'corrections data) if (alist-get 'startIndex corr) - do (put-text-property (alist-get 'startIndex corr) - (alist-get 'endIndex corr) + do (put-text-property (1+ (alist-get 'startIndex corr)) + (+ 2 (alist-get 'endIndex corr)) 'face 'reverso-error-face)) (buffer-string))) (corrections (cl-loop for corr across (alist-get 'corrections data) - collect `((type . ,(alist-get 'type corr)) - (short-description . ,(alist-get 'shortDescription corr)) - (long-description . ,(alist-get 'longDescription corr)) - (mistake-text . ,(alist-get 'mistakeText corr)) - (correction-text . ,(alist-get 'correctionText corr)) - (correction-defition . ,(alist-get 'correctionDefinition corr)) - (suggestions + collect `((:type . ,(alist-get 'type corr)) + (:short-description . ,(alist-get 'shortDescription corr)) + (:long-description . ,(alist-get 'longDescription corr)) + (:mistake-text . ,(alist-get 'mistakeText corr)) + (:correction-text . ,(alist-get 'correctionText corr)) + (:correction-defition . ,(alist-get 'correctionDefinition corr)) + (:start-index . ,(alist-get 'startIndex corr)) + (:end-index . ,(alist-get 'endIndex corr)) + (:suggestions . ,(cl-loop for s across (alist-get 'suggestions corr) collect - `((text . ,(alist-get 'text s)) - (definition . ,(alist-get 'definition s)) - (category . ,(alist-get 'category s))))))))) - `((corrected-text . ,corrected-text) - (source-text . ,source-text-hl) - (corrections . ,corrections)))) + `((:text . ,(alist-get 'text s)) + (:definition . ,(alist-get 'definition s)) + (:category . ,(alist-get 'category s))))))))) + `((:corrected-text . ,corrected-text) + (:source-text-hl . ,source-text-hl) + (:corrections . ,corrections)))) ;;; Buffers @@ -608,16 +638,16 @@ source text." (let ((multiline (string-match-p "\n" text))) (insert (propertize (symbol-name (alist-get :language-from data)) - 'face 'reverso-language-face) + 'face 'reverso-keyword-face) " -> " (propertize (symbol-name (alist-get :language-to data)) - 'face 'reverso-language-face)) + 'face 'reverso-keyword-face)) (when (alist-get :detected-language data) (insert " [detected: " (propertize (symbol-name (alist-get :detected-language data)) - 'face 'reverso-language-face) + 'face 'reverso-keyword-face) "]")) (insert "\n\n") @@ -688,11 +718,11 @@ DATA is a list of alists with the following keys: for target = (alist-get :target datum) do (insert (propertize (format (format "%%-%ds: " lang-length) lang-to-name) - 'face 'reverso-language-face) + 'face 'reverso-keyword-face) source "\n" (propertize (format (format "%%%ds: " lang-length) lang-from-name) - 'face 'reverso-language-face) + 'face 'reverso-keyword-face) target "\n\n"))) (defun reverso--translate-render-brief (text data) @@ -731,7 +761,7 @@ INPUT is the input string. DATA is a list as defined in (when (alist-get :kind datum) (insert (propertize "Part of speech: " - 'face 'reverso-language-face) + 'face 'reverso-keyword-face) (alist-get :kind datum) "\n")) (when (alist-get :synonyms datum) @@ -760,6 +790,67 @@ INPUT is the input string. DATA is a list as defined in (insert "- " (alist-get :antonym antonym) "\n")) (insert "\n")))) +(defun reverso--grammar-render (data) + "Render grammar checking results. + +DATA is an alist as defined in `reverso--get-grammar'." + (insert + (propertize "Source text: " 'face 'reverso-heading-face) + "\n" + (alist-get :source-text-hl data) + "\n\n" + (propertize "Corrected text: " 'face 'reverso-heading-face) + "\n" + (alist-get :corrected-text data) + "\n\n") + (when (alist-get :corrections data) + (insert + (propertize "Corrections: " 'face 'reverso-heading-face) + "\n") + (dolist (corr (alist-get :corrections data)) + (insert + (propertize (or (alist-get :type corr) "") + 'face 'reverso-keyword-face)) + (when (alist-get :short-description corr) + (insert ": " (alist-get :short-description corr))) + (insert "\n") + (when (alist-get :long-description corr) + (insert (alist-get :long-description corr)) + (insert "\n")) + (insert "\n") + (insert + (propertize + (or (alist-get :mistake-text corr) "") + 'face 'reverso-highlight-face) + " -> " + (propertize + (or (alist-get :correction-text corr) "") + 'face 'reverso-highlight-face)) + (when (alist-get :correction-defition corr) + (insert " [" (propertize + (alist-get :correction-defition corr) + 'face 'reverso-definition-face) + "]")) + (insert "\n\n") + (when (alist-get :suggestions corr) + (insert "Suggestions:\n") + (dolist (sg (alist-get :suggestions corr)) + (insert "- ") + (when (alist-get :category sg) + (insert (alist-get :category sg) ": ")) + (insert (propertize + (alist-get :text sg) + 'face 'reverso-highlight-face)) + (when (alist-get :definition sg) + (insert + " [" + (propertize + (alist-get :definition sg) + 'face 'reverso-definition-face) + "]")) + (insert "\n")) + (insert "\n"))))) + (defmacro reverso--with-buffer (&rest body) "Execute BODY in the clean `reverso' results buffer." (declare (indent 0)) @@ -781,14 +872,25 @@ INPUT is the input string. DATA is a list as defined in ((format :initform " %k %d: %v")) "Class used for retrieving the input string.") +(defvar reverso--use-buffer-as-input nil + "Whether to use the current buffer as input. + +That parameter is normally set by the prefix argument, but as it +doesn't pesist between parent and child invocations, this variable is +used instead.") + (cl-defmethod transient-init-value ((obj reverso--transient-input)) + "Initialize the value of `reverso--transient-input'. + +OBJ is an instance of the class." (oset obj value (cond ((and (slot-boundp obj 'value) (oref obj value)) (oref obj value)) ((region-active-p) (buffer-substring-no-properties (region-beginning) (region-end))) - ((equal current-prefix-arg '(4)) + ((or (equal current-prefix-arg '(4)) + reverso--use-buffer-as-input) (if reverso--output reverso--output (buffer-substring-no-properties (point-min) (point-max)))) @@ -796,9 +898,15 @@ INPUT is the input string. DATA is a list as defined in (t "")))) (cl-defmethod transient-infix-read ((obj reverso--transient-input)) + "Read input string from the minibuffer. + +OBJ is an instance of the `reverso--transient-input'." (read-from-minibuffer "Input: " (oref obj value))) (cl-defmethod transient-format-value ((obj reverso--transient-input)) + "Format the input string for display in the transient buffer. + +OBJ is an instance of `reverso--transient-input'." (propertize (with-temp-buffer (insert (oref obj value)) @@ -830,24 +938,47 @@ INPUT is the input string. DATA is a list as defined in (is-target :initarg :is-target :initform nil)) "Class used for switching the language.") -(defvar reverso--source-value nil) +(defvar reverso--source-value nil + "Selected source language in `reverso'. -(defvar reverso--target-value nil) +Used for persistence between invocations of transient buffers.") -(defvar reverso--prefer-brief nil) +(defvar reverso--target-value nil + "Selected target language in `reverso'. -(defun reverso--get-available-languages (obj &optional target-languages-list is-target) +Used for persistence between invocations of transient buffers.") + +(defvar reverso--prefer-brief nil + "If non-nil, select brief translation display by default. + +Used for persistence between invocations of transient buffers.") + +(defun reverso--get-available-languages (obj) + "Get available languages for the operation. + +OBJ is an instance of transient infix. It needs to have the following +slots: +- `languages': list of available languages +- `is-target': if OBJ selects a target language +- `target-languages': alist, where the key is a source language, and + the value is a list of available target languages for that source + language. + +If `is-target' is non-nil, then selection of languages has to limited. +In that case, `reverso--source-value' is used to get the source +language." (let* ((all-languages (seq-sort (lambda (a b) (string-lessp (symbol-name a) (symbol-name b))) (or (oref obj languages) (mapcar #'car reverso--language-mapping)))) + (is-target (oref obj is-target)) (source-language (when is-target (or reverso--source-value (car all-languages)))) - (target-languages (when (and is-target target-languages-list) - (alist-get source-language target-languages-list))) + (target-languages (when (and is-target (oref obj target-languages)) + (alist-get source-language (oref obj target-languages)))) (languages (cl-loop for lang in all-languages if (or (not source-language) (not (eq source-language lang))) @@ -861,11 +992,18 @@ INPUT is the input string. DATA is a list as defined in (seq-intersection intersection-1 target-languages)))))) (defun reverso--get-language-variable (obj) + "Get the name of the variable that stores the selected language. + +OBJ is an instance of a transient infix, that has to have a slot +called `is-target'." (if (oref obj is-target) 'reverso--target-value 'reverso--source-value)) (cl-defmethod transient-init-value ((obj reverso--transient-language)) + "Initialize the value for the language picker. + +OBJ is an instance of `reverso--transient-language'." (let ((value (cond ((and (slot-boundp obj 'value) (oref obj value)) @@ -880,6 +1018,9 @@ INPUT is the input string. DATA is a list as defined in (oset obj value value))) (cl-defmethod transient-format-value ((obj reverso--transient-language)) + "Format the value of the language picker. + +OBJ is an instance of `reverso--transient-language'." (let ((value (transient-infix-value obj))) (concat (propertize "[" 'face 'transient-inactive-value) @@ -889,18 +1030,15 @@ INPUT is the input string. DATA is a list as defined in (if (eq choice value) 'transient-value 'transient-inactive-value))) - (reverso--get-available-languages - obj - (oref obj target-languages) - (oref obj is-target)) + (reverso--get-available-languages obj) (propertize "|" 'face 'transient-inactive-value)) (propertize "]" 'face 'transient-inactive-value)))) (cl-defmethod transient-infix-read ((obj reverso--transient-language)) - (let* ((choices (reverso--get-available-languages - obj - (oref obj target-languages) - (oref obj is-target))) + "Pick a value in the language picker. + +OBJ is an instance of `reverso--transient-language'." + (let* ((choices (reverso--get-available-languages obj)) (current-idx (or (cl-position (oref obj value) choices) -1)) (next-idx (% (1+ current-idx) (length choices))) (next-choice @@ -917,10 +1055,10 @@ INPUT is the input string. DATA is a list as defined in next-choice)) (cl-defmethod transient-infix-value ((obj reverso--transient-language)) - (let* ((choices (reverso--get-available-languages - obj - (oref obj target-languages) - (oref obj is-target))) + "Get the current value of the language picker. + +OBJ is an instance of `reverso--transient-language'." + (let* ((choices (reverso--get-available-languages obj)) (current-idx (or (cl-position (oref obj value) choices) -1))) (nth current-idx choices))) @@ -928,10 +1066,15 @@ INPUT is the input string. DATA is a list as defined in "Toggle brief output.") (cl-defmethod transient-init-value ((obj reverso--transient-brief)) + "Initialize the value of the brief output switcher. + +OBJ is an instance of `reverso--transient-brief'." (oset obj value reverso--prefer-brief)) (cl-defmethod transient-infix-read ((obj reverso--transient-brief)) - "Toggle the switch on or off." + "Toggle the switch on or off. + +OBJ is an instance of `reverso--transient-brief'." (setq reverso--prefer-brief (null (oref obj value)))) @@ -997,6 +1140,7 @@ INPUT is the input string. DATA is a list as defined in (reverso--translate-render input data)))))) (transient-define-prefix reverso-translate () + "Translate text." ["Input" ("i" "Input" reverso--transient-input-infix)] ["Parameters" @@ -1036,6 +1180,10 @@ INPUT is the input string. DATA is a list as defined in (setq-local reverso--data data))))) (transient-define-prefix reverso-context () + "Find bilingual concordances for text. + +A bilingual concordance is a pair of strings of the same text in +different languages." ["Input" ("i" "Input" reverso--transient-input-infix)] ["Parameters" @@ -1065,6 +1213,7 @@ INPUT is the input string. DATA is a list as defined in (setq-local reverso--data data))))) (transient-define-prefix reverso-synonyms () + "Find synomyms." ["Input" ("i" "Input" reverso--transient-input-infix)] ["Parameters" @@ -1073,5 +1222,53 @@ INPUT is the input string. DATA is a list as defined in (reverso--synonyms-exec-suffix) ("q" "Quit" transient-quit-one)]) +(transient-define-infix reverso--transient-grammar-language () + :class 'reverso--transient-language + :description "Language" + :key "s" + :argument "-s" + :languages (alist-get 'grammar reverso--languages)) + +(transient-define-suffix reverso--grammar-exec-suffix (input source) + :key "e" + :description "Check grammar" + (interactive (transient-args transient-current-command)) + (reverso--get-grammar + input source + (lambda (data) + (reverso--with-buffer + (reverso--grammar-render data) + (setq-local reverso--data data))))) + +(transient-define-prefix reverso-grammar () + "Check grammar." + ["Input" + ("i" "Input" reverso--transient-input-infix)] + ["Parameters" + (reverso--transient-grammar-language)] + ["Actions" + (reverso--grammar-exec-suffix) + ("q" "Quit" transient-quit-one)]) + +(transient-define-prefix reverso () + "Translation, grammar checking and bilingual concordances and more." + ["Commands" + ("t" "Translation" reverso-translate) + ("c" "Context" reverso-context) + ("s" "Synomyms" reverso-synonyms) + ("g" "Grammar check" reverso-grammar)] + ["Actions" + ("q" "Quit" transient-quit-one) + ("k" "Kek" (lambda () (interactive) + (message "%s" + (cl-mapcan (lambda (obj) + (when (and (slot-exists-p obj 'scope) + (oref obj scope)) + (oref obj scope))) + (transient-suffixes 'reverso)))))] + (interactive) + (setq-local reverso--use-buffer-as-input (equal current-prefix-arg '(4))) + (transient-setup 'reverso)) + (provide 'reverso) ;;; reverso.el ends here