feat: things generally seem to work

This commit is contained in:
Pavel Korytov 2022-08-24 15:31:32 +03:00
parent 8a775d63c8
commit c9481d7b3a

View file

@ -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. <em> 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