emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/hiddenquote 9dac184 01/14: Initial commit


From: Stefan Monnier
Subject: [elpa] externals/hiddenquote 9dac184 01/14: Initial commit
Date: Tue, 9 Feb 2021 18:06:52 -0500 (EST)

branch: externals/hiddenquote
commit 9dac184568214de2300e7b685d43e220262feaa2
Author: Mauro Aranda <maurooaranda@gmail.com>
Commit: Mauro Aranda <maurooaranda@gmail.com>

    Initial commit
---
 .gitignore     |   36 ++
 hiddenquote.el | 1519 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 puzzles/1.ipuz |  947 +++++++++++++++++++++++++++++++++++
 puzzles/2.ipuz |  532 ++++++++++++++++++++
 puzzles/3.ipuz |  798 +++++++++++++++++++++++++++++
 puzzles/4.ipuz |  532 ++++++++++++++++++++
 6 files changed, 4364 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..24876cb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# Files for Git to ignore.
+
+# Copyright 2021 Mauro Aranda
+
+# This file is part of claringrilla.
+
+# claringrilla is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# claringrilla is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with claringrilla.  If not, see <https://www.gnu.org/licenses/>.
+
+# Logs and temp.
+*.log
+*.tmp
+
+# Patches
+*.patch
+
+# Junk
+*~
+.#*
+\#*\#
+
+# Compiled ELisp
+*.elc
+
+# Design
+hiddenquote.org
diff --git a/hiddenquote.el b/hiddenquote.el
new file mode 100644
index 0000000..c27a191
--- /dev/null
+++ b/hiddenquote.el
@@ -0,0 +1,1519 @@
+;;; hiddenquote.el --- Major mode for doing hidden quote puzzles -*- 
lexical-binding: t -*-
+
+;; Copyright (C) 2020-2021 by Mauro Aranda
+
+;; Author: Mauro Aranda <maurooaranda@gmail.com>
+;; Maintainer: Mauro Aranda <maurooaranda@gmail.com>
+;; Created: Sun Dec 13 11:10:00 2020
+;; Version: 0.1
+;; Package-Version: 0.1
+;; Package-Requires: ((emacs "25.1"))
+;; URL: http://www.mauroaranda.com
+;; Keywords: games
+
+;; This file is NOT part of GNU Emacs.
+
+;; hiddenquote is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License
+;; as published by the Free Software Foundation; either version 3
+;; of the License, or (at your option) any later version.
+;;
+;; hiddenquote is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with hiddenquote. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; This file contains all the hidden quote puzzle rendering, major mode
+;; and commands.  It also defines a source to retrieve puzzles of
+;; the hidden quote type, created by myself.
+;;
+;; Usage:
+;; (require 'hiddenquote)
+;; M-x hiddenquote
+;; Select the source you want to retrieve the puzzle from.
+;; With a prefix argument, the command will prompt for a specific ID number.
+;;
+;; Read the definitions for each word and fill the character cells
+;; to complete the puzzle, marking the syllables as used, as you use them,
+;; Move around the buffer with the usual movement commands.
+;; Edit each character cell with the usual editing commands.
+;; To mark a syllable as used, click it with the mouse or move point to
+;; it and hit RET.
+;; When the puzzle is complete, you should be able to read the hidden
+;; quote in the highlighted cells.
+;;
+;; To change the puzzle sources, customize `hiddenquote-sources'.
+;;
+;; M-x customize-group RET hiddenquote
+;; to see other customization options.
+;; To see what faces you can customize, type
+;; M-x customize-group RET hiddenquote-faces
+
+;;; Code:
+
+;; Requirements.
+(require 'eieio)
+(require 'eieio-base)
+(require 'cl-lib)
+(require 'wid-edit)
+(require 'subr-x)
+(require 'json)
+
+(defvar json-pretty-print-max-secs) ; json.el
+
+;; Customization.
+(defgroup hiddenquote nil
+  "Solve hiddenquote in Emacs."
+  :group 'games
+  :version "0.1")
+
+(defcustom hiddenquote-sources
+  '(("puzzles" ipuz hiddenquote-get-local-puzzle)
+    ("hidden-quote" ipuz hiddenquote-get-hidden-quote-puzzle))
+  "An alist of hidden quote puzzle sources.
+Each member is of the form (SOURCE-NAME ORIG-FORMAT FUNCTION).
+
+SOURCE-NAME should be a string, an unique name for the source/publisher.
+ORIG-FORMAT should be a symbol naming the format in which the puzzle
+is retrieved.  At the moment, there is builtin support for two formats:
+.ipuz format and html format.
+
+REST should be a function that retrieves an instance of
+`hiddenquote-hidden-quote-puzzle'.
+
+The function handles the prefix arg given to `hiddenquote'."
+  :type '(repeat :tag "Hidden Quote Sources"
+                 (choice
+                  (list :tag "IPUZ"
+                        (string :tag "Source name")
+                        (const :tag "Format: ipuz" ipuz)
+                        (function :tag "Custom function"))
+                  (list :tag "HTML"
+                        (string :tag "Source name")
+                        (const :tag "Format: html" html)
+                        (function :tag "Function"))
+                  (sexp :tag "Custom source")))
+  :package-version '(hiddenquote . "0.1"))
+
+(defcustom hiddenquote-mode-hook nil
+  "Hook to run when entering `hiddenquote-mode'."
+  :type 'hook
+  :package-version '(hiddenquote . "0.1"))
+
+(defcustom hiddenquote-buffer-name-format "*Hidden Puzzle %n*"
+  "Format to use for the buffer name.
+
+Supported escapes:
+%t: Insert the puzzle title.
+%n: Insert the puzzle number."
+  :type 'string
+  :package-version '(hiddenquote . "0.1"))
+
+(defcustom hiddenquote-upcase-chars (display-graphic-p)
+  "Non-nil if you don't want chars to get upcased automatically.
+
+In TTYs, it is recommended to set this option to nil,
+so characters like \"á\" can be displayed without problem.  This is relevant
+only if the puzzles you play contain such characters, of course."
+  :type 'boolean
+  :package-version '(hiddenquote . "0.1"))
+
+(defcustom hiddenquote-automatic-check nil
+  "Non-nil means check after each change if the answer is right or wrong."
+  :type 'boolean
+  :package-version '(hiddenquote . "0.1"))
+
+(defcustom hiddenquote-directory
+  (file-name-as-directory (locate-user-emacs-file "hiddenquote"))
+  "Directory where to store the puzzles' files."
+  :type 'directory
+  :package-version '(hiddenquote . "0.1"))
+
+(defcustom hiddenquote-show-time t
+  "Whether to show the time passed since starting playing a puzzle."
+  :type 'boolean
+  :package-version '(hiddenquote . "0.1"))
+
+(defcustom hiddenquote-skip-used-syllables nil
+  "If non-nil, tabbing skips used syllables in the Syllables buffer."
+  :type 'boolean
+  :package-version '(hiddenquote . "0.1"))
+
+(defgroup hiddenquote-faces nil
+  "Faces used by `hiddenquote'."
+  :group 'hiddenquote)
+
+(defface hiddenquote
+  '((t (:inherit default :height 1.5)))
+  "Base face used by `hiddenquote'."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-title
+  '((((class color) (background light))
+     (:foreground "#444" :height 0.75 :inherit hiddenquote))
+    (((class color) (background dark))
+     (:foreground "white" :height 0.75 :inherit hiddenquote))
+    (t (:inherit hiddenquote)))
+  "Face used for the title."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-sep
+  '((t (:height 0.1 :inherit default)))
+  "Face to use in character-separating spaces."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-word-number
+  '((((type tty))
+     (:foreground "black" :background "white" :inherit hiddenquote))
+    (((class color))
+     (:foreground "white" :background "#999" :inherit hiddenquote))
+    (t (:inherit hiddenquote)))
+  "Face used for word indexes."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-word-right
+  '((t (:foreground "white" :background "#5adc5f" :inherit hiddenquote)))
+  "Face used for correct answers."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-word-wrong
+  '((t (:foreground "white" :background "#d80026" :inherit hiddenquote)))
+  "Face used for wrong answers."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-value
+  '((t (:foreground "#333" :background "gray80" :inherit hiddenquote)))
+  "Face for characters inserted by the user."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-quote-value
+  '((t (:foreground "#333" :background "#fcee21" :inherit hiddenquote)))
+  "Face for the characters in the grid that hold the quote."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-used-syllable
+  '((t (:foreground "black" :background "yellow" :inherit hiddenquote)))
+  "Face for syllables that the user has used."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-unused-syllable
+  '((t (:inherit hiddenquote)))
+  "Face for syllables that the user hasn't used."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-highlight-syllable
+  '((t (:background "#fcee21" :height 1.5 :inherit default)))
+  "Face for highlighting syllables when the mouse hovers."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-widget-highlight
+  '((t (:background "#fcee21" :inherit hiddenquote)))
+  "Face for highlighting a widget."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-highlight
+  '((((class color) (background light))
+     (:background "#fcee21" :inherit 'default))
+    (((class color) (background dark))
+     (:background "#fcee21" :foreground "black"))
+    (t (:inherit default)))
+  "Face for highlighting the current definition."
+  :package-version '(hiddenquote . "0.1"))
+
+(defface hiddenquote-doc
+  '((t (:foreground "#999" :inherit default)))
+  "Face used for the puzzle commentary."
+  :package-version '(hiddenquote . "0.1"))
+
+;; Variables.
+(defvar hiddenquote-mode-map
+  (let ((map (make-sparse-keymap)))
+    ;; Mouse support.
+    (define-key map [down-mouse-2] #'widget-button-click)
+    (define-key map [down-mouse-1] #'widget-button-click)
+    ;; Widget movement.
+    (define-key map "\t" #'widget-forward)
+    (define-key map [right] #'hiddenquote-forward)
+    (define-key map [left] #'hiddenquote-backward)
+    (define-key map [(shift tab)] #'hiddenquote-backward)
+    (define-key map [backtab] #'hiddenquote-backward)
+    (define-key map "\C-n" #'hiddenquote-next)
+    (define-key map [down] #'hiddenquote-next)
+    (define-key map "\C-p" #'hiddenquote-prev)
+    (define-key map [up] #'hiddenquote-prev)
+    (define-key map [(meta ?g) (meta ?g)] #'hiddenquote-goto-word)
+    ;; Done.
+    (define-key map [(control ?x) (control ?s)] #'hiddenquote-save)
+    (define-key map [(control ?x) ?!] #'hiddenquote-give-up)
+    (define-key map [(control ?x) ?k] #'hiddenquote-quit)
+    map)
+  "Keymap for the buffer where the grid is at.")
+
+(defvar hiddenquote-character-map
+  (let ((map (copy-keymap hiddenquote-mode-map)))
+    ;; Movement.
+    (define-key map "\C-f" #'widget-forward)
+    (define-key map [right] #'widget-forward)
+    (define-key map "\C-b" #'widget-backward)
+    (define-key map [left] #'widget-backward)
+    (define-key map "\C-a" #'hiddenquote-move-beginning-of-word)
+    (define-key map [home] #'hiddenquote-move-beginning-of-word)
+    (define-key map "\C-e" #'hiddenquote-move-end-of-word)
+    (define-key map [end] #'hiddenquote-move-end-of-word)
+    (define-key map "\C-j" #'hiddenquote-next)
+    ;; Editing.
+    (define-key map "\C-k" #'hiddenquote-kill-word)
+    (define-key map [(control shift backspace)] #'hiddenquote-kill-whole-word)
+    (define-key map [backspace] #'hiddenquote-delete-backward-char)
+    (define-key map [delete] #'delete-char)
+    (define-key map [?\d] #'hiddenquote-delete-backward-char)
+    ;; Actions.
+    (define-key map "?" #'hiddenquote-check-answer)
+    (define-key map "\C-m" #'widget-field-activate)
+    ;; Avoid trouble.
+    (define-key map " " #'ignore)
+    (define-key map "\C-o" #'ignore)
+    map)
+  "Keymap for the `hiddenquote-character' widget.")
+
+(defvar hiddenquote-syllables-map
+  (let ((map (make-sparse-keymap)))
+    (set-keymap-parent map widget-keymap)
+    (define-key map "\t" #'hiddenquote-forward-syllable)
+    (define-key map [(shift tab)] #'hiddenquote-backward-syllable)
+    (define-key map [backtab] #'hiddenquote-backward-syllable)
+    map))
+
+(defvar hiddenquote-previous-window-configuration nil
+  "Holds the window configuration before `hiddenquote' creates its windows.")
+
+(defvar-local hiddenquote-current nil
+  "Hold the current puzzle, a `hiddenquote-grid' widget.")
+
+(defvar-local hiddenquote-definition-markers nil
+  "A vector of cons-cells (START . END), the START and END of a definition.
+
+Used locally in the definitions buffer, to highlight/unhighlight them.")
+
+(defvar-local hiddenquote-syllables nil
+  "Hold all syllable widgets in the syllables buffer.")
+
+(defvar-local hiddenquote-buffer nil "The buffer where the grid is at.")
+
+;; Utilities.
+(defun hiddenquote-insert (c face)
+  "Insert C (a character or a string), with face FACE, using `widget-insert'."
+  (let ((s (if (characterp c) (char-to-string c) c)))
+    (widget-insert (propertize s 'face face))))
+
+(defun hiddenquote-clear-buffer ()
+  "Clear the current buffer."
+  (let ((inhibit-read-only t))
+    (erase-buffer)))
+
+(defun hiddenquote-refuse-kill-buffer ()
+  "Refuse to kill a definitions or a syllables buffer."
+  (error "Can't kill %s buffer.  Kill the main puzzle buffer instead"
+         (buffer-name)))
+
+(defun hiddenquote-ensure-file-exists (file)
+  "Create the file named FILE if it not exists."
+  (unless (file-exists-p file)
+    (let ((paren-dir (file-name-directory file)))
+      (when (and paren-dir (not (file-exists-p paren-dir)))
+        (make-directory paren-dir t)))
+    (write-region "" nil file nil 0)))
+
+(defun hiddenquote-expand-puzzle-file-name (filename source)
+  "Use `expand-file-name' on FILENAME, using SOURCE as a relative directory."
+  (expand-file-name filename
+                    (expand-file-name
+                     (file-name-as-directory source)
+                     (file-name-as-directory hiddenquote-directory))))
+
+(defun hiddenquote-widget-backward ()
+  "Compatibility function, to avoid `widget-backward' to skip widgets."
+  (unless (eobp)
+    (forward-char 1)))
+
+;; Classes.
+;; A `hiddenquote-hidden-quote-puzzle' represents a hidden quote puzzle,
+;; following closely its ipuz spec.
+(defclass hiddenquote-hidden-quote-puzzle ()
+  (;; Mandatory.
+   (version :initarg :version
+            :initform "http://ipuz.org/v2";
+            :type string
+            :documentation "The ipuz version")
+   (kind :initarg :kind
+         :initform ["http://mauroaranda.com/puzzles/hiddenquote#1";
+                    "http://ipuz.org/crossword#1";]
+         :type vector
+         :documentation "The ipuz PuzzleKind field")
+   ;; Optional.
+   (id :initarg :id
+       :initform 1
+       :type natnum
+       :custom integer
+       :documentation "An unique id for the ipuz file, a positive number.")
+   (author :initarg :author
+           :initform ""
+           :type string
+           :custom string
+           :documentation "The ipuz file author.")
+   (editor :initarg :editor
+           :initform ""
+           :type string
+           :custom string
+           :documentation "The ipuz file editor.")
+   (copyright :initarg :copyright
+              :initform ""
+              :type string
+              :documentation "Copyright for the ipuz file.")
+   (publisher :initarg :publisher
+              :initform ""
+              :type string
+              :custom string
+              :documentation "Publisher of the puzzle.")
+   (title :initarg :title
+          :initform ""
+          :type string
+          :documentation "Title of the puzzle.")
+   (url :initarg :url
+        :initform ""
+        :type string
+        :documentation "Permanent url for the puzzle.")
+   (origin :initarg :origin
+           :initform ""
+           :type string
+           :custom string
+           :documentation "Program that created the puzzle.")
+   (created-date :initarg :created-date
+                 :initform (format-time-string "%m/%d/%Y" (current-time))
+                 :type string
+                 :custom string
+                 :documentation "Date of creation, in MM/DD/YYYY format.")
+   (description :initarg :description
+                :initform ""
+                :type string
+                :custom text
+                :documentation
+                "Insert documentation for the user, like whose quote it is.")
+   ;; Crossword fields.
+   (clues :initarg :clues
+          :initform []
+          :type vector
+          :custom (vector
+                   (repeat :inline t
+                           (list (string :tag "Answer") (string :tag "Clue"))))
+          :documentation
+          "Clues and answers used in the puzzle.  Have to be ordered.")
+   (saved :initarg :saved
+          :initform nil
+          :type (or null hiddenquote-edit)
+          :documentation "Saved state for the puzzle.")
+   ;; Hiddenquote extensions.
+   (field-lengths :initarg :field-lengths
+                  :initform []
+                  :documentation "Hold the length of each answer."
+                  :type vector)
+   (arrows :initarg :arrows
+           :initform ""
+           :type string
+           :custom string
+           :documentation "Positions of highlighted characters.
+It has to be of the form ARR1,ARR2")
+   (syllables :initarg :syllables
+              :initform []
+              :type vector
+              :custom (vector (repeat :inline t
+                                      (string :tag "Syllable")))
+              :documentation "Syllables (not necessarily sorted.)")
+   (qquote :initarg :quote
+           :initform ""
+           :type string
+           :custom string
+           :documentation "The quote that is hidden in the puzzle.")
+   (subject :initarg :subject
+            :initform ""
+            :type string
+            :custom string
+            :documentation
+            "The subject for the puzzle (e.g., general, chemistry, etc.")
+   ;; Not part of ipuz.
+   (file :initarg :file
+         :initform ""
+         :type string
+         :documentation "Filename to save this puzzle, in ipuz format.")))
+
+;; A `hiddenquote-edit' object is an object that holds the user edits
+;; in a playing session.  This information can be saved, in an .ipuz file,
+;; effectively saving user progress.
+(defclass hiddenquote-edit ()
+  ((answers :initarg :answers
+            :initform nil
+            :documentation "User answers."
+            :type (or vector null))
+   (checked-answers :initarg :checked-answers
+                    :initform nil
+                    :documentation "Answers that the user has checked."
+                    :type list)
+   (used-syllables :initarg :used-syllables
+                   :initform nil
+                   :documentation "A list with the indexes of used-syllables."
+                   :type list)
+   (elapsed-time :initarg :elapsed-time
+                 :initform 0
+                 :documentation "Time elapsed while completing the puzzle."
+                 :type (or number list))))
+
+(cl-defmethod hiddenquote-create-grid ((self hiddenquote-hidden-quote-puzzle))
+  "Create a grid to complete SELF.
+
+Returns the `hiddenquote-grid' widget created."
+  (let* ((w (widget-create 'hiddenquote-grid self)) ; Create the grid.
+         (def-buff (get-buffer-create (concat (buffer-name) " - Definitions")))
+         (cbuff (current-buffer))
+         (inhibit-read-only t)
+         window)
+    ;; Fill the doc, so it looks better.
+    (fill-region (overlay-start (widget-get w :doc-overlay))
+                 (overlay-end (widget-get w :doc-overlay)))
+    ;; Put point at the first word.
+    (goto-char (point-min))
+    ;; Create definitions.
+    (with-current-buffer def-buff
+      (erase-buffer)
+      (add-hook 'kill-buffer-hook #'hiddenquote-refuse-kill-buffer nil t)
+      (hiddenquote-insert "Definitions\n\n" 'hiddenquote-title)
+      (let* ((defs (mapcar (lambda (el)
+                             (cadr el))
+                           (oref self clues)))
+             (i 0)
+             markers opoint)
+        (dolist (def defs)
+          (setq opoint (point))
+          (insert (format "%d. %s." (setq i (1+ i)) def))
+          (fill-region opoint (point))
+          (push (cons (copy-marker opoint) (point-marker)) markers)
+          (insert "\n\n"))
+        (setq hiddenquote-definition-markers (vconcat (reverse markers))))
+      (goto-char (point-min))
+      (setq buffer-read-only t)
+      (setq hiddenquote-buffer cbuff))
+    (setq window (split-window nil nil 'right))
+    (set-window-buffer window def-buff)
+    (set-window-dedicated-p window t)
+    (set-window-parameter window 'no-other-window t)
+    ;; Syllables.
+    (let ((syll-buff (get-buffer-create (concat (buffer-name) " - Syllables")))
+          (syllables (oref self syllables))
+          (used (and (oref self saved)
+                     (oref (oref self saved) used-syllables))))
+      (with-current-buffer syll-buff
+        (kill-all-local-variables)
+        (hiddenquote-clear-buffer)
+        (remove-overlays)
+        (let ((i 0)
+              (max (apply #'max (mapcar (lambda (el)
+                                          (length el))
+                                        syllables))))
+          (setq hiddenquote-syllables
+                (cl-mapcar
+                 (lambda (syl)
+                   ;; FIXME: Maybe it looks better center-aligned.
+                   (let ((w (widget-create
+                             'hiddenquote-syllable
+                             :tag (concat syl (make-string
+                                               (1+ (- max (length syl)))
+                                               ?\s))
+                             :pressed-face 'hiddenquote-highlight-syllable
+                             :value (member i used))))
+                     (when (= 0 (% (setq i (1+ i)) 7))
+                       (insert "\n"))
+                     w))
+                 syllables)))
+        ;; Avoid highlighting the last syllable too much.
+        (hiddenquote-insert " " 'hiddenquote-sep)
+        (setq-local widget-button-click-moves-point t)
+        (use-local-map hiddenquote-syllables-map)
+        (widget-setup)
+        (setq buffer-read-only t)
+        (setq hiddenquote-buffer cbuff)
+        (add-hook 'kill-buffer-hook #'hiddenquote-refuse-kill-buffer nil t)
+        (when (< emacs-major-version 28)
+          (add-hook 'widget-backward-hook
+                    #'hiddenquote-widget-backward nil t))
+        (goto-char (point-min)))
+      (set-window-buffer (split-window window) syll-buff))
+    ;; In case we loaded an already completed puzzle.
+    (unless (hiddenquote-puzzle-complete-p)
+      (hiddenquote-timer-start-timer (widget-get w :hiddenquote-timer)))
+    w))
+
+(cl-defmethod hiddenquote-puzzle-to-ipuz
+  ((self hiddenquote-hidden-quote-puzzle))
+  "Return a string that is an ipuz representation of SELF."
+  (let ((width (1+ (apply #'max (cl-mapcar (lambda (clue)
+                                             (length (nth 0 clue)))
+                                           (oref self clues)))))
+        (height (length (oref self clues)))
+        (arrows (let ((lst (split-string (oref self arrows) ",")))
+                  (cons (string-to-number (car lst))
+                        (string-to-number (cadr lst)))))
+        (edits (oref self saved)))
+    (with-temp-buffer
+      (insert
+       (json-encode `(("version" . ,(oref self version))
+                      ("kind" . ,(oref self kind))
+                      ("copyright" . ,(oref self copyright))
+                      ("publisher" . ,(oref self publisher))
+                      ("url" . ,(oref self url))
+                      ("uniqueid" . ,(number-to-string (oref self id)))
+                      ("title" . ,(oref self title))
+                      ("author" . ,(oref self author))
+                      ("editor" . ,(oref self editor))
+                      ("date" . ,(oref self created-date))
+                      ("origin" . ,(oref self origin))
+                      ("intro" . ,(oref self description))
+                      ("dimensions" . ,(list (cons "width" width)
+                                             (cons "height" height)))
+                      ("puzzle" .
+                       ,(let ((i 1))
+                          (vconcat
+                           (cl-mapcar
+                            (lambda (clue)
+                              (prog1
+                                  (let ((len (length (nth 0 clue))))
+                                    (vconcat (list (number-to-string i))
+                                             (make-list len 0)
+                                             (when (> (- width len) 0)
+                                               (make-list (- width len) nil))))
+                                (setq i (1+ i))))
+                            (oref self clues)))))
+                      ("clues" .
+                       ,(list
+                         (cons "Clues"
+                               (let ((i 1))
+                                 (vconcat
+                                  (cl-mapcar
+                                   (lambda (clue)
+                                     (prog1
+                                         `(("number" . ,i)
+                                           ("clue" . ,(nth 1 clue))
+                                           ("answer" . ,(nth 0 clue))
+                                           ("enumeration" . ,(length
+                                                              (nth 0 clue))))
+                                       (setq i (1+ i))))
+                                   (oref self clues)))))))
+                      ("answer" . ,(oref self qquote))
+                      ("zones" .
+                       ,(let ((arr1 (car arrows))
+                              (arr2 (cdr arrows))
+                              (fake-height (1- height)))
+                          (vconcat
+                           (list
+                            (list
+                             (cons "rect" (vector 0 0 0 fake-height))
+                             (cons "style" '(("border" . 0))))
+                            (list
+                             (cons "rect" (vector arr1 0 arr1 fake-height))
+                             (cons "style" '(("highlight" . t))))
+                            (list
+                             (cons "rect" (vector arr2 0 arr2 fake-height))
+                             (cons "style" '(("highlight" . t))))))))
+                      ("saved" .
+                       ,(list (cons "answers" (if edits (oref edits answers)
+                                                []))
+                              (cons "checked-answers"
+                                    (when edits
+                                      (oref edits checked-answers)))
+                              (cons "used-syllables"
+                                    (when edits
+                                      (oref edits used-syllables)))
+                              (cons "elapsed-time"
+                                    (if edits
+                                        (oref edits elapsed-time)
+                                      0))))
+                      ("volatile" . (("com.hiddenquote:arrows" . "")
+                                     ("com.hiddenquote:subject" . "")
+                                     ("com.hiddenquote:syllables" . "")))
+                      ("com.hiddenquote:arrows" . ,(oref self arrows))
+                      ("com.hiddenquote:subject" . ,(oref self subject))
+                      ("com.hiddenquote:syllables" .
+                       ,(vconcat (sort (copy-sequence
+                                        (oref self syllables))
+                                       #'string-lessp))))))
+      (let ((json-pretty-print-max-secs 0.0))
+        (json-pretty-print (point-min) (point-max)))
+      (buffer-string))))
+
+(cl-defmethod hiddenquote-load-saved-puzzle
+  ((self hiddenquote-hidden-quote-puzzle))
+  "Return a `hiddenquote-edit' instance, read from the file slot in SELF.
+
+If the file does not exist, the `hiddenquote-edit' instance has default
+values."
+  (if (file-exists-p (oref self file))
+      (let ((temp (hiddenquote-puzzle-from-ipuz
+                   (with-current-buffer (find-file-noselect (oref self file) t)
+                     (prog1 (buffer-string)
+                       (kill-buffer))))))
+        (oref temp saved))
+    (make-instance 'hiddenquote-edit)))
+
+(cl-defmethod hiddenquote-format-buffer-name
+  ((self hiddenquote-hidden-quote-puzzle))
+  "Return a buffer name, using `hiddenquote-buffer-name-format' and SELF."
+  (let ((fn (if (fboundp 'string-replace)
+                #'string-replace
+              #'replace-regexp-in-string))
+        ret)
+    (setq ret (funcall fn "%t" (oref self title)
+                       hiddenquote-buffer-name-format))
+    (setq ret (funcall fn "%n" (if (stringp (oref self id))
+                                   (oref self id)
+                                 (number-to-string (oref self id)))
+                       ret))))
+
+;; Hiddenquote Widget.
+;; Use a widget, `hiddenquote-grid', to hold everything.  Its tag is
+;; the puzzle's title.  The value holds all the answers in the grid,
+;; and the documentation is the puzzle intro.
+(define-widget 'hiddenquote-grid 'default
+  "A widget for a hidden quote puzzle."
+  :format "%t\n%d\n%v\n%T"
+  :format-handler #'hiddenquote-grid-format-handler
+  :convert-widget #'hiddenquote-grid-convert-widget
+  :value-create #'hiddenquote-grid-value-create)
+
+(define-widget 'hiddenquote-word 'default
+  "A widget for each answer in a `hiddenquote-grid' widget."
+  :format "%v\n"
+  :value-create #'hiddenquote-word-value-create
+  :value-get #'hiddenquote-word-value-get
+  :value-set #'hiddenquote-word-value-set
+  :validate #'hiddenquote-word-validate
+  :notify #'hiddenquote-word-notify)
+
+(define-widget 'hiddenquote-number 'item
+  "A clickable item.  When actioned, shows if the answer is right or wrong."
+  :format "%[%t%]"
+  :value 'unchecked
+  :button-face-get #'hiddenquote-number-button-face-get
+  :mouse-face 'hiddenquote-widget-highlight
+  :pressed-face 'hiddenquote-widget-highlight
+  :action #'hiddenquote-number-action)
+
+(define-widget 'hiddenquote-character 'character
+  "A character widget for the `hiddenquote-word' widget."
+  :format "%v"
+  :value ?\s
+  :notify #'hiddenquote-character-notify
+  :value-to-internal (lambda (_widget value)
+                       (if (stringp value) value (char-to-string value)))
+  :keymap hiddenquote-character-map
+  :help-echo "")
+
+(define-widget 'hiddenquote-timer 'item
+  "A widget that shows the elapsed time starting from its creation."
+  :format "%v\n"
+  :value-create #'hiddenquote-timer-value-create
+  :hiddenquote-start-value 0
+  :sample-face 'hiddenquote-title)
+
+;; Use clickable-item widgets for the syllables, so the user can toggle their
+;; state.  When the user marks a syllable as used, draw it with the
+;; `hiddenquote-used-syllable' face, otherwise if the user marks a syllable as
+;; unused, draw it with `hiddenquote-unused-syllable' face.
+(define-widget 'hiddenquote-syllable 'item
+  "Used/unused state for a syllable."
+  :format "%[%v%]"
+  :value-create #'hiddenquote-syllable-value-create
+  :button-face-get #'hiddenquote-syllable-button-face-get
+  :mouse-face 'hiddenquote-highlight-syllable
+  :action #'widget-toggle-action
+  :notify #'hiddenquote-syllable-notify)
+
+(defun hiddenquote-grid-convert-widget (widget)
+  "Initialize :tag and :doc properties of `hiddenquote-grid' widget WIDGET.
+Use the number slot of a `hiddenquote-hidden-quote-puzzle' object to build
+the :tag, and use the description slot to build the :doc."
+  (let ((puzzle (nth 0 (widget-get widget :args))))
+    (widget-put widget :hiddenquote puzzle)
+    (widget-put widget :args nil) ; Don't need them anymore.
+    (widget-put widget :tag (propertize
+                             (oref puzzle title)
+                             'face 'hiddenquote-title))
+    (widget-put widget :doc (oref puzzle description)))
+  widget)
+
+(defun hiddenquote-grid-value-create (widget)
+  "Create a `hiddenquote-grid' widget WIDGET, by creating each word."
+  (let* ((puzzle (widget-get widget :hiddenquote))
+         (lengths (oref puzzle field-lengths))
+         children buttons)
+    (dotimes (i (length lengths))
+      ;; FIXME: Maybe it looks better center-aligned.
+      (let ((opoint (point)))
+        (push (widget-create-child-and-convert
+               widget 'hiddenquote-number
+               :hiddenquote-word-number i
+               :tag (format "%-2d" (1+ i))
+               :value (if (and (oref puzzle saved)
+                               (oref (oref puzzle saved) checked-answers))
+                          ;; `json-encode' outputs the symbols as strings,
+                          ;; so get them back as symbols.
+                          (intern-soft
+                           (nth i (oref (oref puzzle saved) checked-answers)))
+                        'unchecked))
+              buttons)
+        (hiddenquote-insert " " 'hiddenquote-sep)
+        (add-text-properties opoint (point)
+                             `(cursor-sensor-functions
+                               ,(list
+                                 (lambda (_window _prev action)
+                                   (if (eq action 'left)
+                                       (hiddenquote-unhighlight-definition i)
+                                     (hiddenquote-highlight-definition i)
+                                     (widget-forward 1)))))))
+      (push
+       (widget-create-child-and-convert
+        widget 'hiddenquote-word
+        :hiddenquote-word-number i
+        :hiddenquote-word-length (aref lengths i)
+        :value (if (and (oref puzzle saved)
+                        (oref (oref puzzle saved) answers))
+                   (aref (oref (oref puzzle saved) answers) i)
+                 (make-string (aref lengths i) ?\s)))
+       children)
+      (hiddenquote-insert "\n" 'hiddenquote-sep))
+    (widget-put widget :buttons (reverse buttons))
+    (widget-put widget :children (reverse children))))
+
+(defun hiddenquote-grid-format-handler (widget ch)
+  "Recognize escape character CH when creating a `hiddenquote-grid' WIDGET."
+  (let ((saved (oref (widget-get widget :hiddenquote) saved)))
+    (cond ((eq ch ?T) ; Print time.
+           (when hiddenquote-show-time
+             (widget-put widget :hiddenquote-timer
+                         (widget-create
+                          'hiddenquote-timer
+                          :hiddenquote-buffer (current-buffer)
+                          :hiddenquote-start-value
+                          (if (and saved (oref saved elapsed-time))
+                              (if (fboundp 'time-convert)
+                                  (time-convert ; Emacs 27.1
+                                   (oref saved elapsed-time)
+                                   'integer)
+                                (oref saved elapsed-time))
+                            0)))))
+          (t (widget-default-format-handler widget ch)))))
+
+(defun hiddenquote-word-value-create (widget)
+  "Create a `hiddenquote-word' widget WIDGET, by creating each character cell.
+Highlight the character cells that form the quote."
+  (let ((arrows (split-string
+                 (oref (widget-get (widget-get widget :parent) :hiddenquote)
+                       arrows)
+                 ","))
+        (opoint (point))
+        (value (widget-get widget :value))
+        children)
+    (dotimes (i (widget-get widget :hiddenquote-word-length))
+      (push
+       (widget-create-child-and-convert
+        widget 'hiddenquote-character
+        :value (aref value i)
+        :value-face (if (or (= (1+ i) (string-to-number (car arrows)))
+                            (= (1+ i) (string-to-number (cadr arrows))))
+                        'hiddenquote-quote-value
+                      'hiddenquote-value))
+       children)
+      (unless (eql i (1- (widget-get widget :hiddenquote-word-length)))
+        (hiddenquote-insert " " 'hiddenquote-sep)))
+    (add-text-properties opoint (point)
+                         `(cursor-sensor-functions
+                           ,(list
+                             (lambda (_window _prev action)
+                               (if (eq action 'entered)
+                                   (hiddenquote-highlight-definition
+                                    (widget-get widget
+                                                :hiddenquote-word-number))
+                                 (hiddenquote-unhighlight-definition
+                                  (widget-get widget
+                                              :hiddenquote-word-number)))))))
+    (widget-put widget :children (reverse children))))
+
+(defun hiddenquote-word-value-get (widget)
+  "Return `hiddenquote-word' widget WIDGET's value."
+  (apply #'string (mapcar #'widget-value (widget-get widget :children))))
+
+(defun hiddenquote-word-value-set (widget value)
+  "Set the value of `hiddenquote-word' widget WIDGET to VALUE."
+  (let ((children (widget-get widget :children)))
+    (if (nthcdr (1- (length value)) children)
+        (let ((i 0))
+          (dolist (child children)
+            (widget-value-set child (aref value i))
+            (setq i (1+ i))))
+      (error "Expected string of %d characters" (length children)))))
+
+(defun hiddenquote-after-change (from to _old)
+  "Compatibility `after-change-function' for Emacs < 28.
+Starting from Emacs 28.1, `widget-after-change' passes a fake event to the
+field :notify function.  We use that fake event to figure out what to do.
+
+Notify the widget between FROM and TO about a change."
+  (let ((field (widget-field-find from))
+        (other (widget-field-find to)))
+    (when field
+      (unless (eq field other)
+        (error "Change in different fields"))
+      (widget-apply field :notify field (list 'after-change from to)))))
+
+(defun hiddenquote-word-notify (widget child event)
+  "Notify the `hiddenquote-word' widget WIDGET about a change in CHILD.
+
+If the car of EVENT is `before-change', this function just calls
+`widget-default-notify'.  If the of EVENT is `after-change', then advance
+point to some other widget and maybe check the answer."
+  ;; When CHILD is the last WIDGET's children, go to the first child.
+  (when (and (eq (car-safe event) 'after-change)
+             (not (eql (nth 1 event) (nth 2 event))))
+    (if (eq child (car (last (widget-get widget :children))))
+        (goto-char (overlay-start
+                    (widget-get (car (widget-get widget :children))
+                                :field-overlay)))
+      (widget-forward 1)))
+  (when (and (eq (car-safe event) 'after-change)
+             (or hiddenquote-automatic-check
+                 (not
+                  (eq (widget-value
+                       (nth (widget-get widget :hiddenquote-word-number)
+                            (widget-get (widget-get widget :parent) :buttons)))
+                      'unchecked))))
+    (hiddenquote-check-answer))
+  (widget-default-notify widget child event))
+
+(defun hiddenquote-word-validate (widget)
+  "Nil if current value of hiddenquote-word WIDGET is correct."
+  (let ((val (widget-value widget))
+        (puzzle (widget-get (widget-get widget :parent) :hiddenquote)))
+    (unless (string-collate-equalp
+             val
+             (car (aref (oref puzzle clues)
+                        (widget-get widget :hiddenquote-word-number)))
+             nil t)
+      widget)))
+
+(defun hiddenquote-character-notify (widget child &optional event)
+  "Replace characters in character widget WIDGET, so its size is always 1.
+Checks that the car of EVENT is `after-change'.
+
+Calls `widget-default-notify' with WIDGET, CHILD and EVENT as args."
+  (let ((start (nth 1 event))
+        (end (nth 2 event))
+        (parent (widget-get widget :parent))
+        val)
+    (when (eq (car-safe event) 'after-change)
+      (if (eql start end)
+          ;; Hack! Restore the cursor-sensor-functions property here,
+          ;; because `widget-value-set' drops it.
+          (add-text-properties (1- (widget-get parent :from))
+                               (widget-get parent :to)
+                               `(cursor-sensor-functions
+                                 ,(list
+                                   (lambda (_window _prev action)
+                                     (if (eq action 'entered)
+                                         (hiddenquote-highlight-definition
+                                          (widget-get
+                                           parent
+                                           :hiddenquote-word-number))
+                                       (hiddenquote-unhighlight-definition
+                                        (widget-get
+                                         parent
+                                         :hiddenquote-word-number)))))))
+        (setq val (buffer-substring-no-properties start (1+ start)));end))
+        (widget-value-set widget (if hiddenquote-upcase-chars
+                                     (upcase val)
+                                   val))
+        ;; Hack! Restore the cursor-sensor-functions property here,
+        ;; because `widget-value-set' drops it.
+        (add-text-properties (1- (widget-get parent :from))
+                             (widget-get parent :to)
+                             `(cursor-sensor-functions
+                               ,(list
+                                 (lambda (_window _prev action)
+                                   (if (eq action 'entered)
+                                       (hiddenquote-highlight-definition
+                                        (widget-get
+                                         parent
+                                         :hiddenquote-word-number))
+                                     (hiddenquote-unhighlight-definition
+                                      (widget-get
+                                       parent
+                                       :hiddenquote-word-number)))))))))
+    (widget-default-notify widget child event)))
+
+(defun hiddenquote-number-action (widget &optional _event)
+  "Check the answer for the word number that widget WIDGET belongs to."
+  (let ((word (nth (widget-get widget :hiddenquote-word-number)
+                   (widget-get (widget-get widget :parent) :children)))
+        (inhibit-modification-hooks t))
+    (if (string-match " " (widget-value word))
+        (widget-value-set widget 'unchecked)
+      (widget-value-set widget (if (widget-apply word :validate)
+                                   'wrong
+                                 'right)))
+    ;; Hack! Restore the cursor-sensor-functions property here,
+    ;; because `widget-value-set' drops it.
+    (add-text-properties (widget-get widget :from)
+                         (widget-get widget :to)
+                         `(cursor-sensor-functions
+                           ,(list
+                             (lambda (_window _prev action)
+                               (if (eq action 'left)
+                                   (hiddenquote-unhighlight-definition
+                                    (widget-get widget
+                                                :hiddenquote-word-number))
+                                 (hiddenquote-highlight-definition
+                                  (widget-get widget
+                                              :hiddenquote-word-number))
+                                 (widget-forward 1))))))))
+
+(defun hiddenquote-number-button-face-get (widget)
+  "Return the face to use by the `hiddenquote-number' widget WIDGET."
+  (let ((val (widget-value widget)))
+    (cond ((eq val 'unchecked) 'hiddenquote-word-number)
+          ((eq val 'right) 'hiddenquote-word-right)
+          ((eq val 'wrong) 'hiddenquote-word-wrong)
+          ;; Shouldn't happen.
+          (t 'hiddenquote-word-number))))
+
+(defun hiddenquote-timer-update (widget)
+  "Update `hiddenquote-timer' widget WIDGET."
+  (condition-case nil
+      (with-current-buffer (widget-get widget :hiddenquote-buffer)
+        (save-excursion
+          (goto-char (widget-get widget :from))
+          (let ((elapsed (time-since (widget-get widget :hiddenquote-start))))
+            (setq elapsed (if (fboundp 'time-convert)
+                              (time-convert elapsed 'integer)
+                            (+ (* 65536 (nth 0 elapsed))
+                               (nth 1 elapsed)
+                               (* (nth 2 elapsed) 1e-6)
+                               (* (nth 3 elapsed) 1e-12))))
+            (widget-value-set widget elapsed)
+            (oset (oref (widget-get hiddenquote-current :hiddenquote) saved)
+                  elapsed-time elapsed))))
+    (error (hiddenquote-timer-stop-timer))))
+
+(defun hiddenquote-timer-value-create (widget)
+  "Create the `hiddenquote-timer' widget WIDGET."
+  (let* ((startedp (widget-get widget :hiddenquote-start))
+         (elapsed (if startedp
+                      (widget-get widget :value)
+                    (if (fboundp 'time-convert) ; Emacs 27.1
+                        (time-convert
+                         (widget-get widget :hiddenquote-start-value)
+                         'integer)
+                      (widget-get widget :hiddenquote-start-value)))))
+    (with-current-buffer (widget-get widget :hiddenquote-buffer)
+      (hiddenquote-insert (format-seconds "%h:%m:%s"
+                                           (if (numberp elapsed)
+                                               ;; Probably in Emacs 27.1
+                                               elapsed
+                                             ;; Probably in Emacs < 27, so
+                                             ;; compute it by hand.
+                                             (+ (* 65536 (nth 0 elapsed))
+                                                (nth 1 elapsed)
+                                                (* (nth 2 elapsed) 1e-6)
+                                                (* (nth 3 elapsed) 1e-12))))
+                           (widget-apply widget :sample-face-get)))))
+
+(defun hiddenquote-syllable-value-create (widget)
+  "Create the `hiddenquote-syllable' widget WIDGET."
+  (let ((val (widget-value widget))
+        (text (widget-get widget :tag)))
+    (hiddenquote-insert text (if val
+                                  'hiddenquote-used-syllable
+                                'hiddenquote-unused-syllable))))
+
+(defun hiddenquote-syllable-button-face-get (widget)
+  "Return the face to use by the `hiddenquote-syllable' widget WIDGET.
+Return `hiddenquote-used-syllable' if WIDGET's value is non-nil,
+`hiddenquote-unused-syllable' otherwise."
+  (if (widget-value widget)
+      'hiddenquote-used-syllable
+    'hiddenquote-unused-syllable))
+
+(defun hiddenquote-syllable-notify (_widget _child &optional _event)
+  "Check if all syllables are marked as used."
+  (when (hiddenquote-puzzle-complete-p)
+    (hiddenquote-timer-stop-timer)))
+
+;; Functions.
+(defun hiddenquote-puzzle-from-ipuz (ipuz)
+  "Return a `hiddenquote-hidden-quote-puzzle' instance specified by IPUZ."
+  (let* ((json (json-read-from-string ipuz))
+         (num (alist-get 'uniqueid json))
+         (author (alist-get 'author json))
+         (editor (alist-get 'editor json))
+         (publisher (alist-get 'publisher json))
+         (copyright (alist-get 'copyright json))
+         (title (alist-get 'title json))
+         (created-date (alist-get 'date json))
+         (desc (alist-get 'intro json))
+         (qquote (alist-get 'answer json))
+         (clues (alist-get 'Clues (alist-get 'clues json)))
+         (words (vconcat
+                 (cl-mapcar
+                  (lambda (clue)
+                    (list (alist-get 'answer clue) (alist-get 'clue clue)))
+                  clues)))
+         (arrows (alist-get 'com.hiddenquote:arrows json))
+         (lengths (vconcat (cl-mapcar (lambda (clue)
+                                        (alist-get 'enumeration clue))
+                                      clues)))
+         (syllables (alist-get 'com.hiddenquote:syllables json))
+         (saved (let ((spec (alist-get 'saved json)))
+                  (make-instance
+                   'hiddenquote-edit
+                   :answers (alist-get 'answers spec)
+                   :checked-answers (append (alist-get 'checked-answers spec)
+                                            nil)
+                   :used-syllables (append (alist-get 'used-syllables spec)
+                                           nil)
+                   :elapsed-time (or (alist-get 'elapsed-time spec) 0)))))
+    (make-instance 'hiddenquote-hidden-quote-puzzle
+                   :id (string-to-number num)
+                   :author (or author "")
+                   :editor (or editor "")
+                   :copyright (or copyright "")
+                   :publisher (or publisher "")
+                   :quote qquote
+                   :description desc
+                   :title title
+                   :clues words
+                   :arrows arrows
+                   :created-date created-date
+                   :field-lengths lengths
+                   :syllables syllables
+                   :saved saved)))
+
+(defun hiddenquote-get-local-puzzle (&optional n)
+  "Return a puzzle from this package `puzzles' directory.
+With N non-nil, return that puzzle, otherwise return the newest one."
+  (let* ((num (and n (read-number "Enter a puzzle number: ")))
+         (dir (file-name-directory (locate-library "hiddenquote")))
+         (file (if num
+                   (expand-file-name (format "%s.ipuz" num)
+                                     (file-name-as-directory
+                                      (expand-file-name "puzzles" dir)))
+                 (let* ((files
+                         (directory-files
+                          (expand-file-name "puzzles"
+                                            (file-name-directory
+                                             (locate-library "hiddenquote")))
+                          t "[0-9]+.ipuz$" t))
+                        (nums
+                         (sort (mapcar
+                                (lambda (filename)
+                                 (string-match "\\([0-9]+\\).ipuz$" filename)
+                                 (string-to-number
+                                   (match-string 1 filename)))
+                                files)
+                               #'>)))
+                   (expand-file-name (format "%s.ipuz" (car nums))
+                                     (file-name-as-directory
+                                      (expand-file-name "puzzles" dir))))))
+         (saved-file (hiddenquote-expand-puzzle-file-name
+                      (file-name-nondirectory file) "hidden-quote"))
+         puzzle)
+    (setq puzzle (hiddenquote-puzzle-from-ipuz
+                  (with-current-buffer
+                      (find-file-noselect (if (file-exists-p saved-file)
+                                              saved-file
+                                            file)
+                                          t)
+                    (prog1 (buffer-string)
+                      (kill-buffer)))))
+    puzzle))
+                           
+(defun hiddenquote-get-hidden-quote-puzzle (&optional n)
+  "Return a puzzle from the hidden-quote puzzle source.
+
+With N nil, return the latest puzzle.  With N non-nil, return that
+puzzle Nº."
+  (let* ((num (and n (read-number "Enter a puzzle number: ")))
+         (url "http://mauroaranda.com/puzzles/hidden-quote-puzzle/";)
+         (file (and num (hiddenquote-expand-puzzle-file-name
+                         (format "%s.ipuz" num) "hidden-quote")))
+         puzzle)
+    (if (and file (file-exists-p file)) ; Look for the file locally first.
+        (progn
+          (setq puzzle
+                (hiddenquote-puzzle-from-ipuz
+                 (with-current-buffer (find-file-noselect file t)
+                   (prog1 (buffer-string)
+                     (kill-buffer)))))
+          (oset puzzle file file)
+          puzzle)
+      (and num (setq url (concat url (number-to-string num))))
+      (with-current-buffer (url-retrieve-synchronously url)
+        (goto-char (point-min))
+        (re-search-forward "\n\n")
+        (setq puzzle (hiddenquote-puzzle-from-ipuz
+                      (buffer-substring (point) (point-max))))
+        (oset puzzle file (hiddenquote-expand-puzzle-file-name
+                           (format "%s.ipuz" (oref puzzle id)) "hidden-quote"))
+        (oset puzzle saved (hiddenquote-load-saved-puzzle puzzle))
+        puzzle))))
+
+(defun hiddenquote-highlight-definition (n)
+  "Highlight the Nth definition."
+  (with-current-buffer (get-buffer (concat (buffer-name) " - Definitions"))
+    (let ((inhibit-read-only t))
+      (set-text-properties (car (aref hiddenquote-definition-markers n))
+                           (cdr (aref hiddenquote-definition-markers n))
+                           '(face hiddenquote-highlight))
+      (when-let ((w (get-buffer-window (current-buffer))))
+        (set-window-point w (car (aref hiddenquote-definition-markers n)))
+        (or (pos-visible-in-window-p
+             (cdr (aref hiddenquote-definition-markers n)) w)
+            (with-selected-window w
+              (recenter)))))))
+
+(defun hiddenquote-unhighlight-definition (n)
+  "Unhighlight the Nth definition."
+  (with-current-buffer (get-buffer (concat (buffer-name) " - Definitions"))
+    (let ((inhibit-read-only t))
+      (set-text-properties (car (aref hiddenquote-definition-markers n))
+                           (cdr (aref hiddenquote-definition-markers n))
+                           '(face default)))))
+
+(defun hiddenquote-timer-start-timer (widget)
+  "Start the `hiddenquote-timer' WIDGET."
+  (unless (widget-get widget :hiddenquote-start) ; Alreay started.
+    (let ((elapsed (if (fboundp 'time-convert) ; Emacs 27.1
+                       (time-convert
+                        (widget-get widget :hiddenquote-start-value)
+                        'integer)
+                     (widget-get widget :hiddenquote-start-value)))
+          (timer (run-with-timer 1 1 (lambda ()
+                                       (hiddenquote-timer-update widget)))))
+      (widget-put widget :hiddenquote-start
+                  (time-subtract (current-time) elapsed))
+      (widget-put widget :hiddenquote-timer timer)
+      (add-hook 'kill-buffer-hook #'hiddenquote-timer-stop-timer nil t))))
+
+(defun hiddenquote-timer-stop-timer ()
+  "Stop the hiddenquote timer."
+  (when hiddenquote-buffer
+    (with-current-buffer hiddenquote-buffer
+      (let ((timer (widget-get
+                    (widget-get hiddenquote-current :hiddenquote-timer)
+                    :hiddenquote-timer)))
+        (when (timerp timer)
+          (cancel-timer timer))))))
+
+(defun hiddenquote-puzzle-complete-p ()
+  "Non-nil if the grid is complete."
+  (when-let ((used-all-p (with-current-buffer
+                             (concat (buffer-name hiddenquote-buffer)
+                                     " - Syllables")
+                           (cl-every #'widget-value hiddenquote-syllables))))
+    (with-current-buffer hiddenquote-buffer
+      (cl-notany (lambda (w) (widget-apply w :validate))
+                 (widget-get hiddenquote-current :children)))))
+
+(defun hiddenquote-initialize ()
+  "Initialize variables and modes needed by `hiddenquote'."
+  (erase-buffer)
+  (remove-overlays)
+  (setq hiddenquote-previous-window-configuration
+        (current-window-configuration))
+  (delete-other-windows)
+  (cursor-sensor-mode 1)
+  (setq widget-documentation-face 'hiddenquote-doc)
+  (setq truncate-lines t)
+  ;; Compatibility.
+  (when (< emacs-major-version 28)
+    (add-hook 'after-change-functions #'hiddenquote-after-change nil t)
+    (add-hook 'widget-backward-hook #'hiddenquote-widget-backward nil t)))
+
+;; Hiddenquote mode.
+(easy-menu-define hiddenquote-menu (list hiddenquote-mode-map
+                                          hiddenquote-character-map)
+  "Menu for hiddenquote."
+  '("Hiddenquote"
+    ["Automatic checking" hiddenquote-toggle-automatic-check
+     :style radio :selected hiddenquote-automatic-check]
+    ["Manual checking" hiddenquote-toggle-automatic-check
+     :style radio :selected (not hiddenquote-automatic-check)]
+    ["Give up" hiddenquote-give-up]
+    ["Save progress" hiddenquote-save]
+    ["Quit" hiddenquote-quit]))
+
+(easy-menu-define hiddenquote-character-menu hiddenquote-character-map
+  "Menu for hiddenquote, when inside a character cell."
+  '("Hiddenquote"
+    ["Check answer" hiddenquote-check-answer]))
+
+(defvar hiddenquote-tool-bar-map
+  (let ((map (make-sparse-keymap)))
+    (tool-bar-local-item-from-menu #'hiddenquote-give-up "cancel" map
+                                   hiddenquote-mode-map)
+    (tool-bar-local-item-from-menu #'hiddenquote-save "save" map
+                                   hiddenquote-mode-map)
+    (tool-bar-local-item-from-menu #'hiddenquote-quit "exit" map
+                                   hiddenquote-mode-map)
+    (define-key-after map [sep] menu-bar-separator)
+    (tool-bar-local-item-from-menu #'hiddenquote-check-answer "search" map
+                                   hiddenquote-character-map)
+    map))
+
+(define-derived-mode hiddenquote-mode nil "Hiddenquote"
+  "Major mode for `hiddenquote'.
+
+Buffer bindings:
+\\{hiddenquote-mode-map}
+
+Character cell bindings:
+\\{hiddenquote-character-map}."
+  (when (boundp 'tool-bar-map)
+    (setq-local tool-bar-map hiddenquote-tool-bar-map)))
+
+;; Commands.
+(defun hiddenquote-kill-word ()
+  "Starting at point, delete the rest of the characters of the current word."
+  (interactive)
+  (let* ((child (widget-at))
+         (parent (widget-get (widget-at) :parent)))
+    (save-excursion
+      (dolist (w (member child (widget-get parent :children)))
+        (widget-value-set w "")))))
+
+(defun hiddenquote-kill-whole-word ()
+  "Delete each character in a word."
+  (interactive)
+  (let ((parent (widget-get (widget-at) :parent)))
+    (save-excursion
+      (dolist (child (widget-get parent :children))
+        (widget-value-set child "")))))
+
+(defun hiddenquote-delete-backward-char ()
+  "Delete the previous character."
+  (interactive)
+  (widget-backward 1)
+  (unless (eq (widget-type (widget-at)) 'hiddenquote-character)
+    (widget-forward 1))
+  (delete-char 1))
+
+(defun hiddenquote-forward ()
+  "Go to the next character if at a widget, use `forward-char' otherwise."
+  (interactive)
+  (if (widget-at)
+      (progn
+        (widget-forward 1)
+        (while (not (eq (widget-type (widget-at)) 'hiddenquote-character))
+          (widget-forward 1)))
+    (forward-char)))
+
+(defun hiddenquote-backward ()
+  "Go to the previous character if at a widget, use `backward-char' otherwise."
+  (interactive)
+  (if (widget-at)
+      (progn
+        (widget-backward 1)
+        (while (not (eq (widget-type (widget-at)) 'hiddenquote-character))
+          (widget-backward 1)))
+    (backward-char)))
+
+(defun hiddenquote-next ()
+  "Go to the next word if at a widget, to the next line otherwise."
+  (interactive)
+  (if (widget-at)
+      (progn
+        (forward-line)
+        (beginning-of-line)
+        (widget-forward 1)
+        (while (not (eq (widget-type (widget-at)) 'hiddenquote-character))
+          (widget-forward 1)))
+    (forward-line)))
+
+(defun hiddenquote-prev ()
+  "Go to the previous word if at a widget, to the previous line otherwise."
+  (interactive)
+  (if (widget-at)
+      (progn
+        (forward-line -2)
+        (beginning-of-line)
+        (widget-forward 1)
+        (while (not (eq (widget-type (widget-at)) 'hiddenquote-character))
+          (widget-forward 1)))
+    (forward-line -1)))
+
+(defun hiddenquote-move-beginning-of-word ()
+  "Go to the first character of the word point is at."
+  (interactive)
+  (forward-line 0)
+  (widget-forward 1))
+
+(defun hiddenquote-move-end-of-word ()
+  "Go to the last character of the word point is at."
+  (interactive)
+  (let ((inhibit-field-text-motion t))
+    (end-of-line)
+    (widget-backward 1)))
+
+(defun hiddenquote-goto-word (n)
+  "Go to the Nth word."
+  (interactive (list (read-number "Goto word: ")))
+  (if (or (>= (1- n) (length (widget-get hiddenquote-current :buttons)))
+          (< (1- n) 0))
+      (user-error "Invalid word number")
+    (goto-char (widget-get
+                (nth (1- n) (widget-get hiddenquote-current :buttons))
+                :from))))
+
+(defun hiddenquote-forward-syllable ()
+  "Move N syllables forward."
+  (interactive)
+  (if hiddenquote-skip-used-syllables
+      (let ((orig (widget-at)))
+        (widget-forward 1)
+        (while (and (widget-get (widget-at) :value)
+                    (not (eq orig (widget-at))))
+          (widget-forward 1)))
+    (widget-forward 1)))
+
+(defun hiddenquote-backward-syllable ()
+  "Move N syllables backward."
+  (interactive)
+  (if hiddenquote-skip-used-syllables
+      (let ((orig (widget-at)))
+        (widget-backward 1)
+        (while (and (widget-get (widget-at) :value)
+                    (not (eq orig (widget-at))))
+          (widget-backward 1)))
+    (widget-backward 1)))
+
+(defun hiddenquote-check-answer ()
+  "Check if the answer for the word point is at is right or wrong."
+  (interactive)
+  (let ((parent (widget-get (widget-at) :parent)))
+    (widget-apply-action (nth (widget-get parent :hiddenquote-word-number)
+                              (widget-get (widget-get parent :parent)
+                                          :buttons)))))
+
+(defun hiddenquote-toggle-automatic-check ()
+  "Toggle the `hiddenquote-automatic-check' variable."
+  (interactive)
+  (customize-set-variable 'hiddenquote-automatic-check
+                          (not hiddenquote-automatic-check)))
+
+(defun hiddenquote-save ()
+  "Save the puzzle and the user progress."
+  (interactive)
+  (let* ((puzzle (widget-get hiddenquote-current :hiddenquote))
+         (user-answers (vconcat
+                        (mapcar #'widget-value
+                                (widget-get hiddenquote-current :children))))
+         (checked-answers (mapcar #'widget-value
+                                  (widget-get hiddenquote-current :buttons)))
+         (used-syllables
+          (with-current-buffer (concat (buffer-name) " - Syllables")
+            (cl-loop for widget in hiddenquote-syllables
+                     for i = 0 then (1+ i)
+                     when (widget-value widget)
+                     collect i)))
+         (elapsed-time
+          (when (widget-get hiddenquote-current :hiddenquote-timer)
+            (widget-value
+             (widget-get hiddenquote-current :hiddenquote-timer))))
+         (edits (make-instance 'hiddenquote-edit
+                               :answers user-answers
+                               :checked-answers checked-answers
+                               :used-syllables used-syllables
+                               :elapsed-time elapsed-time)))
+    (oset puzzle saved edits)
+    (with-temp-buffer
+      (insert (hiddenquote-puzzle-to-ipuz puzzle))
+      (hiddenquote-ensure-file-exists (oref puzzle file))
+      (write-file (oref puzzle file)))
+    (message "Saved puzzle in %s" (oref puzzle file))))
+
+(defun hiddenquote-give-up ()
+  "Give up completing the puzzle, showing the solutions."
+  (interactive)
+  (when (yes-or-no-p "Really give up? ")
+    (save-excursion
+      (with-current-buffer (concat
+                            (buffer-name hiddenquote-buffer) " - Syllables")
+        (dolist (syllable hiddenquote-syllables)
+          (widget-value-set syllable t)))
+      (let ((i 0)
+            (grid hiddenquote-current))
+        (dolist (child (widget-get grid :children))
+          (widget-value-set
+           child (car (aref (oref (widget-get grid :hiddenquote) clues) i)))
+          (setq i (1+ i))))
+      (hiddenquote-timer-stop-timer))))
+
+(defun hiddenquote-quit ()
+  "Prompt the user and kill the buffer if the answer is \"yes\"."
+  (interactive)
+  (when (yes-or-no-p "Really quit playing Hiddenquote? ")
+    (hiddenquote-timer-stop-timer)
+    (let ((name (buffer-name)))
+      (dolist (buff (mapcar (lambda (suffix)
+                              (concat name suffix))
+                            '(" - Definitions" " - Syllables")))
+        (when (get-buffer buff)
+          (with-current-buffer buff
+            (setq-local kill-buffer-hook nil)
+            (kill-buffer))))
+      (kill-buffer))
+    (set-window-configuration hiddenquote-previous-window-configuration)))
+
+;;;###autoload
+(defun hiddenquote (&optional arg)
+  "Start playing a Hidden Quote puzzle.
+
+If there's only one element in `hiddenquote-sources', choose that source.
+Else, prompt the user with the options in `hiddenquote-sources',
+to get the puzzle to play.
+ARG is passed along to the function that will get the puzzle.  To find out
+how ARG is handled, read the documentation for the publisher function in
+`hiddenquote-sources'.
+
+Returns the buffer with the puzzle."
+  (interactive "P")
+  (let* ((source-fn (if (nthcdr 1 hiddenquote-sources)
+                        (widget-choose "Choose a source"
+                                       (mapcar (lambda (source)
+                                                 (cons (nth 0 source)
+                                                       (nth 2 source)))
+                                               hiddenquote-sources))
+                      (nth 2 (car hiddenquote-sources))))
+         (puzzle (funcall source-fn arg))
+         (buff (get-buffer-create (hiddenquote-format-buffer-name puzzle))))
+    (switch-to-buffer buff)
+    (hiddenquote-mode)
+    (hiddenquote-initialize)
+    ;; Local variables after initializing, otherwise they get killed.
+    (setq hiddenquote-buffer buff)
+    (setq hiddenquote-current (hiddenquote-create-grid puzzle))
+    ;; Wrap up.
+    (widget-setup)
+    (current-buffer)))
+
+(provide 'hiddenquote)
+;;; hiddenquote.el ends here
diff --git a/puzzles/1.ipuz b/puzzles/1.ipuz
new file mode 100644
index 0000000..30e2728
--- /dev/null
+++ b/puzzles/1.ipuz
@@ -0,0 +1,947 @@
+{
+  "version": "http://ipuz.org/v2";,
+  "kind": [
+    "http://mauroaranda.com/puzzles/hiddenquote#1";,
+    "http://ipuz.org/crossword#1";
+  ],
+  "copyright": "Copyright (C) 2021 tbb",
+  "publisher": "MauroAranda",
+  "url": "http://mauroaranda.com/puzzles/hidden-quote-puzzle/1";,
+  "uniqueid": "1",
+  "title": "Hidden Puzzle Nº1",
+  "author": "Mauro Aranda",
+  "editor": "Mauro Aranda",
+  "date": "01/15/2021",
+  "origin": "GNU Emacs",
+  "intro": "Complete the grid to find a quote from Vincent Van Gogh.",
+  "dimensions": {
+    "width": 13,
+    "height": 35
+  },
+  "puzzle": [
+    [
+      "1",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "2",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null
+    ],
+    [
+      "3",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "4",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "5",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "6",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "7",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "8",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "9",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "10",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "11",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "12",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "13",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "14",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "15",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "16",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "17",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "18",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "19",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null
+    ],
+    [
+      "20",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "21",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "22",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "23",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "24",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "25",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "26",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "27",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null
+    ],
+    [
+      "28",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "29",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "30",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "31",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "32",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "33",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "34",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "35",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ]
+  ],
+  "clues": {
+    "Clues": [
+      {
+        "number": 1,
+        "clue": "A man well born; one of good family; one above the condition 
of a yeoman",
+        "answer": "gentleman",
+        "enumeration": 9
+      },
+      {
+        "number": 2,
+        "clue": "A transformation that can be applied to an image to prepare 
it for printing",
+        "answer": "rasterising",
+        "enumeration": 11
+      },
+      {
+        "number": 3,
+        "clue": "To bottle",
+        "answer": "embottle",
+        "enumeration": 8
+      },
+      {
+        "number": 4,
+        "clue": "A muscle which serves to draw a part out, or form the median 
line of the body",
+        "answer": "abductor",
+        "enumeration": 8
+      },
+      {
+        "number": 5,
+        "clue": "(Geom.) A plane four-sided figure, having two sides parallel 
to each other",
+        "answer": "trapezoid",
+        "enumeration": 9
+      },
+      {
+        "number": 6,
+        "clue": "A salt of tartaric acid",
+        "answer": "tartrate",
+        "enumeration": 8
+      },
+      {
+        "number": 7,
+        "clue": "Without a helm or rudder",
+        "answer": "helmless",
+        "enumeration": 8
+      },
+      {
+        "number": 8,
+        "clue": "The act of including, or the state of being included",
+        "answer": "inclusion",
+        "enumeration": 9
+      },
+      {
+        "number": 9,
+        "clue": "Full of neglect, careless",
+        "answer": "neglectful",
+        "enumeration": 10
+      },
+      {
+        "number": 10,
+        "clue": "A musician who plays the guitar",
+        "answer": "guitarist",
+        "enumeration": 9
+      },
+      {
+        "number": 11,
+        "clue": "The opinions and maxims of the Stoics",
+        "answer": "stoicism",
+        "enumeration": 8
+      },
+      {
+        "number": 12,
+        "clue": "The palace of the Moorish kings at Granada",
+        "answer": "alhambra",
+        "enumeration": 8
+      },
+      {
+        "number": 13,
+        "clue": "A fall or descent of rain",
+        "answer": "rainfall",
+        "enumeration": 8
+      },
+      {
+        "number": 14,
+        "clue": "Outward, exterior",
+        "answer": "external",
+        "enumeration": 8
+      },
+      {
+        "number": 15,
+        "clue": "Next in order after the eighty-ninth",
+        "answer": "ninetieth",
+        "enumeration": 9
+      },
+      {
+        "number": 16,
+        "clue": "During the night",
+        "answer": "overnight",
+        "enumeration": 9
+      },
+      {
+        "number": 17,
+        "clue": "The act of forming into a table or tables",
+        "answer": "tabluation",
+        "enumeration": 10
+      },
+      {
+        "number": 18,
+        "clue": "The state of becoming gradually less",
+        "answer": "decrement",
+        "enumeration": 9
+      },
+      {
+        "number": 19,
+        "clue": "An autographic record made by an oscillograph",
+        "answer": "oscillogram",
+        "enumeration": 11
+      },
+      {
+        "number": 20,
+        "clue": "The point between the north and east, at an equal distance 
from each",
+        "answer": "northeast",
+        "enumeration": 9
+      },
+      {
+        "number": 21,
+        "clue": "Capable of being expanded or spread out widely",
+        "answer": "expansible",
+        "enumeration": 10
+      },
+      {
+        "number": 22,
+        "clue": "A U.S. city, in Maryland",
+        "answer": "Baltimore",
+        "enumeration": 9
+      },
+      {
+        "number": 23,
+        "clue": "Orangeroot; An American ranunculaceous plant, having a yellow 
tuberous root",
+        "answer": "yellowroot",
+        "enumeration": 10
+      },
+      {
+        "number": 24,
+        "clue": "Of or pertaining to one only",
+        "answer": "individual",
+        "enumeration": 10
+      },
+      {
+        "number": 25,
+        "clue": "Long and tedious talk without much substance; superfluity of 
words",
+        "answer": "macrology",
+        "enumeration": 9
+      },
+      {
+        "number": 26,
+        "clue": "Somewhat purple",
+        "answer": "purplish",
+        "enumeration": 8
+      },
+      {
+        "number": 27,
+        "clue": "Not used; not habituated; unfamiliar",
+        "answer": "unaccostumed",
+        "enumeration": 12
+      },
+      {
+        "number": 28,
+        "clue": "An edifice or place full of intricate passageways which 
render it difficult to find the way from the interior to the entrance",
+        "answer": "labyrinth",
+        "enumeration": 9
+      },
+      {
+        "number": 29,
+        "clue": "Cane sugar",
+        "answer": "saccharose",
+        "enumeration": 10
+      },
+      {
+        "number": 30,
+        "clue": "To give courage to",
+        "answer": "encourage",
+        "enumeration": 9
+      },
+      {
+        "number": 31,
+        "clue": "Cooked on a barbecue",
+        "answer": "barbecued",
+        "enumeration": 9
+      },
+      {
+        "number": 32,
+        "clue": "One who pushes a principle or measure to extremes; an 
extremist",
+        "answer": "ultraist",
+        "enumeration": 8
+      },
+      {
+        "number": 33,
+        "clue": "Pain in a tooth or in the teeth",
+        "answer": "toothache",
+        "enumeration": 9
+      },
+      {
+        "number": 34,
+        "clue": "Firmness; moral principle; steadfastness",
+        "answer": "backbone",
+        "enumeration": 8
+      },
+      {
+        "number": 35,
+        "clue": " - terrier: Very small breed, having a long glossy coat of 
bluish-grey and tan",
+        "answer": "yorkshire",
+        "enumeration": 9
+      }
+    ]
+  },
+  "answer": "Great things are not done by impulse, but by a series of small 
things brought together",
+  "zones": [
+    {
+      "rect": [
+        0,
+        0,
+        0,
+        34
+      ],
+      "style": {
+        "border": 0
+      }
+    },
+    {
+      "rect": [
+        1,
+        0,
+        1,
+        34
+      ],
+      "style": {
+        "highlight": true
+      }
+    },
+    {
+      "rect": [
+        8,
+        0,
+        8,
+        34
+      ],
+      "style": {
+        "highlight": true
+      }
+    }
+  ],
+  "volatile": {
+    "com.hiddenquote:arrows": "",
+    "com.hiddenquote:subject": "",
+    "com.hiddenquote:syllables": ""
+  },
+  "com.hiddenquote:arrows": "1,8",
+  "com.hiddenquote:subject": "General",
+  "com.hiddenquote:syllables": [
+    "ab",
+    "ac",
+    "ache",
+    "age",
+    "al",
+    "al",
+    "back",
+    "bal",
+    "bar",
+    "be",
+    "ble",
+    "bo",
+    "bone",
+    "bra",
+    "cha",
+    "cil",
+    "cism",
+    "clu",
+    "cour",
+    "crol",
+    "cued",
+    "cus",
+    "dec",
+    "di",
+    "duc",
+    "e",
+    "east",
+    "em",
+    "en",
+    "eth",
+    "ex",
+    "ex",
+    "fall",
+    "ful",
+    "gen",
+    "gram",
+    "gui",
+    "gy",
+    "ham",
+    "helm",
+    "i",
+    "in",
+    "in",
+    "ist",
+    "ist",
+    "la",
+    "lab",
+    "lect",
+    "less",
+    "lo",
+    "low",
+    "ma",
+    "man",
+    "ment",
+    "more",
+    "nal",
+    "neg",
+    "night",
+    "nine",
+    "north",
+    "o",
+    "o",
+    "os",
+    "pan",
+    "plish",
+    "pur",
+    "rain",
+    "ras",
+    "re",
+    "ri",
+    "rinth",
+    "root",
+    "rose",
+    "sac",
+    "shire",
+    "si",
+    "sing",
+    "sion",
+    "sto",
+    "tab",
+    "tar",
+    "tar",
+    "te",
+    "ter",
+    "ti",
+    "ti",
+    "tion",
+    "tle",
+    "tle",
+    "tomed",
+    "tooth",
+    "tor",
+    "tra",
+    "trap",
+    "trate",
+    "u",
+    "u",
+    "ul",
+    "un",
+    "ver",
+    "vid",
+    "y",
+    "yel",
+    "york",
+    "zoid"
+  ]
+}
diff --git a/puzzles/2.ipuz b/puzzles/2.ipuz
new file mode 100644
index 0000000..6d970b3
--- /dev/null
+++ b/puzzles/2.ipuz
@@ -0,0 +1,532 @@
+{
+  "version": "http://ipuz.org/v2";,
+  "kind": [
+    "http://mauroaranda.com/puzzles/hiddenquote#1";,
+    "http://ipuz.org/crossword#1";
+  ],
+  "copyright": "Copyright (C) 2021 Mauro Aranda",
+  "publisher": "MauroAranda",
+  "url": "http://mauroaranda.com/puzzles/hidden-quote-puzzle/2";,
+  "uniqueid": "2",
+  "title": "Grid Nº2",
+  "author": "Mauro Aranda",
+  "editor": "Mauro Aranda",
+  "date": "01/17/2021",
+  "origin": "GNU Emacs",
+  "intro": "Find a quote from Ovid.",
+  "dimensions": {
+    "width": 15,
+    "height": 17
+  },
+  "puzzle": [
+    [
+      "1",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "2",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "3",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "4",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "5",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "6",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "7",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null
+    ],
+    [
+      "8",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "9",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "10",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "11",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "12",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "13",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "14",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "15",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "16",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "17",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ]
+  ],
+  "clues": {
+    "Clues": [
+      {
+        "number": 1,
+        "clue": "Loosed from any limitation or condition",
+        "answer": "absolute",
+        "enumeration": 8
+      },
+      {
+        "number": 2,
+        "clue": "Without a name",
+        "answer": "nameless",
+        "enumeration": 8
+      },
+      {
+        "number": 3,
+        "clue": "A former country of southeastern Europe bordering the 
Adriatic Sea",
+        "answer": "yugoslavia",
+        "enumeration": 10
+      },
+      {
+        "number": 4,
+        "clue": "One who teaches or instructs",
+        "answer": "teacher",
+        "enumeration": 7
+      },
+      {
+        "number": 5,
+        "clue": "Of or pertaining to hydraulics, or to fluids in motion",
+        "answer": "hydraulic",
+        "enumeration": 9
+      },
+      {
+        "number": 6,
+        "clue": "The state or quality of being identical, or the same",
+        "answer": "identity",
+        "enumeration": 8
+      },
+      {
+        "number": 7,
+        "clue": "The manipulation or construction of objects with sizes in the 
nanometer range or smaller",
+        "answer": "nanotechnology",
+        "enumeration": 14
+      },
+      {
+        "number": 8,
+        "clue": "A piece of ground appropriated to the cultivation of herbs, 
fruits, flowers, or vegetables",
+        "answer": "garden",
+        "enumeration": 6
+      },
+      {
+        "number": 9,
+        "clue": "An orthopterous insect of the genus Blatta, and allied 
genera",
+        "answer": "cockroach",
+        "enumeration": 9
+      },
+      {
+        "number": 10,
+        "clue": "Violent or riotous behavior",
+        "answer": "rampage",
+        "enumeration": 7
+      },
+      {
+        "number": 11,
+        "clue": "The act of adding two or more things together",
+        "answer": "addition",
+        "enumeration": 8
+      },
+      {
+        "number": 12,
+        "clue": "Someone injured or killed or captured or missing in a 
military engagement",
+        "answer": "casualty",
+        "enumeration": 8
+      },
+      {
+        "number": 13,
+        "clue": "Chemical element, symbol Kr",
+        "answer": "krypton",
+        "enumeration": 7
+      },
+      {
+        "number": 14,
+        "clue": "One employed by another",
+        "answer": "employee",
+        "enumeration": 8
+      },
+      {
+        "number": 15,
+        "clue": "That causes disgust",
+        "answer": "disgusting",
+        "enumeration": 10
+      },
+      {
+        "number": 16,
+        "clue": "A person who cuts wood",
+        "answer": "woodcutter",
+        "enumeration": 10
+      },
+      {
+        "number": 17,
+        "clue": "A man born in Ireland or of the Irish race",
+        "answer": "irishman",
+        "enumeration": 8
+      }
+    ]
+  },
+  "answer": "Anything cracked will shatter at a touch",
+  "zones": [
+    {
+      "rect": [
+        0,
+        0,
+        0,
+        16
+      ],
+      "style": {
+        "border": 0
+      }
+    },
+    {
+      "rect": [
+        1,
+        0,
+        1,
+        16
+      ],
+      "style": {
+        "highlight": true
+      }
+    },
+    {
+      "rect": [
+        5,
+        0,
+        5,
+        16
+      ],
+      "style": {
+        "highlight": true
+      }
+    }
+  ],
+  "volatile": {
+    "com.hiddenquote:arrows": "",
+    "com.hiddenquote:subject": "",
+    "com.hiddenquote:syllables": ""
+  },
+  "com.hiddenquote:arrows": "1,5",
+  "com.hiddenquote:subject": "general",
+  "com.hiddenquote:syllables": [
+    "a",
+    "ab",
+    "ad",
+    "age",
+    "al",
+    "cas",
+    "cock",
+    "cut",
+    "den",
+    "den",
+    "di",
+    "dis",
+    "drau",
+    "ee",
+    "em",
+    "er",
+    "gar",
+    "gos",
+    "gust",
+    "gy",
+    "hy",
+    "i",
+    "i",
+    "ing",
+    "kryp",
+    "la",
+    "less",
+    "lic",
+    "lute",
+    "man",
+    "name",
+    "nan",
+    "nol",
+    "o",
+    "o",
+    "ploy",
+    "ramp",
+    "rish",
+    "roach",
+    "so",
+    "teach",
+    "tech",
+    "ter",
+    "ti",
+    "tion",
+    "ton",
+    "ty",
+    "ty",
+    "u",
+    "vi",
+    "wood",
+    "yu"
+  ]
+}
diff --git a/puzzles/3.ipuz b/puzzles/3.ipuz
new file mode 100644
index 0000000..6abd8cc
--- /dev/null
+++ b/puzzles/3.ipuz
@@ -0,0 +1,798 @@
+{
+  "version": "http://ipuz.org/v2";,
+  "kind": [
+    "http://mauroaranda.com/puzzles/hiddenquote#1";,
+    "http://ipuz.org/crossword#1";
+  ],
+  "copyright": "Copyright (C) 2021 Mauro Aranda",
+  "publisher": "MauroAranda",
+  "url": "http://mauroaranda.com/puzzles/hidden-quote-puzzle/3";,
+  "uniqueid": "3",
+  "title": "Grid Nº3",
+  "author": "Mauro Aranda",
+  "editor": "Mauro Aranda",
+  "date": "01/19/2021",
+  "origin": "GNU Emacs",
+  "intro": "Complete the grid to find a quote from Blaise Pascal",
+  "dimensions": {
+    "width": 14,
+    "height": 28
+  },
+  "puzzle": [
+    [
+      "1",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "2",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null
+    ],
+    [
+      "3",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "4",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "5",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "6",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "7",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "8",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "9",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "10",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "11",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "12",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "13",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "14",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "15",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "16",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "17",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "18",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "19",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "20",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "21",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "22",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "23",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "24",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "25",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "26",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "27",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "28",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ]
+  ],
+  "clues": {
+    "Clues": [
+      {
+        "number": 1,
+        "clue": "(Li) A metallic element of the alkaline group",
+        "answer": "lithium",
+        "enumeration": 7
+      },
+      {
+        "number": 2,
+        "clue": "Mental disorder in which people interpret reality abnormally",
+        "answer": "schizophrenia",
+        "enumeration": 13
+      },
+      {
+        "number": 3,
+        "clue": "A brother by the marriage of one's parent with the parent of 
another",
+        "answer": "stepbrother",
+        "enumeration": 11
+      },
+      {
+        "number": 4,
+        "clue": "A temperature scale",
+        "answer": "celsius",
+        "enumeration": 7
+      },
+      {
+        "number": 5,
+        "clue": "- steroid.  Any of a variety of synthetic deriatives or 
analogs of testoterone, which promote the increase of muscle mass and strength",
+        "answer": "anabolic",
+        "enumeration": 8
+      },
+      {
+        "number": 6,
+        "clue": "To hinder; to withhold",
+        "answer": "abstain",
+        "enumeration": 7
+      },
+      {
+        "number": 7,
+        "clue": "(Massicot) Lead monoxide, used in making flint glass",
+        "answer": "litharge",
+        "enumeration": 8
+      },
+      {
+        "number": 8,
+        "clue": "A fot and moist substance applied externally to some part of 
the body",
+        "answer": "cataplasm",
+        "enumeration": 9
+      },
+      {
+        "number": 9,
+        "clue": "Lessons done in class or assigned to be done at home",
+        "answer": "schoolwork",
+        "enumeration": 10
+      },
+      {
+        "number": 10,
+        "clue": "U.S. city, in Illinois",
+        "answer": "Chicago",
+        "enumeration": 7
+      },
+      {
+        "number": 11,
+        "clue": "An event or accomplishment of great significance",
+        "answer": "landmark",
+        "enumeration": 8
+      },
+      {
+        "number": 12,
+        "clue": "A book in which a log is recorded",
+        "answer": "logbook",
+        "enumeration": 7
+      },
+      {
+        "number": 13,
+        "clue": "Pertaining to the sense of hearing, the organs of hearing, or 
the science of sounds",
+        "answer": "acoustic",
+        "enumeration": 8
+      },
+      {
+        "number": 14,
+        "clue": "The office or power of a censor",
+        "answer": "censorship",
+        "enumeration": 10
+      },
+      {
+        "number": 15,
+        "clue": "The brow above the eye",
+        "answer": "eyebrow",
+        "enumeration": 7
+      },
+      {
+        "number": 16,
+        "clue": "(The -).  One of Francis Ford Coppola movies",
+        "answer": "godfather",
+        "enumeration": 9
+      },
+      {
+        "number": 17,
+        "clue": "A small writing board with a clip attached at the top for 
holding papers",
+        "answer": "clipboard",
+        "enumeration": 9
+      },
+      {
+        "number": 18,
+        "clue": "One of the West Goths",
+        "answer": "visigoth",
+        "enumeration": 8
+      },
+      {
+        "number": 19,
+        "clue": "A cubic volume of ten liters",
+        "answer": "decaliter",
+        "enumeration": 9
+      },
+      {
+        "number": 20,
+        "clue": "The pulpy fruit of a tree of tropical America",
+        "answer": "avocado",
+        "enumeration": 7
+      },
+      {
+        "number": 21,
+        "clue": "One who, or that which, develops",
+        "answer": "developer",
+        "enumeration": 9
+      },
+      {
+        "number": 22,
+        "clue": "A veteran who has honorably completed his service",
+        "answer": "emeritus",
+        "enumeration": 8
+      },
+      {
+        "number": 23,
+        "clue": "One who practices rope dancing, high vaulting, or other 
daring gymnastic feats",
+        "answer": "acrobat",
+        "enumeration": 7
+      },
+      {
+        "number": 24,
+        "clue": "Meritorious",
+        "answer": "desertful",
+        "enumeration": 9
+      },
+      {
+        "number": 25,
+        "clue": "Merely imaginary; fanciful; fantastic",
+        "answer": "chimerical",
+        "enumeration": 10
+      },
+      {
+        "number": 26,
+        "clue": "A large dark-colored variety of the leopard",
+        "answer": "panther",
+        "enumeration": 7
+      },
+      {
+        "number": 27,
+        "clue": "The wife of a duke",
+        "answer": "duchess",
+        "enumeration": 7
+      },
+      {
+        "number": 28,
+        "clue": "An engraving on wood",
+        "answer": "woodcut",
+        "enumeration": 7
+      }
+    ]
+  },
+  "answer": "The last thing one discovers in composing a work is what to put 
first",
+  "zones": [
+    {
+      "rect": [
+        0,
+        0,
+        0,
+        27
+      ],
+      "style": {
+        "border": 0
+      }
+    },
+    {
+      "rect": [
+        3,
+        0,
+        3,
+        27
+      ],
+      "style": {
+        "highlight": true
+      }
+    },
+    {
+      "rect": [
+        7,
+        0,
+        7,
+        27
+      ],
+      "style": {
+        "highlight": true
+      }
+    }
+  ],
+  "volatile": {
+    "com.hiddenquote:arrows": "",
+    "com.hiddenquote:subject": "",
+    "com.hiddenquote:syllables": ""
+  },
+  "com.hiddenquote:arrows": "3,7",
+  "com.hiddenquote:subject": "general",
+  "com.hiddenquote:syllables": [
+    "a",
+    "a",
+    "a",
+    "a",
+    "a",
+    "ab",
+    "ac",
+    "al",
+    "an",
+    "arge",
+    "av",
+    "bat",
+    "board",
+    "bol",
+    "book",
+    "broth",
+    "brow",
+    "ca",
+    "ca",
+    "cat",
+    "cel",
+    "cen",
+    "chi",
+    "chi",
+    "clip",
+    "cous",
+    "cut",
+    "de",
+    "de",
+    "dec",
+    "do",
+    "duch",
+    "e",
+    "er",
+    "er",
+    "ess",
+    "eye",
+    "fa",
+    "ful",
+    "go",
+    "god",
+    "goth",
+    "i",
+    "i",
+    "i",
+    "ic",
+    "ic",
+    "land",
+    "li",
+    "lith",
+    "lith",
+    "log",
+    "mark",
+    "mer",
+    "mer",
+    "ni",
+    "o",
+    "o",
+    "op",
+    "pan",
+    "phre",
+    "plasm",
+    "ro",
+    "schiz",
+    "school",
+    "sert",
+    "ship",
+    "si",
+    "sor",
+    "stain",
+    "step",
+    "ter",
+    "ther",
+    "ther",
+    "tic",
+    "tus",
+    "um",
+    "us",
+    "vel",
+    "vis",
+    "wood",
+    "work"
+  ]
+}
diff --git a/puzzles/4.ipuz b/puzzles/4.ipuz
new file mode 100644
index 0000000..feb5394
--- /dev/null
+++ b/puzzles/4.ipuz
@@ -0,0 +1,532 @@
+{
+  "version": "http://ipuz.org/v2";,
+  "kind": [
+    "http://mauroaranda.com/puzzles/hiddenquote#1";,
+    "http://ipuz.org/crossword#1";
+  ],
+  "copyright": "Copyright (C) 2021 tbb",
+  "publisher": "MauroAranda",
+  "url": "http://mauroaranda.com/puzzles/hidden-quote-puzzle/1";,
+  "uniqueid": "4",
+  "title": "Hidden Puzzle Nº1",
+  "author": "Mauro Aranda",
+  "editor": "Mauro Aranda",
+  "date": "01/26/2021",
+  "origin": "GNU Emacs",
+  "intro": "Complete the grid to read an Aristotle quote",
+  "dimensions": {
+    "width": 11,
+    "height": 20
+  },
+  "puzzle": [
+    [
+      "1",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "2",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "3",
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "4",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "5",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "6",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "7",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "8",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "9",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null
+    ],
+    [
+      "10",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "11",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null
+    ],
+    [
+      "12",
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "13",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "14",
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "15",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null
+    ],
+    [
+      "16",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null
+    ],
+    [
+      "17",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null
+    ],
+    [
+      "18",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null,
+      null
+    ],
+    [
+      "19",
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      0,
+      null
+    ],
+    [
+      "20",
+      0,
+      0,
+      0,
+      0,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      null
+    ]
+  ],
+  "clues": {
+    "Clues": [
+      {
+        "number": 1,
+        "clue": "(Arch.) A low wall, especially one serving to protect the 
edge of a platform, roof, bridge, or the like",
+        "answer": "parapet",
+        "enumeration": 7
+      },
+      {
+        "number": 2,
+        "clue": "Playing on the surface; touching lightly",
+        "answer": "lambent",
+        "enumeration": 7
+      },
+      {
+        "number": 3,
+        "clue": "A lighted coal, smoldering amid ashes",
+        "answer": "ember",
+        "enumeration": 5
+      },
+      {
+        "number": 4,
+        "clue": "Any product of human workmanship",
+        "answer": "artifact",
+        "enumeration": 8
+      },
+      {
+        "number": 5,
+        "clue": "Destitute of sap; not juicy",
+        "answer": "sapless",
+        "enumeration": 7
+      },
+      {
+        "number": 6,
+        "clue": "Inspection or analysis of the urine as a means of diagnosis",
+        "answer": "uroscopy",
+        "enumeration": 8
+      },
+      {
+        "number": 7,
+        "clue": "To obtain again",
+        "answer": "reobtain",
+        "enumeration": 8
+      },
+      {
+        "number": 8,
+        "clue": "To cut with a chisel",
+        "answer": "enchisel",
+        "enumeration": 8
+      },
+      {
+        "number": 9,
+        "clue": "Act of beginning; commencement; inception",
+        "answer": "inchoation",
+        "enumeration": 10
+      },
+      {
+        "number": 10,
+        "clue": "A name given in affectionate familiarity",
+        "answer": "nickname",
+        "enumeration": 8
+      },
+      {
+        "number": 11,
+        "clue": "A large extent or tract of land",
+        "answer": "territory",
+        "enumeration": 9
+      },
+      {
+        "number": 12,
+        "clue": "(- of Troy) The beautiful daughter of Zeus and Leda, who was 
abducted by Paris",
+        "answer": "helen",
+        "enumeration": 5
+      },
+      {
+        "number": 13,
+        "clue": "One of the fundamental subatomic particles, of negative 
charge",
+        "answer": "electron",
+        "enumeration": 8
+      },
+      {
+        "number": 14,
+        "clue": "The Hebrew prophet, who was cast overboard as one who 
endangered the ship",
+        "answer": "jonah",
+        "enumeration": 5
+      },
+      {
+        "number": 15,
+        "clue": "An instrument attached to a vehicle which measures the 
distance traversed",
+        "answer": "odometer",
+        "enumeration": 8
+      },
+      {
+        "number": 16,
+        "clue": "To ornament with a jewel or with jewels",
+        "answer": "bejewel",
+        "enumeration": 7
+      },
+      {
+        "number": 17,
+        "clue": "One of the three geologic eras of the Phanerozoic Eon",
+        "answer": "paleozoic",
+        "enumeration": 9
+      },
+      {
+        "number": 18,
+        "clue": "Not courteous or gracious",
+        "answer": "uncordial",
+        "enumeration": 9
+      },
+      {
+        "number": 19,
+        "clue": "Landlocked country in Central Asia",
+        "answer": "tajikistan",
+        "enumeration": 10
+      },
+      {
+        "number": 20,
+        "clue": "An asiatic leguminous herb, which seeds are used in preparing 
soy",
+        "answer": "soja",
+        "enumeration": 4
+      }
+    ]
+  },
+  "answer": "Pleasure in the job puts perfection in the work.",
+  "zones": [
+    {
+      "rect": [
+        0,
+        0,
+        0,
+        19
+      ],
+      "style": {
+        "border": 0
+      }
+    },
+    {
+      "rect": [
+        1,
+        0,
+        1,
+        19
+      ],
+      "style": {
+        "highlight": true
+      }
+    },
+    {
+      "rect": [
+        5,
+        0,
+        5,
+        19
+      ],
+      "style": {
+        "highlight": true
+      }
+    }
+  ],
+  "volatile": {
+    "com.hiddenquote:arrows": "",
+    "com.hiddenquote:subject": "",
+    "com.hiddenquote:syllables": ""
+  },
+  "com.hiddenquote:arrows": "1,5",
+  "com.hiddenquote:subject": "general",
+  "com.hiddenquote:syllables": [
+    "a",
+    "a",
+    "ar",
+    "be",
+    "bent",
+    "ber",
+    "chis",
+    "cho",
+    "co",
+    "cor",
+    "dial",
+    "dom",
+    "e",
+    "e",
+    "el",
+    "el",
+    "em",
+    "en",
+    "en",
+    "fact",
+    "hel",
+    "i",
+    "ic",
+    "in",
+    "ja",
+    "jew",
+    "jik",
+    "jo",
+    "lam",
+    "le",
+    "lec",
+    "less",
+    "nah",
+    "name",
+    "nick",
+    "o",
+    "o",
+    "ob",
+    "pa",
+    "par",
+    "pet",
+    "py",
+    "re",
+    "ri",
+    "ros",
+    "ry",
+    "sap",
+    "so",
+    "stan",
+    "ta",
+    "tain",
+    "ter",
+    "ter",
+    "ti",
+    "tion",
+    "to",
+    "tron",
+    "u",
+    "un",
+    "zo"
+  ]
+}



reply via email to

[Prev in Thread] Current Thread [Next in Thread]