;;; ein:markdown-mode.el --- Major mode for Markdown-formatted text -*- lexical-binding: t; -*-

;; Copyright (C) 2007-2017 Jason R. Blevins and markdown-mode
;; contributors.

;; Author: Jason R. Blevins <jblevins@xbeta.org>

;; This file is not part of GNU Emacs.

;; This program 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.

;; This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Pare markdown-mode for EIN (and fix some bugs)

;;; Code:

(require 'easymenu)
(require 'outline)
(require 'thingatpt)
(require 'cl-lib)
(require 'url-parse)
(require 'button)
(require 'color)
(require 'rx)

(defvar jit-lock-start)
(defvar jit-lock-end)
(defvar flyspell-generic-check-word-predicate)

(declare-function eww-open-file "eww")
(declare-function url-path-and-query "url-parse")


;;; Constants =================================================================

(defconst ein:markdown-mode-version "2.4-dev"
  "ein:markdown mode version number.")

(defconst ein:markdown-output-buffer-name "*markdown-output*"
  "Name of temporary buffer for markdown command output.")


;;; Global Variables ==========================================================

(defvar ein:markdown-reference-label-history nil
  "History of used reference labels.")

;;; Customizable Variables ====================================================

(defvar ein:markdown-mode-hook nil
  "Hook run when entering Markdown mode.")

(defgroup ein:markdown nil
  "Major mode for editing text files in Markdown format."
  :prefix "ein:markdown-"
  :group 'text)

(defcustom ein:markdown-command "ein:markdown"
  "Command to run markdown."
  :group 'ein:markdown
  :type '(choice (string :tag "Shell command") function))

(defcustom ein:markdown-command-needs-filename nil
  "Set to non-nil if `markdown-command' does not accept input from stdin.
Instead, it will be passed a filename as the final command line
option.  As a result, you will only be able to run Markdown from
buffers which are visiting a file."
  :group 'ein:markdown
  :type 'boolean)

(defcustom ein:markdown-open-command nil
  "Command used for opening Markdown files directly.
For example, a standalone Markdown previewer.  This command will
be called with a single argument: the filename of the current
buffer.  It can also be a function, which will be called without
arguments."
  :group 'ein:markdown
  :type '(choice file function (const :tag "None" nil)))

(defcustom ein:markdown-hr-strings
  '("-------------------------------------------------------------------------------"
    "* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"
    "---------------------------------------"
    "* * * * * * * * * * * * * * * * * * * *"
    "---------"
    "* * * * *")
  "Strings to use when inserting horizontal rules.
The first string in the list will be the default when inserting a
horizontal rule.  Strings should be listed in decreasing order of
prominence (as in headings from level one to six) for use with
promotion and demotion functions."
  :group 'ein:markdown
  :type '(repeat string))

(defcustom ein:markdown-bold-underscore nil
  "Use two underscores when inserting bold text instead of two asterisks."
  :group 'ein:markdown
  :type 'boolean)

(defcustom ein:markdown-italic-underscore nil
  "Use underscores when inserting italic text instead of asterisks."
  :group 'ein:markdown
  :type 'boolean)

(defcustom ein:markdown-marginalize-headers nil
  "When non-nil, put opening atx header markup in a left margin.

This setting goes well with `markdown-asymmetric-header'.  But
sadly it conflicts with `linum-mode' since they both use the
same margin."
  :group 'ein:markdown
  :type 'boolean
  :safe 'booleanp
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-marginalize-headers-margin-width 6
  "Character width of margin used for marginalized headers.
The default value is based on there being six heading levels
defined by Markdown and HTML.  Increasing this produces extra
whitespace on the left.  Decreasing it may be preferred when
fewer than six nested heading levels are used."
  :group 'ein:markdown
  :type 'natnump
  :safe 'natnump
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-asymmetric-header nil
  "Determines if atx header style will be asymmetric.
Set to a non-nil value to use asymmetric header styling, placing
header markup only at the beginning of the line. By default,
balanced markup will be inserted at the beginning and end of the
line around the header title."
  :group 'ein:markdown
  :type 'boolean)

(defcustom ein:markdown-indent-function 'ein:markdown-indent-line
  "Function to use to indent."
  :group 'ein:markdown
  :type 'function)

(defcustom ein:markdown-indent-on-enter t
  "Determines indentation behavior when pressing \\[newline].
Possible settings are nil, t, and \\='indent-and-new-item.

When non-nil, pressing \\[newline] will call `newline-and-indent'
to indent the following line according to the context using
`markdown-indent-function'.  In this case, note that
\\[electric-newline-and-maybe-indent] can still be used to insert
a newline without indentation.

When set to \\='indent-and-new-item and the point is in a list item
when \\[newline] is pressed, the list will be continued on the next
line, where a new item will be inserted.

When set to nil, simply call `newline' as usual.  In this case,
you can still indent lines using \\[ein:markdown-cycle] and continue
lists with \\[ein:markdown-insert-list-item].

Note that this assumes the variable `electric-indent-mode' is
non-nil (enabled).  When it is *disabled*, the behavior of
\\[newline] and `\\[electric-newline-and-maybe-indent]' are
reversed."
  :group 'ein:markdown
  :type '(choice (const :tag "Don't automatically indent" nil)
                 (const :tag "Automatically indent" t)
                 (const :tag "Automatically indent and insert new list items" indent-and-new-item)))

(defcustom ein:markdown-uri-types
  '("acap" "cid" "data" "dav" "fax" "file" "ftp"
    "gopher" "http" "https" "imap" "ldap" "mailto"
    "mid" "message" "modem" "news" "nfs" "nntp"
    "pop" "prospero" "rtsp" "service" "sip" "tel"
    "telnet" "tip" "urn" "vemmi" "wais")
  "Link types for syntax highlighting of URIs."
  :group 'ein:markdown
  :type '(repeat (string :tag "URI scheme")))

(defcustom ein:markdown-url-compose-char
  '(?∞ ?… ?⋯ ?# ?★ ?⚓)
  "Placeholder character for hidden URLs.
This may be a single character or a list of characters. In case
of a list, the first one that satisfies `char-displayable-p' will
be used."
  :type '(choice
          (character :tag "Single URL replacement character")
          (repeat :tag "List of possible URL replacement characters"
                  character))
  :package-version '(ein:markdown-mode . "2.3"))

(defcustom ein:markdown-blockquote-display-char
  '("▌" "┃" ">")
  "String to display when hiding blockquote markup.
This may be a single string or a list of string. In case of a
list, the first one that satisfies `char-displayable-p' will be
used."
  :type 'string
  :type '(choice
          (string :tag "Single blockquote display string")
          (repeat :tag "List of possible blockquote display strings" string))
  :package-version '(ein:markdown-mode . "2.3"))

(defcustom ein:markdown-hr-display-char
  '(?─ ?━ ?-)
  "Character for hiding horizontal rule markup.
This may be a single character or a list of characters.  In case
of a list, the first one that satisfies `char-displayable-p' will
be used."
  :group 'ein:markdown
  :type '(choice
          (character :tag "Single HR display character")
          (repeat :tag "List of possible HR display characters" character))
  :package-version '(ein:markdown-mode . "2.3"))

(defcustom ein:markdown-definition-display-char
  '(?⁘ ?⁙ ?≡ ?⌑ ?◊ ?:)
  "Character for replacing definition list markup.
This may be a single character or a list of characters.  In case
of a list, the first one that satisfies `char-displayable-p' will
be used."
  :type '(choice
          (character :tag "Single definition list character")
          (repeat :tag "List of possible definition list characters" character))
  :package-version '(ein:markdown-mode . "2.3"))

(defcustom ein:markdown-enable-math nil
  "Syntax highlighting for inline LaTeX and itex expressions.
Set this to a non-nil value to turn on math support by default.
Math support can be enabled, disabled, or toggled later using
`markdown-toggle-math' or \\[ein:markdown-toggle-math]."
  :group 'ein:markdown
  :type 'boolean
  :safe 'booleanp)
(make-variable-buffer-local 'ein:markdown-enable-math)

(defcustom ein:markdown-enable-html t
  "Enable font-lock support for HTML tags and attributes."
  :group 'ein:markdown
  :type 'boolean
  :safe 'booleanp
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-css-paths nil
  "URL of CSS file to link to in the output XHTML."
  :group 'ein:markdown
  :type '(repeat (string :tag "CSS File Path")))

(defcustom ein:markdown-content-type "text/html"
  "Content type string for the http-equiv header in XHTML output.
When set to an empty string, this attribute is omitted.  Defaults to
`text/html'."
  :group 'ein:markdown
  :type 'string)

(defcustom ein:markdown-coding-system nil
  "Character set string for the http-equiv header in XHTML output.
Defaults to `buffer-file-coding-system' (and falling back to
`utf-8' when not available).  Common settings are `iso-8859-1'
and `iso-latin-1'.  Use `list-coding-systems' for more choices."
  :group 'ein:markdown
  :type 'coding-system)

(defcustom ein:markdown-xhtml-header-content ""
  "Additional content to include in the XHTML <head> block."
  :group 'ein:markdown
  :type 'string)

(defcustom ein:markdown-xhtml-body-preamble ""
  "Content to include in the XHTML <body> block, before the output."
  :group 'ein:markdown
  :type 'string
  :safe 'stringp
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-xhtml-body-epilogue ""
  "Content to include in the XHTML <body> block, after the output."
  :group 'ein:markdown
  :type 'string
  :safe 'stringp
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-xhtml-standalone-regexp
  "^\\(<\\?xml\\|<!DOCTYPE\\|<html\\)"
  "Regexp indicating whether `markdown-command' output is standalone XHTML."
  :group 'ein:markdown
  :type 'regexp)

(defcustom ein:markdown-link-space-sub-char "_"
  "Character to use instead of spaces when mapping wiki links to filenames."
  :group 'ein:markdown
  :type 'string)

(defcustom ein:markdown-reference-location 'header
  "Position where new reference definitions are inserted in the document."
  :group 'ein:markdown
  :type '(choice (const :tag "At the end of the document" end)
                 (const :tag "Immediately after the current block" immediately)
                 (const :tag "At the end of the subtree" subtree)
                 (const :tag "Before next header" header)))

(defcustom ein:markdown-footnote-location 'end
  "Position where new footnotes are inserted in the document."
  :group 'ein:markdown
  :type '(choice (const :tag "At the end of the document" end)
                 (const :tag "Immediately after the current block" immediately)
                 (const :tag "At the end of the subtree" subtree)
                 (const :tag "Before next header" header)))

(defcustom ein:markdown-footnote-display '((raise 0.2) (height 0.8))
  "Display specification for footnote markers and inline footnotes.
By default, footnote text is reduced in size and raised.  Set to
nil to disable this."
  :group 'ein:markdown
  :type '(choice (sexp :tag "Display specification")
                 (const :tag "Don't set display property" nil))
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-sub-superscript-display
  '(((raise -0.3) (height 0.7)) . ((raise 0.3) (height 0.7)))
  "Display specification for subscript and superscripts.
The car is used for subscript, the cdr is used for superscripts."
  :group 'ein:markdown
  :type '(cons (choice (sexp :tag "Subscript form")
                       (const :tag "No lowering" nil))
               (choice (sexp :tag "Superscript form")
                       (const :tag "No raising" nil)))
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-unordered-list-item-prefix "  * "
  "String inserted before unordered list items."
  :group 'ein:markdown
  :type 'string)

(defcustom ein:markdown-nested-imenu-heading-index t
  "Use nested or flat imenu heading index.
A nested index may provide more natural browsing from the menu,
but a flat list may allow for faster keyboard navigation via tab
completion."
  :group 'ein:markdown
  :type 'boolean
  :safe 'booleanp
  :package-version '(ein:markdown-mode . "2.2"))

(defcustom ein:markdown-add-footnotes-to-imenu t
  "Add footnotes to end of imenu heading index."
  :group 'ein:markdown
  :type 'boolean
  :safe 'booleanp
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-use-pandoc-style-yaml-metadata nil
  "When non-nil, allow YAML metadata anywhere in the document."
  :group 'ein:markdown
  :type 'boolean)

(defcustom ein:markdown-split-window-direction 'any
  "Preference for splitting windows for static and live preview.
The default value is \\='any, which instructs Emacs to use
`split-window-sensibly' to automatically choose how to split
windows based on the values of `split-width-threshold' and
`split-height-threshold' and the available windows.  To force
vertically split (left and right) windows, set this to \\='vertical
or \\='right.  To force horizontally split (top and bottom) windows,
set this to \\='horizontal or \\='below."
  :group 'ein:markdown
  :type '(choice (const :tag "Automatic" any)
                 (const :tag "Right (vertical)" right)
                 (const :tag "Below (horizontal)" below))
  :package-version '(ein:markdown-mode . "2.2"))

(defcustom ein:markdown-list-indent-width 4
  "Depth of indentation for ein:markdown lists.
Used in `markdown-demote-list-item' and
`markdown-promote-list-item'."
  :group 'ein:markdown
  :type 'integer)

(defcustom ein:markdown-enable-prefix-prompts t
  "Display prompts for certain prefix commands.
Set to nil to disable these prompts."
  :group 'ein:markdown
  :type 'boolean
  :safe 'booleanp
  :package-version '(ein:markdown-mode . "2.3"))

(defcustom ein:markdown-edit-code-block-default-mode 'normal-mode
  "Default mode to use for editing code blocks.
This mode is used when automatic detection fails, such as for GFM
code blocks with no language specified."
  :group 'ein:markdown
  :type '(choice function (const :tag "None" nil))
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-translate-filename-function #'identity
  "Function to use to translate filenames when following links.
\\<ein:markdown-mode-map>\\[ein:markdown-follow-thing-at-point] and \\[ein:markdown-follow-link-at-point]
call this function with the filename as only argument whenever
they encounter a filename (instead of a URL) to be visited and
use its return value instead of the filename in the link.  For
example, if absolute filenames are actually relative to a server
root directory, you can set
`markdown-translate-filename-function' to a function that
prepends the root directory to the given filename."
  :group 'ein:markdown
  :type 'function
  :risky t
  :package-version '(ein:markdown-mode . "2.4"))

(defcustom ein:markdown-max-image-size nil
  "Maximum width and height for displayed inline images.
This variable may be nil or a cons cell (MAX-WIDTH . MAX-HEIGHT).
When nil, use the actual size.  Otherwise, use ImageMagick to
resize larger images to be of the given maximum dimensions.  This
requires Emacs to be built with ImageMagick support."
  :group 'ein:markdown
  :package-version '(ein:markdown-mode . "2.4")
  :type '(choice
          (const :tag "Use actual image width" nil)
          (cons (choice (sexp :tag "Maximum width in pixels")
                        (const :tag "No maximum width" nil))
                (choice (sexp :tag "Maximum height in pixels")
                        (const :tag "No maximum height" nil)))))


;;; Markdown-Specific `rx' Macro ==============================================

;; Based on python-rx from python.el.
(eval-and-compile
  (defconst ein:markdown-rx-constituents
    `((newline . ,(rx "\n"))
      (indent . ,(rx (or (repeat 4 " ") "\t")))
      (block-end . ,(rx (and (or (one-or-more (zero-or-more blank) "\n") line-end))))
      (numeral . ,(rx (and (one-or-more (any "0-9#")) ".")))
      (bullet . ,(rx (any "*+:-")))
      (list-marker . ,(rx (or (and (one-or-more (any "0-9#")) ".")
                              (any "*+:-"))))
      (checkbox . ,(rx "[" (any " xX") "]")))
    "ein:markdown-specific sexps for `markdown-rx'")

  (defun ein:markdown-rx-to-string (form &optional no-group)
    "ein:markdown mode specialized `rx-to-string' function.
This variant supports named Markdown expressions in FORM.
NO-GROUP non-nil means don't put shy groups around the result."
    (let ((rx-constituents (append ein:markdown-rx-constituents rx-constituents)))
      (rx-to-string form no-group)))

  (defmacro ein:markdown-rx (&rest regexps)
    "ein:markdown mode specialized rx macro.
This variant of `rx' supports common Markdown named REGEXPS."
    (cond ((null regexps)
           (error "No regexp"))
          ((cdr regexps)
           (ein:markdown-rx-to-string `(and ,@regexps) t))
          (t
           (ein:markdown-rx-to-string (car regexps) t)))))


;;; Regular Expressions =======================================================

(defconst ein:markdown-regex-comment-start
  "<!--"
  "Regular expression matches HTML comment opening.")

(defconst ein:markdown-regex-comment-end
  "--[ \t]*>"
  "Regular expression matches HTML comment closing.")

(defconst ein:markdown-regex-link-inline
  "\\(!\\)?\\(\\[\\)\\([^]^][^]]*\\|\\)\\(\\]\\)\\((\\)\\([^)]*?\\)\\(?:\\s-+\\(\"[^\"]*\"\\)\\)?\\()\\)"
  "Regular expression for a [text](file) or an image link ![text](file).
Group 1 matches the leading exclamation point (optional).
Group 2 matches the opening square bracket.
Group 3 matches the text inside the square brackets.
Group 4 matches the closing square bracket.
Group 5 matches the opening parenthesis.
Group 6 matches the URL.
Group 7 matches the title (optional).
Group 8 matches the closing parenthesis.")

(defconst ein:markdown-regex-link-reference
  "\\(!\\)?\\(\\[\\)\\([^]^][^]]*\\|\\)\\(\\]\\)[ ]?\\(\\[\\)\\([^]]*?\\)\\(\\]\\)"
  "Regular expression for a reference link [text][id].
Group 1 matches the leading exclamation point (optional).
Group 2 matches the opening square bracket for the link text.
Group 3 matches the text inside the square brackets.
Group 4 matches the closing square bracket for the link text.
Group 5 matches the opening square bracket for the reference label.
Group 6 matches the reference label.
Group 7 matches the closing square bracket for the reference label.")

(defconst ein:markdown-regex-reference-definition
  "^ \\{0,3\\}\\(\\[\\)\\([^]\n]+?\\)\\(\\]\\)\\(:\\)\\s *\\(.*?\\)\\s *\\( \"[^\"]*\"$\\|$\\)"
  "Regular expression for a reference definition.
Group 1 matches the opening square bracket.
Group 2 matches the reference label.
Group 3 matches the closing square bracket.
Group 4 matches the colon.
Group 5 matches the URL.
Group 6 matches the title attribute (optional).")

(defconst ein:markdown-regex-footnote
  "\\(\\[\\^\\)\\(.+?\\)\\(\\]\\)"
  "Regular expression for a footnote marker [^fn].
Group 1 matches the opening square bracket and carat.
Group 2 matches only the label, without the surrounding markup.
Group 3 matches the closing square bracket.")

(defconst ein:markdown-regex-header
  "^\\(?:\\([^\r\n\t -].*\\)\n\\(?:\\(=+\\)\\|\\(-+\\)\\)\\|\\(#+[ \t]+\\)\\(.*?\\)\\([ \t]*#*\\)\\)$"
  "Regexp identifying Markdown headings.
Group 1 matches the text of a setext heading.
Group 2 matches the underline of a level-1 setext heading.
Group 3 matches the underline of a level-2 setext heading.
Group 4 matches the opening hash marks of an atx heading and whitespace.
Group 5 matches the text, without surrounding whitespace, of an atx heading.
Group 6 matches the closing whitespace and hash marks of an atx heading.")

(defconst ein:markdown-regex-header-setext
  "^\\([^\r\n\t -].*\\)\n\\(=+\\|-+\\)$"
  "Regular expression for generic setext-style (underline) headers.")

(defconst ein:markdown-regex-header-atx
  "^\\(#+\\)[ \t]+\\(.*?\\)[ \t]*\\(#*\\)$"
  "Regular expression for generic atx-style (hash mark) headers.")

(defconst ein:markdown-regex-hr
  (rx line-start
      (group (or (and (repeat 3 (and "*" (? " "))) (* (any "* ")))
                 (and (repeat 3 (and "-" (? " "))) (* (any "- ")))
                 (and (repeat 3 (and "_" (? " "))) (* (any "_ ")))))
      line-end)
  "Regular expression for matching ein:markdown horizontal rules.")

(defconst ein:markdown-regex-code
  "\\(?:\\`\\|[^\\]\\)\\(\\(`+\\)\\(\\(?:.\\|\n[^\n]\\)*?[^`]\\)\\(\\2\\)\\)\\(?:[^`]\\|\\'\\)"
  "Regular expression for matching inline code fragments.

Group 1 matches the entire code fragment including the backquotes.
Group 2 matches the opening backquotes.
Group 3 matches the code fragment itself, without backquotes.
Group 4 matches the closing backquotes.

The leading, unnumbered group ensures that the leading backquote
character is not escaped.
The last group, also unnumbered, requires that the character
following the code fragment is not a backquote.
Note that \\(?:.\\|\n[^\n]\\) matches any character, including newlines,
but not two newlines in a row.")

(defconst ein:markdown-regex-kbd
  "\\(<kbd>\\)\\(\\(?:.\\|\n[^\n]\\)*?\\)\\(</kbd>\\)"
  "Regular expression for matching <kbd> tags.
Groups 1 and 3 match the opening and closing tags.
Group 2 matches the key sequence.")

(defconst ein:markdown-regex-pre
  "^\\(    \\|\t\\).*$"
  "Regular expression for matching preformatted text sections.")

(defconst ein:markdown-regex-list
  (ein:markdown-rx line-start
               ;; 1. Leading whitespace
               (group (* blank))
               ;; 2. List marker: a numeral, bullet, or colon
               (group list-marker)
               ;; 3. Trailing whitespace
               (group (+ blank))
               ;; 4. Optional checkbox for GFM task list items
               (opt (group (and checkbox (* blank)))))
  "Regular expression for matching list items.")

(defconst ein:markdown-regex-bold
  "\\(^\\|[^\\]\\)\\(\\([*_]\\{2\\}\\)\\([^ \n\t\\]\\|[^ \n\t]\\(?:.\\|\n[^\n]\\)*?[^\\ ]\\)\\(\\3\\)\\)"
  "Regular expression for matching bold text.
Group 1 matches the character before the opening asterisk or
underscore, if any, ensuring that it is not a backslash escape.
Group 2 matches the entire expression, including delimiters.
Groups 3 and 5 matches the opening and closing delimiters.
Group 4 matches the text inside the delimiters.")

(defconst ein:markdown-regex-italic
  "\\(?:^\\|[^\\]\\)\\(\\([*_]\\)\\([^ \n\t\\]\\|[^ \n\t*]\\(?:.\\|\n[^\n]\\)*?[^\\ ]\\)\\(\\2\\)\\)"
  "Regular expression for matching italic text.
The leading unnumbered matches the character before the opening
asterisk or underscore, if any, ensuring that it is not a
backslash escape.
Group 1 matches the entire expression, including delimiters.
Groups 2 and 4 matches the opening and closing delimiters.
Group 3 matches the text inside the delimiters.")

(defconst ein:markdown-regex-strike-through
  "\\(^\\|[^\\]\\)\\(\\(~~\\)\\([^ \n\t\\]\\|[^ \n\t]\\(?:.\\|\n[^\n]\\)*?[^\\ ]\\)\\(~~\\)\\)"
  "Regular expression for matching strike-through text.
Group 1 matches the character before the opening tilde, if any,
ensuring that it is not a backslash escape.
Group 2 matches the entire expression, including delimiters.
Groups 3 and 5 matches the opening and closing delimiters.
Group 4 matches the text inside the delimiters.")

(defconst ein:markdown-regex-blockquote
  "^[ \t]*\\([A-Z]?>\\)\\([ \t]*\\)\\(.*\\)$"
  "Regular expression for matching blockquote lines.
Also accounts for a potential capital letter preceding the angle
bracket, for use with Leanpub blocks (asides, warnings, info
blocks, etc.).
Group 1 matches the leading angle bracket.
Group 2 matches the separating whitespace.
Group 3 matches the text.")

(defconst ein:markdown-regex-line-break
  "[^ \n\t][ \t]*\\(  \\)$"
  "Regular expression for matching line breaks.")

(defconst ein:markdown-regex-uri
  (concat "\\(" (regexp-opt ein:markdown-uri-types) ":[^]\t\n\r<>,;() ]+\\)")
  "Regular expression for matching inline URIs.")

(defconst ein:markdown-regex-angle-uri
  (concat "\\(<\\)\\(" (regexp-opt ein:markdown-uri-types) ":[^]\t\n\r<>,;()]+\\)\\(>\\)")
  "Regular expression for matching inline URIs in angle brackets.")

(defconst ein:markdown-regex-email
  "<\\(\\(?:\\sw\\|\\s_\\|\\s.\\)+@\\(?:\\sw\\|\\s_\\|\\s.\\)+\\)>"
  "Regular expression for matching inline email addresses.")

(defsubst ein:markdown-make-regex-link-generic ()
  "Make regular expression for matching any recognized link."
  (concat "\\(?:" ein:markdown-regex-link-inline
          "\\|" ein:markdown-regex-link-reference
          "\\|" ein:markdown-regex-angle-uri "\\)"))

(defconst ein:markdown-regex-blank-line
  "^[[:blank:]]*$"
  "Regular expression that matches a blank line.")

(defconst ein:markdown-regex-block-separator
  "\n[\n\t\f ]*\n"
  "Regular expression for matching block boundaries.")

(defconst ein:markdown-regex-block-separator-noindent
  (concat "\\(\\`\\|\\(" ein:markdown-regex-block-separator "\\)[^\n\t\f ]\\)")
  "Regexp for block separators before lines with no indentation.")

(defconst ein:markdown-regex-math-inline-single
  "\\(?:^\\|[^\\]\\)\\(\\$\\)\\(\\(?:[^\\$]\\|\\\\.\\)*\\)\\(\\$\\)"
  "Regular expression for itex $..$ math mode expressions.
Groups 1 and 3 match the opening and closing dollar signs.
Group 2 matches the mathematical expression contained within.")

(defconst ein:markdown-regex-math-inline-double
  "\\(?:^\\|[^\\]\\)\\(\\$\\$\\)\\(\\(?:[^\\$]\\|\\\\.\\)*\\)\\(\\$\\$\\)"
  "Regular expression for itex $$..$$ math mode expressions.
Groups 1 and 3 match opening and closing dollar signs.
Group 2 matches the mathematical expression contained within.")

(defconst ein:markdown-regex-math-display
  (rx line-start (* blank)
      (group (group (repeat 1 2 "\\")) "[")
      (group (*? anything))
      (group (backref 2) "]")
      line-end)
  "Regular expression for \[..\] or \\[..\\] display math.
Groups 1 and 4 match the opening and closing markup.
Group 3 matches the mathematical expression contained within.
Group 2 matches the opening slashes, and is used internally to
match the closing slashes.")

(defsubst ein:markdown-make-tilde-fence-regex (num-tildes &optional end-of-line)
  "Return regexp matching a tilde code fence at least NUM-TILDES long.
END-OF-LINE is the regexp construct to indicate end of line; $ if
missing."
  (format "%s%d%s%s" "^[[:blank:]]*\\([~]\\{" num-tildes ",\\}\\)"
          (or end-of-line "$")))

(defconst ein:markdown-regex-tilde-fence-begin
  (ein:markdown-make-tilde-fence-regex
   3 "\\([[:blank:]]*{?\\)[[:blank:]]*\\([^[:space:]]+?\\)?\\(?:[[:blank:]]+\\(.+?\\)\\)?\\([[:blank:]]*}?[[:blank:]]*\\)$")
  "Regular expression for matching tilde-fenced code blocks.
Group 1 matches the opening tildes.
Group 2 matches (optional) opening brace and surrounding whitespace.
Group 3 matches the language identifier (optional).
Group 4 matches the info string (optional).
Group 5 matches the closing brace (optional) and any surrounding whitespace.
Groups need to agree with `markdown-regex-gfm-code-block-open'.")

(defconst ein:markdown-regex-declarative-metadata
  "^\\([[:alpha:]][[:alpha:] _-]*?\\)\\([:=][ \t]*\\)\\(.*\\)$"
  "Regular expression for matching declarative metadata statements.
This matches MultiMarkdown metadata as well as YAML and TOML
assignments such as the following:

    variable: value

or

    variable = value")

(defconst ein:markdown-regex-pandoc-metadata
  "^\\(%\\)\\([ \t]*\\)\\(.*\\(?:\n[ \t]+.*\\)*\\)"
  "Regular expression for matching Pandoc metadata.")

(defconst ein:markdown-regex-yaml-metadata-border
  "\\(-\\{3\\}\\)$"
  "Regular expression for matching YAML metadata.")

(defconst ein:markdown-regex-yaml-pandoc-metadata-end-border
  "^\\(\\.\\{3\\}\\|\\-\\{3\\}\\)$"
  "Regular expression for matching YAML metadata end borders.")

(defsubst ein:markdown-get-yaml-metadata-start-border ()
  "Return YAML metadata start border depending upon whether Pandoc is used."
  (concat
   (if ein:markdown-use-pandoc-style-yaml-metadata "^" "\\`")
   ein:markdown-regex-yaml-metadata-border))

(defsubst ein:markdown-get-yaml-metadata-end-border (_)
  "Return YAML metadata end border depending upon whether Pandoc is used."
  (if ein:markdown-use-pandoc-style-yaml-metadata
      ein:markdown-regex-yaml-pandoc-metadata-end-border
    ein:markdown-regex-yaml-metadata-border))

(defconst ein:markdown-regex-inline-attributes
  "[ \t]*\\({:?\\)[ \t]*\\(\\(#[[:alpha:]_.:-]+\\|\\.[[:alpha:]_.:-]+\\|\\w+=['\"]?[^\n'\"]*['\"]?\\),?[ \t]*\\)+\\(}\\)[ \t]*$"
  "Regular expression for matching inline identifiers or attribute lists.
Compatible with Pandoc, Python ein:markdown, PHP Markdown Extra, and Leanpub.")

(defconst ein:markdown-regex-leanpub-sections
  (concat
   "^\\({\\)\\("
   (regexp-opt '("frontmatter" "mainmatter" "backmatter" "appendix" "pagebreak"))
   "\\)\\(}\\)[ \t]*\n")
  "Regular expression for Leanpub section markers and related syntax.")

(defconst ein:markdown-regex-sub-superscript
  "\\(?:^\\|[^\\~^]\\)\\(\\([~^]\\)\\([[:alnum:]]+\\)\\(\\2\\)\\)"
  "The regular expression matching a sub- or superscript.
The leading un-numbered group matches the character before the
opening tilde or carat, if any, ensuring that it is not a
backslash escape, carat, or tilde.
Group 1 matches the entire expression, including markup.
Group 2 matches the opening markup--a tilde or carat.
Group 3 matches the text inside the delimiters.
Group 4 matches the closing markup--a tilde or carat.")

(defconst ein:markdown-regex-include
  "^\\(<<\\)\\(?:\\(\\[\\)\\(.*\\)\\(\\]\\)\\)?\\(?:\\((\\)\\(.*\\)\\()\\)\\)?\\(?:\\({\\)\\(.*\\)\\(}\\)\\)?$"
  "Regular expression matching common forms of include syntax.
Marked 2, Leanpub, and other processors support some of these forms:

<<[sections/section1.md]
<<(folder/filename)
<<[Code title](folder/filename)
<<{folder/raw_file.html}

Group 1 matches the opening two angle brackets.
Groups 2-4 match the opening square bracket, the text inside,
and the closing square bracket, respectively.
Groups 5-7 match the opening parenthesis, the text inside, and
the closing parenthesis.
Groups 8-10 match the opening brace, the text inside, and the brace.")

(defconst ein:markdown-regex-pandoc-inline-footnote
  "\\(\\^\\)\\(\\[\\)\\(\\(?:.\\|\n[^\n]\\)*?\\)\\(\\]\\)"
  "Regular expression for Pandoc inline footnote^[footnote text].
Group 1 matches the opening caret.
Group 2 matches the opening square bracket.
Group 3 matches the footnote text, without the surrounding markup.
Group 4 matches the closing square bracket.")

(defconst ein:markdown-regex-html-attr
  "\\(\\<[[:alpha:]:-]+\\>\\)\\(\\s-*\\(=\\)\\s-*\\(\".*?\"\\|'.*?'\\|[^'\">[:space:]]+\\)?\\)?"
  "Regular expression for matching HTML attributes and values.
Group 1 matches the attribute name.
Group 2 matches the following whitespace, equals sign, and value, if any.
Group 3 matches the equals sign, if any.
Group 4 matches single-, double-, or un-quoted attribute values.")

(defconst ein:markdown-regex-html-tag
  (concat "\\(</?\\)\\(\\w+\\)\\(\\(\\s-+" ein:markdown-regex-html-attr
          "\\)+\\s-*\\|\\s-*\\)\\(/?>\\)")
  "Regular expression for matching HTML tags.
Groups 1 and 9 match the beginning and ending angle brackets and slashes.
Group 2 matches the tag name.
Group 3 matches all attributes and whitespace following the tag name.")

(defconst ein:markdown-regex-html-entity
  "\\(&#?[[:alnum:]]+;\\)"
  "Regular expression for matching HTML entities.")


;;; Syntax ====================================================================

(defvar ein:markdown--syntax-properties
  (list 'ein:markdown-tilde-fence-begin nil
        'ein:markdown-tilde-fence-end nil
        'ein:markdown-fenced-code nil
        'ein:markdown-yaml-metadata-begin nil
        'ein:markdown-yaml-metadata-end nil
        'ein:markdown-yaml-metadata-section nil
        'ein:markdown-list-item nil
        'ein:markdown-pre nil
        'ein:markdown-blockquote nil
        'ein:markdown-hr nil
        'ein:markdown-comment nil
        'ein:markdown-heading nil
        'ein:markdown-heading-1-setext nil
        'ein:markdown-heading-2-setext nil
        'ein:markdown-heading-1-atx nil
        'ein:markdown-heading-2-atx nil
        'ein:markdown-heading-3-atx nil
        'ein:markdown-heading-4-atx nil
        'ein:markdown-heading-5-atx nil
        'ein:markdown-heading-6-atx nil
        'ein:markdown-metadata-key nil
        'ein:markdown-metadata-value nil
        'ein:markdown-metadata-markup nil)
  "Property list of all Markdown syntactic properties.")

(defsubst ein:markdown-in-comment-p (&optional pos)
  "Return non-nil if POS is in a comment.
If POS is not given, use point instead."
  (get-text-property (or pos (point)) 'ein:markdown-comment))

(defun ein:markdown--cur-list-item-bounds ()
  "Return a list describing the list item at point.
Assumes that match data is set for `markdown-regex-list'.  See the
documentation for `markdown-cur-list-item-bounds' for the format of
the returned list."
  (save-excursion
    (let* ((begin (match-beginning 0))
           (indent (length (match-string-no-properties 1)))
           (nonlist-indent (- (match-end 3) (match-beginning 0)))
           (marker (buffer-substring-no-properties
                    (match-beginning 2) (match-end 3)))
           (checkbox (match-string-no-properties 4))
           (match (butlast (match-data t)))
           (end (ein:markdown-cur-list-item-end nonlist-indent)))
      (list begin end indent nonlist-indent marker checkbox match))))

(defun ein:markdown--append-list-item-bounds (marker indent cur-bounds bounds)
  "Update list item BOUNDS given list MARKER, block INDENT, and CUR-BOUNDS.
Here, MARKER is a string representing the type of list and INDENT
is an integer giving the indentation, in spaces, of the current
block.  CUR-BOUNDS is a list of the form returned by
`markdown-cur-list-item-bounds' and BOUNDS is a list of bounds
values for parent list items.  When BOUNDS is nil, it means we are
at baseline (not inside of a nested list)."
  (let ((prev-indent (or (cl-third (car bounds)) 0)))
    (cond
     ;; New list item at baseline.
     ((and marker (null bounds))
      (list cur-bounds))
     ;; List item with greater indentation (four or more spaces).
     ;; Increase list level by consing CUR-BOUNDS onto BOUNDS.
     ((and marker (>= indent (+ prev-indent 4)))
      (cons cur-bounds bounds))
     ;; List item with greater or equal indentation (less than four spaces).
     ;; Keep list level the same by replacing the car of BOUNDS.
     ((and marker (>= indent prev-indent))
      (cons cur-bounds (cdr bounds)))
     ;; Lesser indentation level.
     ;; Pop appropriate number of elements off BOUNDS list (e.g., lesser
     ;; indentation could move back more than one list level).  Note
     ;; that this block need not be the beginning of list item.
     ((< indent prev-indent)
      (while (and (> (length bounds) 1)
                  (setq prev-indent (cl-third (cadr bounds)))
                  (< indent (+ prev-indent 4)))
        (setq bounds (cdr bounds)))
      (cons cur-bounds bounds))
     ;; Otherwise, do nothing.
     (t bounds))))

(defun ein:markdown-syntax-propertize-list-items (start end)
  "Propertize list items from START to END.
Stores nested list item information in the `markdown-list-item'
text property to make later syntax analysis easier.  The value of
this property is a list with elements of the form (begin . end)
giving the bounds of the current and parent list items."
  (save-excursion
    (goto-char start)
    (let (bounds level pre-regexp)
      ;; Find a baseline point with zero list indentation
      (ein:markdown-search-backward-baseline)
      ;; Search for all list items between baseline and END
      (while (and (< (point) end)
                  (re-search-forward ein:markdown-regex-list end 'limit))
        ;; Level of list nesting
        (setq level (length bounds))
        ;; Pre blocks need to be indented one level past the list level
        (setq pre-regexp (format "^\\(    \\|\t\\)\\{%d\\}" (1+ level)))
        (beginning-of-line)
        (cond
         ;; Reset at headings, horizontal rules, and top-level blank lines.
         ;; Propertize baseline when in range.
         ((ein:markdown-new-baseline)
          (setq bounds nil))
         ;; Make sure this is not a line from a pre block
         ((looking-at-p pre-regexp))
         ;; If not, then update levels and propertize list item when in range.
         (t
          (let* ((indent (current-indentation))
                 (cur-bounds (ein:markdown--cur-list-item-bounds))
                 (first (cl-first cur-bounds))
                 (last (cl-second cur-bounds))
                 (marker (cl-fifth cur-bounds)))
            (setq bounds (ein:markdown--append-list-item-bounds
                          marker indent cur-bounds bounds))
          (when (and (<= start (point)) (<= (point) end))
            (put-text-property first last 'ein:markdown-list-item bounds)))))
        (end-of-line)))))

(defun ein:markdown-syntax-propertize-pre-blocks (start end)
  "Match preformatted text blocks from START to END."
  (save-excursion
    (goto-char start)
    (let ((levels (ein:markdown-calculate-list-levels))
          indent pre-regexp close-regexp open close)
      (while (and (< (point) end) (not close))
        ;; Search for a region with sufficient indentation
        (if (null levels)
            (setq indent 1)
          (setq indent (1+ (length levels))))
        (setq pre-regexp (format "^\\(    \\|\t\\)\\{%d\\}" indent))
        (setq close-regexp (format "^\\(    \\|\t\\)\\{0,%d\\}\\([^ \t]\\)" (1- indent)))

        (cond
         ;; If not at the beginning of a line, move forward
         ((not (bolp)) (forward-line))
         ;; Move past blank lines
         ((ein:markdown-cur-line-blank-p) (forward-line))
         ;; At headers and horizontal rules, reset levels
         ((ein:markdown-new-baseline) (forward-line) (setq levels nil))
         ;; If the current line has sufficient indentation, mark out pre block
         ;; The opening should be preceded by a blank line.
         ((and (ein:markdown-prev-line-blank) (looking-at pre-regexp))
          (setq open (match-beginning 0))
          (while (and (or (looking-at-p pre-regexp) (ein:markdown-cur-line-blank-p))
                      (not (eobp)))
            (forward-line))
          (skip-syntax-backward "-")
          (setq close (point)))
         ;; If current line has a list marker, update levels, move to end of block
         ((looking-at ein:markdown-regex-list)
          (setq levels (ein:markdown-update-list-levels
                        (match-string 2) (current-indentation) levels))
          (ein:markdown-end-of-text-block))
         ;; If this is the end of the indentation level, adjust levels accordingly.
         ;; Only match end of indentation level if levels is not the empty list.
         ((and (car levels) (looking-at-p close-regexp))
          (setq levels (ein:markdown-update-list-levels
                        nil (current-indentation) levels))
          (ein:markdown-end-of-text-block))
         (t (ein:markdown-end-of-text-block))))

      (when (and open close)
        ;; Set text property data
        (put-text-property open close 'ein:markdown-pre (list open close))
        ;; Recursively search again
        (ein:markdown-syntax-propertize-pre-blocks (point) end)))))

(defconst ein:markdown-fenced-block-pairs
  `(((,ein:markdown-regex-tilde-fence-begin ein:markdown-tilde-fence-begin)
     (ein:markdown-make-tilde-fence-regex ein:markdown-tilde-fence-end)
     ein:markdown-fenced-code)
    ((ein:markdown-get-yaml-metadata-start-border ein:markdown-yaml-metadata-begin)
     (ein:markdown-get-yaml-metadata-end-border ein:markdown-yaml-metadata-end)
     ein:markdown-yaml-metadata-section))
  "Mapping of regular expressions to fenced-block constructs.
These constructs are distinguished by having a distinctive start
and end pattern, both of which take up an entire line of text,
but no special pattern to identify text within the fenced
blocks (unlike blockquotes and indented-code sections).

Each element within this list takes the form:

  ((START-REGEX-OR-FUN START-PROPERTY)
   (END-REGEX-OR-FUN END-PROPERTY)
   MIDDLE-PROPERTY)

Each *-REGEX-OR-FUN element can be a regular expression as a string, or a
function which evaluates to same. Functions for START-REGEX-OR-FUN accept no
arguments, but functions for END-REGEX-OR-FUN accept a single numerical argument
which is the length of the first group of the START-REGEX-OR-FUN match, which
can be ignored if unnecessary. `markdown-maybe-funcall-regexp' is used to
evaluate these into \"real\" regexps.

The *-PROPERTY elements are the text properties applied to each part of the
block construct when it is matched using
`markdown-syntax-propertize-fenced-block-constructs'. START-PROPERTY is applied
to the text matching START-REGEX-OR-FUN, END-PROPERTY to END-REGEX-OR-FUN, and
MIDDLE-PROPERTY to the text in between the two. The value of *-PROPERTY is the
`match-data' when the regexp was matched to the text. In the case of
MIDDLE-PROPERTY, the value is a false match data of the form \\='(begin end), with
begin and end set to the edges of the \"middle\" text. This makes fontification
easier.")

(defun ein:markdown-text-property-at-point (prop)
  (get-text-property (point) prop))

(defsubst ein:markdown-maybe-funcall-regexp (object &optional arg)
  (cond ((functionp object)
         (if arg (funcall object arg) (funcall object)))
        ((stringp object) object)
        (t (error "Object cannot be turned into regex"))))

(defsubst ein:markdown-get-start-fence-regexp ()
  "Return regexp to find all \"start\" sections of fenced block constructs.
Which construct is actually contained in the match must be found separately."
  (mapconcat
   #'identity
   (mapcar (lambda (entry) (ein:markdown-maybe-funcall-regexp (caar entry)))
           ein:markdown-fenced-block-pairs)
   "\\|"))

(defun ein:markdown-get-fenced-block-begin-properties ()
  (cl-mapcar (lambda (entry) (cl-cadar entry)) ein:markdown-fenced-block-pairs))

(defun ein:markdown-get-fenced-block-end-properties ()
  (cl-mapcar (lambda (entry) (cl-cadadr entry)) ein:markdown-fenced-block-pairs))

(defun ein:markdown-get-fenced-block-middle-properties ()
  (cl-mapcar #'cl-third ein:markdown-fenced-block-pairs))

(defun ein:markdown-find-previous-prop (prop &optional lim)
  "Find previous place where property PROP is non-nil, up to LIM.
Return a cons of (pos . property). pos is point if point contains
non-nil PROP."
  (let ((res
         (if (get-text-property (point) prop) (point)
           (previous-single-property-change
            (point) prop nil (or lim (point-min))))))
    (when (and (not (get-text-property res prop))
               (> res (point-min))
               (get-text-property (1- res) prop))
      (cl-decf res))
    (when (and res (get-text-property res prop)) (cons res prop))))

(defun ein:markdown-find-next-prop (prop &optional lim)
  "Find next place where property PROP is non-nil, up to LIM.
Return a cons of (POS . PROPERTY) where POS is point if point
contains non-nil PROP."
  (let ((res
         (if (get-text-property (point) prop) (point)
           (next-single-property-change
            (point) prop nil (or lim (point-max))))))
    (when (and res (get-text-property res prop)) (cons res prop))))

(defun ein:markdown-min-of-seq (map-fn seq)
  "Apply MAP-FN to SEQ and return element of SEQ with minimum value of MAP-FN."
  (cl-loop for el in seq
           with min = 1.0e+INF          ; infinity
           with min-el = nil
           do (let ((res (funcall map-fn el)))
                (when (< res min)
                  (setq min res)
                  (setq min-el el)))
           finally return min-el))

(defun ein:markdown-max-of-seq (map-fn seq)
  "Apply MAP-FN to SEQ and return element of SEQ with maximum value of MAP-FN."
  (cl-loop for el in seq
           with max = -1.0e+INF          ; negative infinity
           with max-el = nil
           do (let ((res (funcall map-fn el)))
                (when (and res (> res max))
                  (setq max res)
                  (setq max-el el)))
           finally return max-el))

(defun ein:markdown-find-previous-block ()
  "Find previous block.
Detect whether `markdown-syntax-propertize-fenced-block-constructs' was
unable to propertize the entire block, but was able to propertize the beginning
of the block. If so, return a cons of (pos . property) where the beginning of
the block was propertized."
  (let ((start-pt (point))
        (closest-open
         (ein:markdown-max-of-seq
          #'car
          (cl-remove-if
           #'null
           (cl-mapcar
            #'ein:markdown-find-previous-prop
            (ein:markdown-get-fenced-block-begin-properties))))))
    (when closest-open
      (let* ((length-of-open-match
              (let ((match-d
                     (get-text-property (car closest-open) (cdr closest-open))))
                (- (cl-fourth match-d) (cl-third match-d))))
             (end-regexp
              (ein:markdown-maybe-funcall-regexp
               (cl-caadr
                (cl-find-if
                 (lambda (entry) (eq (cl-cadar entry) (cdr closest-open)))
                 ein:markdown-fenced-block-pairs))
               length-of-open-match))
             (end-prop-loc
              (save-excursion
                (save-match-data
                  (goto-char (car closest-open))
                  (and (re-search-forward end-regexp start-pt t)
                       (match-beginning 0))))))
        (and (not end-prop-loc) closest-open)))))

(defun ein:markdown-get-fenced-block-from-start (prop)
  "Return limits of an enclosing fenced block from its start, using PROP.
Return value is a list usable as `match-data'."
  (catch 'no-rest-of-block
    (let* ((correct-entry
            (cl-find-if
             (lambda (entry) (eq (cl-cadar entry) prop))
             ein:markdown-fenced-block-pairs))
           (begin-of-begin (cl-first (ein:markdown-text-property-at-point prop)))
           (middle-prop (cl-third correct-entry))
           (end-prop (cl-cadadr correct-entry))
           (end-of-end
            (save-excursion
              (goto-char (match-end 0))   ; end of begin
              (unless (eobp) (forward-char))
              (let ((mid-prop-v (ein:markdown-text-property-at-point middle-prop)))
                (if (not mid-prop-v)    ; no middle
                    (progn
                      ;; try to find end by advancing one
                      (let ((end-prop-v
                             (ein:markdown-text-property-at-point end-prop)))
                        (if end-prop-v (cl-second end-prop-v)
                          (throw 'no-rest-of-block nil))))
                  (set-match-data mid-prop-v)
                  (goto-char (match-end 0))   ; end of middle
                  (beginning-of-line)         ; into end
                  (cl-second (ein:markdown-text-property-at-point end-prop)))))))
      (list begin-of-begin end-of-end))))

(defun ein:markdown-get-fenced-block-from-middle (prop)
  "Return limits of an enclosing fenced block from its middle, using PROP.
Return value is a list usable as `match-data'."
  (let* ((correct-entry
          (cl-find-if
           (lambda (entry) (eq (cl-third entry) prop))
           ein:markdown-fenced-block-pairs))
         (begin-prop (cl-cadar correct-entry))
         (begin-of-begin
          (save-excursion
            (goto-char (match-beginning 0))
            (unless (bobp) (forward-line -1))
            (beginning-of-line)
            (cl-first (ein:markdown-text-property-at-point begin-prop))))
         (end-prop (cl-cadadr correct-entry))
         (end-of-end
          (save-excursion
            (goto-char (match-end 0))
            (beginning-of-line)
            (cl-second (ein:markdown-text-property-at-point end-prop)))))
    (list begin-of-begin end-of-end)))

(defun ein:markdown-get-fenced-block-from-end (prop)
  "Return limits of an enclosing fenced block from its end, using PROP.
Return value is a list usable as `match-data'."
  (let* ((correct-entry
          (cl-find-if
           (lambda (entry) (eq (cl-cadadr entry) prop))
           ein:markdown-fenced-block-pairs))
         (end-of-end (cl-second (ein:markdown-text-property-at-point prop)))
         (middle-prop (cl-third correct-entry))
         (begin-prop (cl-cadar correct-entry))
         (begin-of-begin
          (save-excursion
            (goto-char (match-beginning 0)) ; beginning of end
            (unless (bobp) (backward-char)) ; into middle
            (let ((mid-prop-v (ein:markdown-text-property-at-point middle-prop)))
              (if (not mid-prop-v)
                  (progn
                    (beginning-of-line)
                    (cl-first (ein:markdown-text-property-at-point begin-prop)))
                (set-match-data mid-prop-v)
                (goto-char (match-beginning 0))   ; beginning of middle
                (unless (bobp) (forward-line -1)) ; into beginning
                (beginning-of-line)
                (cl-first (ein:markdown-text-property-at-point begin-prop)))))))
    (list begin-of-begin end-of-end)))

(defun ein:markdown-get-enclosing-fenced-block-construct (&optional pos)
  "Get \"fake\" match data for block enclosing POS.
Returns fake match data which encloses the start, middle, and end
of the block construct enclosing POS, if it exists. Used in
`markdown-code-block-at-pos'."
  (save-excursion
    (when pos (goto-char pos))
    (beginning-of-line)
    (car
     (cl-remove-if
      #'null
      (cl-mapcar
       (lambda (fun-and-prop)
         (cl-destructuring-bind (fun prop) fun-and-prop
           (when prop
             (save-match-data
               (set-match-data (ein:markdown-text-property-at-point prop))
               (funcall fun prop)))))
       `((ein:markdown-get-fenced-block-from-start
          ,(cl-find-if
            #'ein:markdown-text-property-at-point
            (ein:markdown-get-fenced-block-begin-properties)))
         (ein:markdown-get-fenced-block-from-middle
          ,(cl-find-if
            #'ein:markdown-text-property-at-point
            (ein:markdown-get-fenced-block-middle-properties)))
         (ein:markdown-get-fenced-block-from-end
          ,(cl-find-if
            #'ein:markdown-text-property-at-point
            (ein:markdown-get-fenced-block-end-properties)))))))))

(defun ein:markdown-propertize-end-match (reg end fence-spec middle-begin)
  "Get match for REG up to END, if exists, and propertize appropriately.
FENCE-SPEC is an entry in `markdown-fenced-block-pairs' and
MIDDLE-BEGIN is the start of the \"middle\" section of the block."
  (when (re-search-forward reg end t)
    (let ((close-begin (match-beginning 0)) ; Start of closing line.
          (close-end (match-end 0))         ; End of closing line.
          (close-data (match-data t)))      ; Match data for closing line.
      ;; Propertize middle section of fenced block.
      (put-text-property middle-begin close-begin
                         (cl-third fence-spec)
                         (list middle-begin close-begin))
      ;; If the block is a YAML block, propertize the declarations inside
      (ein:markdown-syntax-propertize-yaml-metadata middle-begin close-begin)
      ;; Propertize closing line of fenced block.
      (put-text-property close-begin close-end
                         (cl-cadadr fence-spec) close-data))))

(defun ein:markdown-syntax-propertize-fenced-block-constructs (start end)
  "Propertize according to `markdown-fenced-block-pairs' from START to END.
If unable to propertize an entire block (if the start of a block is within START
and END, but the end of the block is not), propertize the start section of a
block, then in a subsequent call propertize both middle and end by finding the
start which was previously propertized."
  (let ((start-reg (ein:markdown-get-start-fence-regexp)))
    (save-excursion
      (goto-char start)
      ;; start from previous unclosed block, if exists
      (let ((prev-begin-block (ein:markdown-find-previous-block)))
        (when prev-begin-block
          (let* ((correct-entry
                  (cl-find-if (lambda (entry)
                                (eq (cdr prev-begin-block) (cl-cadar entry)))
                              ein:markdown-fenced-block-pairs))
                 (enclosed-text-start (1+ (car prev-begin-block)))
                 (start-length
                  (save-excursion
                    (goto-char (car prev-begin-block))
                    (string-match
                     (ein:markdown-maybe-funcall-regexp
                      (caar correct-entry))
                     (buffer-substring
                      (line-beginning-position) (line-end-position)))
                    (- (match-end 1) (match-beginning 1))))
                 (end-reg (ein:markdown-maybe-funcall-regexp
                           (cl-caadr correct-entry) start-length)))
            (ein:markdown-propertize-end-match
             end-reg end correct-entry enclosed-text-start))))
      ;; find all new blocks within region
      (while (re-search-forward start-reg end t)
        ;; we assume the opening constructs take up (only) an entire line,
        ;; so we re-check the current line
        (let* ((cur-line (buffer-substring (line-beginning-position) (line-end-position)))
               ;; find entry in `markdown-fenced-block-pairs' corresponding
               ;; to regex which was matched
               (correct-entry
                (cl-find-if
                 (lambda (fenced-pair)
                   (string-match-p
                    (ein:markdown-maybe-funcall-regexp (caar fenced-pair))
                    cur-line))
                 ein:markdown-fenced-block-pairs))
               (enclosed-text-start
                (save-excursion (1+ (line-end-position))))
               (end-reg
                (ein:markdown-maybe-funcall-regexp
                 (cl-caadr correct-entry)
                 (if (and (match-beginning 1) (match-end 1))
                     (- (match-end 1) (match-beginning 1))
                   0))))
          ;; get correct match data
          (save-excursion
            (beginning-of-line)
            (re-search-forward
             (ein:markdown-maybe-funcall-regexp (caar correct-entry))
             (line-end-position)))
          ;; mark starting, even if ending is outside of region
          (put-text-property (match-beginning 0) (match-end 0)
                             (cl-cadar correct-entry) (match-data t))
          (ein:markdown-propertize-end-match
           end-reg end correct-entry enclosed-text-start))))))

(defun ein:markdown-syntax-propertize-blockquotes (start end)
  "Match blockquotes from START to END."
  (save-excursion
    (goto-char start)
    (while (and (re-search-forward ein:markdown-regex-blockquote end t)
                (not (ein:markdown-code-block-at-pos (match-beginning 0))))
      (put-text-property (match-beginning 0) (match-end 0)
                         'ein:markdown-blockquote
                         (match-data t)))))

(defun ein:markdown-syntax-propertize-hrs (start end)
  "Match horizontal rules from START to END."
  (save-excursion
    (goto-char start)
    (while (re-search-forward ein:markdown-regex-hr end t)
      (let ((beg (match-beginning 0))
            (end (match-end 0)))
        (goto-char beg)
        (unless (or (ein:markdown-on-heading-p)
                    (ein:markdown-code-block-at-point-p))
          (put-text-property beg end 'ein:markdown-hr (match-data t)))
        (goto-char end)))))

(defun ein:markdown-syntax-propertize-yaml-metadata (start end)
  "Propertize elements inside YAML metadata blocks from START to END.
Assumes region from START and END is already known to be the interior
region of a YAML metadata block as propertized by
`markdown-syntax-propertize-fenced-block-constructs'."
  (save-excursion
    (goto-char start)
    (cl-loop
     while (re-search-forward ein:markdown-regex-declarative-metadata end t)
     do (progn
          (put-text-property (match-beginning 1) (match-end 1)
                             'ein:markdown-metadata-key (match-data t))
          (put-text-property (match-beginning 2) (match-end 2)
                             'ein:markdown-metadata-markup (match-data t))
          (put-text-property (match-beginning 3) (match-end 3)
                             'ein:markdown-metadata-value (match-data t))))))

(defun ein:markdown-syntax-propertize-headings (start end)
  "Match headings of type SYMBOL with REGEX from START to END."
  (goto-char start)
  (while (re-search-forward ein:markdown-regex-header end t)
    (unless (ein:markdown-code-block-at-pos (match-beginning 0))
      (put-text-property
       (match-beginning 0) (match-end 0) 'ein:markdown-heading
       (match-data t))
      (put-text-property
       (match-beginning 0) (match-end 0)
       (cond ((match-string-no-properties 2) 'ein:markdown-heading-1-setext)
             ((match-string-no-properties 3) 'ein:markdown-heading-2-setext)
             (t (let ((atx-level (length (ein:markdown-trim-whitespace
                                          (match-string-no-properties 4)))))
                  (intern (format "ein:markdown-heading-%d-atx" atx-level)))))
       (match-data t)))))

(defun ein:markdown-syntax-propertize-comments (start end)
  "Match HTML comments from the START to END."
  (let* ((in-comment (nth 4 (syntax-ppss)))
         (comment-begin (nth 8 (syntax-ppss))))
    (goto-char start)
    (cond
     ;; Comment start
     ((and (not in-comment)
           (re-search-forward ein:markdown-regex-comment-start end t)
           (not (ein:markdown-inline-code-at-point-p))
           (not (ein:markdown-code-block-at-point-p)))
      (let ((open-beg (match-beginning 0)))
        (put-text-property open-beg (1+ open-beg)
                           'syntax-table (string-to-syntax "<"))
        (ein:markdown-syntax-propertize-comments
         (min (1+ (match-end 0)) end (point-max)) end)))
     ;; Comment end
     ((and in-comment comment-begin
           (re-search-forward ein:markdown-regex-comment-end end t))
      (let ((comment-end (match-end 0)))
        (put-text-property (1- comment-end) comment-end
                           'syntax-table (string-to-syntax ">"))
        ;; Remove any other text properties inside the comment
        (remove-text-properties comment-begin comment-end
                                ein:markdown--syntax-properties)
        (put-text-property comment-begin comment-end
                           'ein:markdown-comment (list comment-begin comment-end))
        (ein:markdown-syntax-propertize-comments
         (min (1+ comment-end) end (point-max)) end)))
     ;; Nothing found
     (t nil))))

(defun ein:markdown-syntax-propertize (start end)
  "Function used as `syntax-propertize-function'.
START and END delimit region to propertize."
  (with-silent-modifications
    (save-excursion
      (remove-text-properties start end ein:markdown--syntax-properties)
      (ein:markdown-syntax-propertize-fenced-block-constructs start end)
      (ein:markdown-syntax-propertize-list-items start end)
      (ein:markdown-syntax-propertize-pre-blocks start end)
      (ein:markdown-syntax-propertize-blockquotes start end)
      (ein:markdown-syntax-propertize-headings start end)
      (ein:markdown-syntax-propertize-hrs start end)
      (ein:markdown-syntax-propertize-comments start end))))


;;; Markup Hiding =============================================================

(defconst ein:markdown-markup-properties
  '(face ein:markdown-markup-face)
  "List of properties and values to apply to markup.")

(defconst ein:markdown-language-keyword-properties
  '(face ein:markdown-language-keyword-face)
  "List of properties and values to apply to code block language names.")

(defconst ein:markdown-language-info-properties
  '(face ein:markdown-language-info-face)
  "List of properties and values to apply to code block language info strings.")

(defconst ein:markdown-include-title-properties
  '(face ein:markdown-link-title-face)
  "List of properties and values to apply to included code titles.")

;;; Font Lock =================================================================

(require 'font-lock)

(defvar ein:markdown-italic-face 'ein:markdown-italic-face
  "Face name to use for italic text.")

(defvar ein:markdown-bold-face 'ein:markdown-bold-face
  "Face name to use for bold text.")

(defvar ein:markdown-strike-through-face 'ein:markdown-strike-through-face
  "Face name to use for strike-through text.")

(defvar ein:markdown-header-delimiter-face 'ein:markdown-header-delimiter-face
  "Face name to use as a base for header delimiters.")

(defvar ein:markdown-header-rule-face 'ein:markdown-header-rule-face
  "Face name to use as a base for header rules.")

(defvar ein:markdown-header-face 'ein:markdown-header-face
  "Face name to use as a base for headers.")

(defvar ein:markdown-header-face-1 'ein:markdown-header-face-1
  "Face name to use for level-1 headers.")

(defvar ein:markdown-header-face-2 'ein:markdown-header-face-2
  "Face name to use for level-2 headers.")

(defvar ein:markdown-header-face-3 'ein:markdown-header-face-3
  "Face name to use for level-3 headers.")

(defvar ein:markdown-header-face-4 'ein:markdown-header-face-4
  "Face name to use for level-4 headers.")

(defvar ein:markdown-header-face-5 'ein:markdown-header-face-5
  "Face name to use for level-5 headers.")

(defvar ein:markdown-header-face-6 'ein:markdown-header-face-6
  "Face name to use for level-6 headers.")

(defvar ein:markdown-inline-code-face 'ein:markdown-inline-code-face
  "Face name to use for inline code.")

(defvar ein:markdown-list-face 'ein:markdown-list-face
  "Face name to use for list markers.")

(defvar ein:markdown-blockquote-face 'ein:markdown-blockquote-face
  "Face name to use for blockquote.")

(defvar ein:markdown-pre-face 'ein:markdown-pre-face
  "Face name to use for preformatted text.")

(defvar ein:markdown-language-keyword-face 'ein:markdown-language-keyword-face
  "Face name to use for programming language identifiers.")

(defvar ein:markdown-language-info-face 'ein:markdown-language-info-face
  "Face name to use for programming info strings.")

(defvar ein:markdown-link-face 'ein:markdown-link-face
  "Face name to use for links.")

(defvar ein:markdown-missing-link-face 'ein:markdown-missing-link-face
  "Face name to use for links where the linked file does not exist.")

(defvar ein:markdown-reference-face 'ein:markdown-reference-face
  "Face name to use for reference.")

(defvar ein:markdown-footnote-marker-face 'ein:markdown-footnote-marker-face
  "Face name to use for footnote markers.")

(defvar ein:markdown-url-face 'ein:markdown-url-face
  "Face name to use for URLs.")

(defvar ein:markdown-link-title-face 'ein:markdown-link-title-face
  "Face name to use for reference link titles.")

(defvar ein:markdown-line-break-face 'ein:markdown-line-break-face
  "Face name to use for hard line breaks.")

(defvar ein:markdown-comment-face 'ein:markdown-comment-face
  "Face name to use for HTML comments.")

(defvar ein:markdown-math-face 'ein:markdown-math-face
  "Face name to use for LaTeX expressions.")

(defvar ein:markdown-metadata-key-face 'ein:markdown-metadata-key-face
  "Face name to use for metadata keys.")

(defvar ein:markdown-metadata-value-face 'ein:markdown-metadata-value-face
  "Face name to use for metadata values.")

(defvar ein:markdown-highlight-face 'ein:markdown-highlight-face
  "Face name to use for mouse highlighting.")

(defvar ein:markdown-markup-face 'ein:markdown-markup-face
  "Face name to use for markup elements.")

(make-obsolete-variable 'ein:markdown-italic-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-bold-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-strike-through-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-delimiter-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-rule-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-face-1 "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-face-2 "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-face-3 "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-face-4 "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-face-5 "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-header-face-6 "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-inline-code-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-list-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-blockquote-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-pre-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-language-keyword-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-language-info-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-link-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-missing-link-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-reference-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-footnote-marker-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-url-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-link-title-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-line-break-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-comment-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-math-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-metadata-key-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-metadata-value-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-highlight-face "Use face name directly" "v2.4")
(make-obsolete-variable 'ein:markdown-markup-face "Use face name directly" "v2.4")

(defgroup ein:markdown-faces nil
  "Faces used in ein:markdown Mode"
  :group 'ein:markdown
  :group 'faces)

(defface ein:markdown-italic-face
  '((t (:inherit italic)))
  "Face for italic text."
  :group 'ein:markdown-faces)

(defface ein:markdown-bold-face
  '((t (:inherit bold)))
  "Face for bold text."
  :group 'ein:markdown-faces)

(defface ein:markdown-strike-through-face
  '((t (:strike-through t)))
  "Face for strike-through text."
  :group 'ein:markdown-faces)

(defface ein:markdown-markup-face
  '((t (:inherit shadow :slant normal :weight normal)))
  "Face for markup elements."
  :group 'ein:markdown-faces)

(defface ein:markdown-header-rule-face
  '((t (:inherit ein:markdown-markup-face)))
  "Base face for headers rules."
  :group 'ein:markdown-faces)

(defface ein:markdown-header-delimiter-face
  '((t (:inherit ein:markdown-markup-face)))
  "Base face for headers hash delimiter."
  :group 'ein:markdown-faces)

(defface ein:markdown-list-face
  '((t (:inherit ein:markdown-markup-face)))
  "Face for list item markers."
  :group 'ein:markdown-faces)

(defface ein:markdown-blockquote-face
  '((t (:inherit font-lock-doc-face)))
  "Face for blockquote sections."
  :group 'ein:markdown-faces)

(defface ein:markdown-code-face
  '((t (:inherit fixed-pitch)))
  "Face for inline code, pre blocks, and fenced code blocks.
This may be used, for example, to add a contrasting background to
inline code fragments and code blocks."
  :group 'ein:markdown-faces)

(defface ein:markdown-inline-code-face
  '((t (:inherit (ein:markdown-code-face font-lock-constant-face))))
  "Face for inline code."
  :group 'ein:markdown-faces)

(defface ein:markdown-pre-face
  '((t (:inherit (ein:markdown-code-face font-lock-constant-face))))
  "Face for preformatted text."
  :group 'ein:markdown-faces)

(defface ein:markdown-table-face
  '((t (:inherit (ein:markdown-code-face))))
  "Face for tables."
  :group 'ein:markdown-faces)

(defface ein:markdown-language-keyword-face
  '((t (:inherit font-lock-type-face)))
  "Face for programming language identifiers."
  :group 'ein:markdown-faces)

(defface ein:markdown-language-info-face
  '((t (:inherit font-lock-string-face)))
  "Face for programming language info strings."
  :group 'ein:markdown-faces)

(defface ein:markdown-link-face
  '((t (:inherit link)))
  "Face for links."
  :group 'ein:markdown-faces)

(defface ein:markdown-missing-link-face
  '((t (:inherit font-lock-warning-face)))
  "Face for missing links."
  :group 'ein:markdown-faces)

(defface ein:markdown-reference-face
  '((t (:inherit ein:markdown-markup-face)))
  "Face for link references."
  :group 'ein:markdown-faces)

(define-obsolete-face-alias 'ein:markdown-footnote-face
  'ein:markdown-footnote-marker-face "v2.3")

(defface ein:markdown-footnote-marker-face
  '((t (:inherit ein:markdown-markup-face)))
  "Face for footnote markers."
  :group 'ein:markdown-faces)

(defface ein:markdown-footnote-text-face
  '((t (:inherit font-lock-comment-face)))
  "Face for footnote text."
  :group 'ein:markdown-faces)

(defface ein:markdown-url-face
  '((t (:inherit font-lock-string-face)))
  "Face for URLs that are part of markup.
For example, this applies to URLs in inline links:
[link text](http://example.com/)."
  :group 'ein:markdown-faces)

(defface ein:markdown-plain-url-face
  '((t (:inherit ein:markdown-link-face)))
  "Face for URLs that are also links.
For example, this applies to plain angle bracket URLs:
<http://example.com/>."
  :group 'ein:markdown-faces)

(defface ein:markdown-link-title-face
  '((t (:inherit font-lock-comment-face)))
  "Face for reference link titles."
  :group 'ein:markdown-faces)

(defface ein:markdown-line-break-face
  '((t (:inherit font-lock-constant-face :underline t)))
  "Face for hard line breaks."
  :group 'ein:markdown-faces)

(defface ein:markdown-comment-face
  '((t (:inherit font-lock-comment-face)))
  "Face for HTML comments."
  :group 'ein:markdown-faces)

(defface ein:markdown-math-face
  '((t (:inherit font-lock-string-face)))
  "Face for LaTeX expressions."
  :group 'ein:markdown-faces)

(defface ein:markdown-metadata-key-face
  '((t (:inherit font-lock-variable-name-face)))
  "Face for metadata keys."
  :group 'ein:markdown-faces)

(defface ein:markdown-metadata-value-face
  '((t (:inherit font-lock-string-face)))
  "Face for metadata values."
  :group 'ein:markdown-faces)

(defface ein:markdown-highlight-face
  '((t (:inherit highlight)))
  "Face for mouse highlighting."
  :group 'ein:markdown-faces)

(defface ein:markdown-hr-face
  '((t (:inherit ein:markdown-markup-face)))
  "Face for horizontal rules."
  :group 'ein:markdown-faces)

(defface ein:markdown-html-tag-name-face
  '((t (:inherit font-lock-type-face)))
  "Face for HTML tag names."
  :group 'ein:markdown-faces)

(defface ein:markdown-html-tag-delimiter-face
  '((t (:inherit ein:markdown-markup-face)))
  "Face for HTML tag delimiters."
  :group 'ein:markdown-faces)

(defface ein:markdown-html-attr-name-face
  '((t (:inherit font-lock-variable-name-face)))
  "Face for HTML attribute names."
  :group 'ein:markdown-faces)

(defface ein:markdown-html-attr-value-face
  '((t (:inherit font-lock-string-face)))
  "Face for HTML attribute values."
  :group 'ein:markdown-faces)

(defface ein:markdown-html-entity-face
  '((t (:inherit font-lock-variable-name-face)))
  "Face for HTML entities."
  :group 'ein:markdown-faces)

(defcustom ein:markdown-header-scaling nil
  "Whether to use variable-height faces for headers.
When non-nil, `markdown-header-face' will inherit from
`variable-pitch' and the scaling values in
`markdown-header-scaling-values' will be applied to
headers of levels one through six respectively."
  :type 'boolean
  :initialize 'custom-initialize-default
  :set (lambda (symbol value)
         (set-default symbol value)
         (ein:markdown-update-header-faces))
  :group 'ein:markdown-faces
  :package-version '(ein:markdown-mode . "2.2"))

(defcustom ein:markdown-header-scaling-values
  '(2.0 1.7 1.4 1.1 1.0 1.0)
  "List of scaling values for headers of level one through six.
Used when `markdown-header-scaling' is non-nil."
  :type 'list
  :initialize 'custom-initialize-default
  :set (lambda (symbol value)
         (set-default symbol value)
         (ein:markdown-update-header-faces))
  :group 'ein:markdown-faces)

(defun ein:markdown-make-header-faces ()
  "Build the faces used for ein:markdown headers."
  (unless (facep 'ein:markdown-header-face)
    (defface ein:markdown-header-face
      '((t (:inherit (font-lock-function-name-face) :weight bold)))
      "Base face for headers."
      :group 'ein:markdown-faces)
    (dotimes (num 6)
      (let* ((num1 (1+ num))
             (face-name (intern (format "ein:markdown-header-face-%s" num1)))
             (scale (float (nth num ein:markdown-header-scaling-values))))
        (eval
         `(defface ,face-name
            '((t (:inherit (variable-pitch ein:markdown-header-face) :height ,scale)))
            (format "Face for level %s headers.
You probably don't want to customize this face directly. Instead
you can customize the base face `markdown-header-face' or the
variable-height variable `markdown-header-scaling'." ,num1)
            :group 'ein:markdown-faces))))))

(defun ein:markdown-update-header-faces (&optional _scaling _scaling-values)
  "Update header faces using current values of
ein:markdown-header-scaling and
ein:markdown-header-scaling-values.  Arguments are ignored but
retained to avoid breakage."
  (ein:markdown-make-header-faces)
  (dotimes (num 6)
    (let* ((face-name (intern (format "ein:markdown-header-face-%s" (1+ num))))
           (scale (if ein:markdown-header-scaling
                      (float (nth num ein:markdown-header-scaling-values))
                    1.0)))
      (unless (get face-name 'saved-face) ; Don't update customized faces
        (set-face-attribute face-name nil :height scale)))))

(defun ein:markdown-syntactic-face (state)
  "Return font-lock face for characters with given STATE.
See `font-lock-syntactic-face-function' for details."
  (let ((in-comment (nth 4 state)))
    (cond
     (in-comment 'ein:markdown-comment-face)
     (t nil))))

(defcustom ein:markdown-list-item-bullets
  '("●" "◎" "○" "◆" "◇" "►" "•")
  "List of bullets to use for unordered lists.
It can contain any number of symbols, which will be repeated.
Depending on your font, some reasonable choices are:
♥ ● ◇ ✚ ✜ ☯ ◆ ♠ ♣ ♦ ❀ ◆ ◖ ▶ ► • ★ ▸."
  :group 'ein:markdown
  :type '(repeat (string :tag "Bullet character"))
  :package-version '(ein:markdown-mode . "2.3"))

(defun ein:markdown--footnote-marker-properties ()
  "Return a font-lock facespec expression for footnote marker text."
  `(face ein:markdown-footnote-marker-face))

(defun ein:markdown--pandoc-inline-footnote-properties ()
  "Return a font-lock facespec expression for Pandoc inline footnote text."
  `(face ein:markdown-footnote-text-face))

(define-obsolete-variable-alias
 'ein:markdown-mode-font-lock-keywords-basic
 'ein:markdown-mode-font-lock-keywords "v2.4")

(defvar ein:markdown-mode-font-lock-keywords
  `((ein:markdown-match-yaml-metadata-begin . ((1 'ein:markdown-markup-face)))
    (ein:markdown-match-yaml-metadata-end . ((1 'ein:markdown-markup-face)))
    (ein:markdown-match-yaml-metadata-key . ((1 'ein:markdown-metadata-key-face)
                                         (2 'ein:markdown-markup-face)
                                         (3 'ein:markdown-metadata-value-face)))
    (ein:markdown-fontify-tables)
    (ein:markdown-match-fenced-start-code-block . ((1 ein:markdown-markup-properties)
                                               (2 ein:markdown-markup-properties nil t)
                                               (3 ein:markdown-language-keyword-properties nil t)
                                               (4 ein:markdown-language-info-properties nil t)
                                               (5 ein:markdown-markup-properties nil t)))
    (ein:markdown-match-fenced-end-code-block . ((0 ein:markdown-markup-properties)))
    (ein:markdown-fontify-fenced-code-blocks)
    (ein:markdown-match-pre-blocks . ((0 'ein:markdown-pre-face)))
    (ein:markdown-fontify-headings)
    (ein:markdown-match-declarative-metadata . ((1 'ein:markdown-metadata-key-face)
                                              (2 'ein:markdown-markup-face)
                                              (3 'ein:markdown-metadata-value-face)))
    (ein:markdown-match-pandoc-metadata . ((1 'ein:markdown-markup-face)
                                       (2 'ein:markdown-markup-face)
                                       (3 'ein:markdown-metadata-value-face)))
    (ein:markdown-fontify-hrs)
    (ein:markdown-match-code . ((1 ein:markdown-markup-properties prepend)
                            (2 'ein:markdown-inline-code-face prepend)
                            (3 ein:markdown-markup-properties prepend)))
    (,ein:markdown-regex-kbd . ((1 ein:markdown-markup-properties)
                            (2 'ein:markdown-inline-code-face)
                            (3 ein:markdown-markup-properties)))
    (ein:markdown-fontify-angle-uris)
    (,ein:markdown-regex-email . 'ein:markdown-plain-url-face)
    (ein:markdown-match-html-tag . ((1 'ein:markdown-html-tag-delimiter-face t)
                                (2 'ein:markdown-html-tag-name-face t)
                                (3 'ein:markdown-html-tag-delimiter-face t)
                                ;; Anchored matcher for HTML tag attributes
                                (,ein:markdown-regex-html-attr
                                 ;; Before searching, move past tag
                                 ;; name; set limit at tag close.
                                 (progn
                                   (goto-char (match-end 2)) (match-end 3))
                                 nil
                                 . ((1 'ein:markdown-html-attr-name-face)
                                    (3 'ein:markdown-html-tag-delimiter-face nil t)
                                    (4 'ein:markdown-html-attr-value-face nil t)))))
    (,ein:markdown-regex-html-entity . 'ein:markdown-html-entity-face)
    (ein:markdown-fontify-list-items)
    (,ein:markdown-regex-footnote . ((1 ein:markdown-markup-properties)    ; [^
                                 (2 (ein:markdown--footnote-marker-properties)) ; label
                                 (3 ein:markdown-markup-properties)))  ; ]
    (,ein:markdown-regex-pandoc-inline-footnote . ((1 ein:markdown-markup-properties)   ; ^
                                               (2 ein:markdown-markup-properties)   ; [
                                               (3 (ein:markdown--pandoc-inline-footnote-properties)) ; text
                                               (4 ein:markdown-markup-properties))) ; ]
    (ein:markdown-match-includes . ((1 ein:markdown-markup-properties)
                                (2 ein:markdown-markup-properties nil t)
                                (3 ein:markdown-include-title-properties nil t)
                                (4 ein:markdown-markup-properties nil t)
                                (5 ein:markdown-markup-properties)
                                (6 'ein:markdown-url-face)
                                (7 ein:markdown-markup-properties)))
    (ein:markdown-fontify-inline-links)
    (ein:markdown-fontify-reference-links)
    (,ein:markdown-regex-reference-definition . ((1 'ein:markdown-markup-face) ; [
                                             (2 'ein:markdown-reference-face) ; label
                                             (3 'ein:markdown-markup-face)    ; ]
                                             (4 'ein:markdown-markup-face)    ; :
                                             (5 'ein:markdown-url-face)       ; url
                                             (6 'ein:markdown-link-title-face))) ; "title" (optional)
    (ein:markdown-fontify-plain-uris)
    ;; Math mode $..$
    (ein:markdown-match-math-single . ((1 'ein:markdown-markup-face prepend)
                                   (2 'ein:markdown-math-face append)
                                   (3 'ein:markdown-markup-face prepend)))
    ;; Math mode $$..$$
    (ein:markdown-match-math-double . ((1 'ein:markdown-markup-face prepend)
                                   (2 'ein:markdown-math-face append)
                                   (3 'ein:markdown-markup-face prepend)))
    ;; Math mode \[..\] and \\[..\\]
    (ein:markdown-match-math-display . ((1 'ein:markdown-markup-face prepend)
                                    (3 'ein:markdown-math-face append)
                                    (4 'ein:markdown-markup-face prepend)))
    (ein:markdown-match-bold . ((1 ein:markdown-markup-properties prepend)
                            (2 'ein:markdown-bold-face append)
                            (3 ein:markdown-markup-properties prepend)))
    (ein:markdown-match-italic . ((1 ein:markdown-markup-properties prepend)
                              (2 'ein:markdown-italic-face append)
                              (3 ein:markdown-markup-properties prepend)))
    (,ein:markdown-regex-strike-through . ((3 ein:markdown-markup-properties)
                                       (4 'ein:markdown-strike-through-face)
                                       (5 ein:markdown-markup-properties)))
    (,ein:markdown-regex-line-break . (1 'ein:markdown-line-break-face prepend))
    (ein:markdown-fontify-sub-superscripts)
    (ein:markdown-match-inline-attributes . ((0 ein:markdown-markup-properties prepend)))
    (ein:markdown-match-leanpub-sections . ((0 ein:markdown-markup-properties)))
    (ein:markdown-fontify-blockquotes))
  "Syntax highlighting for ein:markdown files.")

;; Footnotes
(defvar ein:markdown-footnote-counter 0
  "Counter for footnote numbers.")
(make-variable-buffer-local 'ein:markdown-footnote-counter)

(defconst ein:markdown-footnote-chars
  "[[:alnum:]-]"
  "Regex matching any character allowed in a footnote identifier.")

(defconst ein:markdown-regex-footnote-definition
  (concat "^ \\{0,3\\}\\[\\(\\^" ein:markdown-footnote-chars "*?\\)\\]:\\(?:[ \t]+\\|$\\)")
  "Regular expression matching a footnote definition, capturing the label.")


;;; Compatibility =============================================================

(defun ein:markdown-replace-regexp-in-string (regexp rep string)
  "Replace ocurrences of REGEXP with REP in STRING.
This is a compatibility wrapper to provide `replace-regexp-in-string'
in XEmacs 21."
  (if (featurep 'xemacs)
      (replace-in-string string regexp rep)
    (replace-regexp-in-string regexp rep string)))

;; `markdown-use-region-p' is a compatibility function which checks
;; for an active region, with fallbacks for older Emacsen and XEmacs.
(eval-and-compile
  (cond
   ;; Emacs 24 and newer
   ((fboundp 'use-region-p)
    (defalias 'ein:markdown-use-region-p 'use-region-p))
   ;; XEmacs
   ((fboundp 'region-active-p)
    (defalias 'ein:markdown-use-region-p 'region-active-p))))

;; Use new names for outline-mode functions in Emacs 25 and later.
(eval-and-compile
  (defalias 'ein:markdown-hide-sublevels
    (if (fboundp 'outline-hide-sublevels)
        'outline-hide-sublevels
      'hide-sublevels))
  (defalias 'ein:markdown-show-all
    (if (fboundp 'outline-show-all)
        'outline-show-all
      'show-all))
  (defalias 'ein:markdown-hide-body
    (if (fboundp 'outline-hide-body)
        'outline-hide-body
      'hide-body))
  (defalias 'ein:markdown-show-children
    (if (fboundp 'outline-show-children)
        'outline-show-children
      'show-children))
  (defalias 'ein:markdown-show-subtree
    (if (fboundp 'outline-show-subtree)
        'outline-show-subtree
      'show-subtree))
  (defalias 'ein:markdown-hide-subtree
    (if (fboundp 'outline-hide-subtree)
        'outline-hide-subtree
      'hide-subtree)))

;; Provide directory-name-p to Emacs 24
(defsubst ein:markdown-directory-name-p (name)
  "Return non-nil if NAME ends with a directory separator character.
Taken from `directory-name-p' from Emacs 25 and provided here for
backwards compatibility."
  (let ((len (length name))
        (lastc ?.))
    (if (> len 0)
        (setq lastc (aref name (1- len))))
    (or (= lastc ?/)
        (and (memq system-type '(windows-nt ms-dos))
             (= lastc ?\\)))))

;; Provide a function to find files recursively in Emacs 24.
(defalias 'ein:markdown-directory-files-recursively
  (if (fboundp 'directory-files-recursively)
      'directory-files-recursively
    (lambda (dir regexp)
    "Return list of all files under DIR that have file names matching REGEXP.
This function works recursively.  Files are returned in \"depth first\"
order, and files from each directory are sorted in alphabetical order.
Each file name appears in the returned list in its absolute form.
Based on `directory-files-recursively' from Emacs 25 and provided
here for backwards compatibility."
  (let ((result nil)
        (files nil)
        ;; When DIR is "/", remote file names like "/method:" could
        ;; also be offered.  We shall suppress them.
        (tramp-mode (and tramp-mode (file-remote-p (expand-file-name dir)))))
    (dolist (file (sort (file-name-all-completions "" dir)
                        'string<))
      (unless (member file '("./" "../"))
        (if (ein:markdown-directory-name-p file)
            (let* ((leaf (substring file 0 (1- (length file))))
                   (full-file (expand-file-name leaf dir)))
              (setq result
                    (nconc result (ein:markdown-directory-files-recursively
                                   full-file regexp))))
          (when (string-match-p regexp file)
            (push (expand-file-name file dir) files)))))
    (nconc result (nreverse files))))))

(defun ein:markdown-flyspell-check-word-p ()
  "Return t if `flyspell' should check word just before point.
Used for `flyspell-generic-check-word-predicate'."
  (save-excursion
    (goto-char (1- (point)))
    (not (or (ein:markdown-code-block-at-point-p)
             (ein:markdown-inline-code-at-point-p)
             (ein:markdown-in-comment-p)
             (let ((faces (get-text-property (point) 'face)))
               (if (listp faces)
                   (or (memq 'ein:markdown-reference-face faces)
                       (memq 'ein:markdown-markup-face faces)
                       (memq 'ein:markdown-plain-url-face faces)
                       (memq 'ein:markdown-inline-code-face faces)
                       (memq 'ein:markdown-url-face faces))
                 (memq faces '(ein:markdown-reference-face
                               ein:markdown-markup-face
                               ein:markdown-plain-url-face
                               ein:markdown-inline-code-face
                               ein:markdown-url-face))))))))

;;; ein:markdown Parsing Functions ================================================

(define-obsolete-function-alias
  'ein:markdown-cur-line-blank 'ein:markdown-cur-line-blank-p "v2.4")
(define-obsolete-function-alias
  'ein:markdown-next-line-blank 'ein:markdown-next-line-blank-p "v2.4")

(defun ein:markdown-cur-line-blank-p ()
  "Return t if the current line is blank and nil otherwise."
  (save-excursion
    (beginning-of-line)
    (looking-at-p ein:markdown-regex-blank-line)))

(defun ein:markdown-prev-line-blank ()
  "Return t if the previous line is blank and nil otherwise.
If we are at the first line, then consider the previous line to be blank."
  (or (= (line-beginning-position) (point-min))
      (save-excursion
        (forward-line -1)
        (looking-at ein:markdown-regex-blank-line))))

(defun ein:markdown-prev-line-blank-p ()
  "Like `markdown-prev-line-blank', but preserve `match-data'."
  (save-match-data (ein:markdown-prev-line-blank)))

(defun ein:markdown-next-line-blank-p ()
  "Return t if the next line is blank and nil otherwise.
If we are at the last line, then consider the next line to be blank."
  (or (= (line-end-position) (point-max))
      (save-excursion
        (forward-line 1)
        (ein:markdown-cur-line-blank-p))))

(defun ein:markdown-prev-line-indent ()
  "Return the number of leading whitespace characters in the previous line.
Return 0 if the current line is the first line in the buffer."
  (save-excursion
    (if (= (line-beginning-position) (point-min))
        0
      (forward-line -1)
      (current-indentation))))

(defun ein:markdown-next-line-indent ()
  "Return the number of leading whitespace characters in the next line.
Return 0 if line is the last line in the buffer."
  (save-excursion
    (if (= (line-end-position) (point-max))
        0
      (forward-line 1)
      (current-indentation))))

(defun ein:markdown-new-baseline ()
  "Determine if the current line begins a new baseline level.
Assume point is positioned at beginning of line."
  (or (looking-at ein:markdown-regex-header)
      (looking-at ein:markdown-regex-hr)
      (and (= (current-indentation) 0)
           (not (looking-at ein:markdown-regex-list))
           (ein:markdown-prev-line-blank))))

(defun ein:markdown-search-backward-baseline ()
  "Search backward baseline point with no indentation and not a list item."
  (end-of-line)
  (let (stop)
    (while (not (or stop (bobp)))
      (re-search-backward ein:markdown-regex-block-separator-noindent nil t)
      (when (match-end 2)
        (goto-char (match-end 2))
        (cond
         ((ein:markdown-new-baseline)
          (setq stop t))
         ((looking-at-p ein:markdown-regex-list)
          (setq stop nil))
         (t (setq stop t)))))))

(defun ein:markdown-update-list-levels (marker indent levels)
  "Update list levels given list MARKER, block INDENT, and current LEVELS.
Here, MARKER is a string representing the type of list, INDENT is an integer
giving the indentation, in spaces, of the current block, and LEVELS is a
list of the indentation levels of parent list items.  When LEVELS is nil,
it means we are at baseline (not inside of a nested list)."
  (cond
   ;; New list item at baseline.
   ((and marker (null levels))
    (setq levels (list indent)))
   ;; List item with greater indentation (four or more spaces).
   ;; Increase list level.
   ((and marker (>= indent (+ (car levels) 4)))
    (setq levels (cons indent levels)))
   ;; List item with greater or equal indentation (less than four spaces).
   ;; Do not increase list level.
   ((and marker (>= indent (car levels)))
    levels)
   ;; Lesser indentation level.
   ;; Pop appropriate number of elements off LEVELS list (e.g., lesser
   ;; indentation could move back more than one list level).  Note
   ;; that this block need not be the beginning of list item.
   ((< indent (car levels))
    (while (and (> (length levels) 1)
                (< indent (+ (cadr levels) 4)))
      (setq levels (cdr levels)))
    levels)
   ;; Otherwise, do nothing.
   (t levels)))

(defun ein:markdown-calculate-list-levels ()
  "Calculate list levels at point.
Return a list of the form (n1 n2 n3 ...) where n1 is the
indentation of the deepest nested list item in the branch of
the list at the point, n2 is the indentation of the parent
list item, and so on.  The depth of the list item is therefore
the length of the returned list.  If the point is not at or
immediately  after a list item, return nil."
  (save-excursion
    (let ((first (point)) levels indent pre-regexp)
      ;; Find a baseline point with zero list indentation
      (ein:markdown-search-backward-baseline)
      ;; Search for all list items between baseline and LOC
      (while (and (< (point) first)
                  (re-search-forward ein:markdown-regex-list first t))
        (setq pre-regexp (format "^\\(    \\|\t\\)\\{%d\\}" (1+ (length levels))))
        (beginning-of-line)
        (cond
         ;; Make sure this is not a header or hr
         ((ein:markdown-new-baseline) (setq levels nil))
         ;; Make sure this is not a line from a pre block
         ((looking-at-p pre-regexp))
         ;; If not, then update levels
         (t
          (setq indent (current-indentation))
          (setq levels (ein:markdown-update-list-levels (match-string 2)
                                                    indent levels))))
        (end-of-line))
      levels)))

(defun ein:markdown-prev-list-item (level)
  "Search backward from point for a list item with indentation LEVEL.
Set point to the beginning of the item, and return point, or nil
upon failure."
  (let (bounds indent prev)
    (setq prev (point))
    (forward-line -1)
    (setq indent (current-indentation))
    (while
        (cond
         ;; List item
         ((and (looking-at-p ein:markdown-regex-list)
               (setq bounds (ein:markdown-cur-list-item-bounds)))
          (cond
           ;; Stop and return point at item of equal indentation
           ((= (nth 3 bounds) level)
            (setq prev (point))
            nil)
           ;; Stop and return nil at item with lesser indentation
           ((< (nth 3 bounds) level)
            (setq prev nil)
            nil)
           ;; Stop at beginning of buffer
           ((bobp) (setq prev nil))
           ;; Continue at item with greater indentation
           ((> (nth 3 bounds) level) t)))
         ;; Stop at beginning of buffer
         ((bobp) (setq prev nil))
         ;; Continue if current line is blank
         ((ein:markdown-cur-line-blank-p) t)
         ;; Continue while indentation is the same or greater
         ((>= indent level) t)
         ;; Stop if current indentation is less than list item
         ;; and the next is blank
         ((and (< indent level)
               (ein:markdown-next-line-blank-p))
          (setq prev nil))
         ;; Stop at a header
         ((looking-at-p ein:markdown-regex-header) (setq prev nil))
         ;; Stop at a horizontal rule
         ((looking-at-p ein:markdown-regex-hr) (setq prev nil))
         ;; Otherwise, continue.
         (t t))
      (forward-line -1)
      (setq indent (current-indentation)))
    prev))

(defun ein:markdown-next-list-item (level)
  "Search forward from point for the next list item with indentation LEVEL.
Set point to the beginning of the item, and return point, or nil
upon failure."
  (let (bounds indent next)
    (setq next (point))
    (if (looking-at ein:markdown-regex-header-setext)
        (goto-char (match-end 0)))
    (forward-line)
    (setq indent (current-indentation))
    (while
        (cond
         ;; Stop at end of the buffer.
         ((eobp) nil)
         ;; Continue if the current line is blank
         ((ein:markdown-cur-line-blank-p) t)
         ;; List item
         ((and (looking-at-p ein:markdown-regex-list)
               (setq bounds (ein:markdown-cur-list-item-bounds)))
          (cond
           ;; Continue at item with greater indentation
           ((> (nth 3 bounds) level) t)
           ;; Stop and return point at item of equal indentation
           ((= (nth 3 bounds) level)
            (setq next (point))
            nil)
           ;; Stop and return nil at item with lesser indentation
           ((< (nth 3 bounds) level)
            (setq next nil)
            nil)))
         ;; Continue while indentation is the same or greater
         ((>= indent level) t)
         ;; Stop if current indentation is less than list item
         ;; and the previous line was blank.
         ((and (< indent level)
               (ein:markdown-prev-line-blank-p))
          (setq next nil))
         ;; Stop at a header
         ((looking-at-p ein:markdown-regex-header) (setq next nil))
         ;; Stop at a horizontal rule
         ((looking-at-p ein:markdown-regex-hr) (setq next nil))
         ;; Otherwise, continue.
         (t t))
      (forward-line)
      (setq indent (current-indentation)))
    next))

(defun ein:markdown-cur-list-item-end (level)
  "Move to end of list item with pre-marker indentation LEVEL.
Return the point at the end when a list item was found at the
original point.  If the point is not in a list item, do nothing."
  (let (indent)
    (forward-line)
    (setq indent (current-indentation))
    (while
        (cond
         ;; Stop at end of the buffer.
         ((eobp) nil)
         ;; Continue while indentation is the same or greater
         ((>= indent level) t)
         ;; Continue if the current line is blank
         ((looking-at ein:markdown-regex-blank-line) t)
         ;; Stop if current indentation is less than list item
         ;; and the previous line was blank.
         ((and (< indent level)
               (ein:markdown-prev-line-blank))
          nil)
         ;; Stop at a new list items of the same or lesser
         ;; indentation, headings, and horizontal rules.
         ((looking-at (concat "\\(?:" ein:markdown-regex-list
                              "\\|" ein:markdown-regex-header
                              "\\|" ein:markdown-regex-hr "\\)"))
          nil)
         ;; Otherwise, continue.
         (t t))
      (forward-line)
      (setq indent (current-indentation)))
    ;; Don't skip over whitespace for empty list items (marker and
    ;; whitespace only), just move to end of whitespace.
    (if (save-excursion
          (beginning-of-line)
          (looking-at (concat ein:markdown-regex-list "[ \t]*$")))
        (goto-char (match-end 3))
      (skip-chars-backward " \t\n"))
    (end-of-line)
    (point)))

(defun ein:markdown-cur-list-item-bounds ()
  "Return bounds for list item at point.
Return a list of the following form:

    (begin end indent nonlist-indent marker checkbox match)

The named components are:

  - begin: Position of beginning of list item, including leading indentation.
  - end: Position of the end of the list item, including list item text.
  - indent: Number of characters of indentation before list marker (an integer).
  - nonlist-indent: Number characters of indentation, list
    marker, and whitespace following list marker (an integer).
  - marker: String containing the list marker and following whitespace
            (e.g., \"- \" or \"* \").
  - checkbox: String containing the GFM checkbox portion, if any,
    including any trailing whitespace before the text
    begins (e.g., \"[x] \").
  - match: match data for ein:markdown-regex-list

As an example, for the following unordered list item

   - item

the returned list would be

    (1 14 3 5 \"- \" nil (1 6 1 4 4 5 5 6))

If the point is not inside a list item, return nil."
  (car (get-text-property (line-beginning-position) 'ein:markdown-list-item)))

(defun ein:markdown-list-item-at-point-p ()
  "Return t if there is a list item at the point and nil otherwise."
  (save-match-data (ein:markdown-cur-list-item-bounds)))

(defun ein:markdown-prev-list-item-bounds ()
  "Return bounds of previous item in the same list of any level.
The return value has the same form as that of
`markdown-cur-list-item-bounds'."
  (save-excursion
    (let ((cur-bounds (ein:markdown-cur-list-item-bounds))
          (beginning-of-list (save-excursion (ein:markdown-beginning-of-list)))
          stop)
      (when cur-bounds
        (goto-char (nth 0 cur-bounds))
        (while (and (not stop) (not (bobp))
                    (re-search-backward ein:markdown-regex-list
                                        beginning-of-list t))
          (unless (or (looking-at ein:markdown-regex-hr)
                      (ein:markdown-code-block-at-point-p))
            (setq stop (point))))
        (ein:markdown-cur-list-item-bounds)))))

(defun ein:markdown-next-list-item-bounds ()
  "Return bounds of next item in the same list of any level.
The return value has the same form as that of
`markdown-cur-list-item-bounds'."
  (save-excursion
    (let ((cur-bounds (ein:markdown-cur-list-item-bounds))
          (end-of-list (save-excursion (ein:markdown-end-of-list)))
          stop)
      (when cur-bounds
        (goto-char (nth 0 cur-bounds))
        (end-of-line)
        (while (and (not stop) (not (eobp))
                    (re-search-forward ein:markdown-regex-list
                                       end-of-list t))
          (unless (or (looking-at ein:markdown-regex-hr)
                      (ein:markdown-code-block-at-point-p))
            (setq stop (point))))
        (when stop
          (ein:markdown-cur-list-item-bounds))))))

(defun ein:markdown-beginning-of-list ()
  "Move point to beginning of list at point, if any."
  (interactive)
  (let ((orig-point (point))
        (list-begin (save-excursion
                      (ein:markdown-search-backward-baseline)
                      ;; Stop at next list item, regardless of the indentation.
                      (ein:markdown-next-list-item (point-max))
                      (when (looking-at ein:markdown-regex-list)
                        (point)))))
    (when (and list-begin (<= list-begin orig-point))
      (goto-char list-begin))))

(defun ein:markdown-end-of-list ()
  "Move point to end of list at point, if any."
  (interactive)
  (let ((start (point))
        (end (save-excursion
               (when (ein:markdown-beginning-of-list)
                 ;; Items can't have nonlist-indent <= 1, so this
                 ;; moves past all list items.
                 (ein:markdown-next-list-item 1)
                 (skip-syntax-backward "-")
                 (unless (eobp) (forward-char 1))
                 (point)))))
    (when (and end (>= end start))
      (goto-char end))))

(defun ein:markdown-up-list ()
  "Move point to beginning of parent list item."
  (interactive)
  (let ((cur-bounds (ein:markdown-cur-list-item-bounds)))
    (when cur-bounds
      (ein:markdown-prev-list-item (1- (nth 3 cur-bounds)))
      (let ((up-bounds (ein:markdown-cur-list-item-bounds)))
        (when (and up-bounds (< (nth 3 up-bounds) (nth 3 cur-bounds)))
          (point))))))

(defun ein:markdown-bounds-of-thing-at-point (thing)
  "Call `bounds-of-thing-at-point' for THING with slight modifications.
Does not include trailing newlines when THING is \\='line.  Handles the
end of buffer case by setting both endpoints equal to the value of
`point-max', since an empty region will trigger empty markup insertion.
Return bounds of form (beg . end) if THING is found, or nil otherwise."
  (let* ((bounds (bounds-of-thing-at-point thing))
         (a (car bounds))
         (b (cdr bounds)))
    (when bounds
      (when (eq thing 'line)
        (cond ((and (eobp) (ein:markdown-cur-line-blank-p))
               (setq a b))
              ((char-equal (char-before b) ?\^J)
               (setq b (1- b)))))
      (cons a b))))

(defun ein:markdown-reference-definition (reference)
  "Find out whether ein:markdown REFERENCE is defined.
REFERENCE should not include the square brackets.
When REFERENCE is defined, return a list of the form (text start end)
containing the definition text itself followed by the start and end
locations of the text.  Otherwise, return nil.
Leave match data for `markdown-regex-reference-definition'
intact additional processing."
  (let ((reference (downcase reference)))
    (save-excursion
      (goto-char (point-min))
      (catch 'found
        (while (re-search-forward ein:markdown-regex-reference-definition nil t)
          (when (string= reference (downcase (match-string-no-properties 2)))
            (throw 'found
                   (list (match-string-no-properties 5)
                         (match-beginning 5) (match-end 5)))))))))

(defun ein:markdown-get-defined-references ()
  "Return all defined reference labels and their line numbers (not
including square brackets)."
  (save-excursion
    (goto-char (point-min))
    (let (refs)
      (while (re-search-forward ein:markdown-regex-reference-definition nil t)
        (let ((target (match-string-no-properties 2)))
          (cl-pushnew
           (cons (downcase target)
                 (ein:markdown-line-number-at-pos (match-beginning 2)))
           refs :test #'equal :key #'car)))
      (reverse refs))))

(defun ein:markdown-get-used-uris ()
  "Return a list of all used URIs in the buffer."
  (save-excursion
    (goto-char (point-min))
    (let (uris)
      (while (re-search-forward
              (concat "\\(?:" ein:markdown-regex-link-inline
                      "\\|" ein:markdown-regex-angle-uri
                      "\\|" ein:markdown-regex-uri
                      "\\|" ein:markdown-regex-email
                      "\\)")
              nil t)
        (unless (or (ein:markdown-inline-code-at-point-p)
                    (ein:markdown-code-block-at-point-p))
          (cl-pushnew (or (match-string-no-properties 6)
                          (match-string-no-properties 10)
                          (match-string-no-properties 12)
                          (match-string-no-properties 13))
                      uris :test #'equal)))
      (reverse uris))))

(defun ein:markdown-inline-code-at-pos (pos)
  "Return non-nil if there is an inline code fragment at POS.
Return nil otherwise.  Set match data according to
`markdown-match-code' upon success.
This function searches the block for a code fragment that
contains the point using `markdown-match-code'.  We do this
because `thing-at-point-looking-at' does not work reliably with
`markdown-regex-code'.

The match data is set as follows:
Group 1 matches the opening backquotes.
Group 2 matches the code fragment itself, without backquotes.
Group 3 matches the closing backquotes."
  (save-excursion
    (goto-char pos)
    (let ((old-point (point))
          (end-of-block (progn (ein:markdown-end-of-text-block) (point)))
          found)
      (ein:markdown-beginning-of-text-block)
      (while (and (ein:markdown-match-code end-of-block)
                  (setq found t)
                  (< (match-end 0) old-point)))
      (and found                              ; matched something
           (<= (match-beginning 0) old-point) ; match contains old-point
           (> (match-end 0) old-point)))))

(defun ein:markdown-inline-code-at-pos-p (pos)
  "Return non-nil if there is an inline code fragment at POS.
Like `markdown-inline-code-at-pos`, but preserves match data."
  (save-match-data (ein:markdown-inline-code-at-pos pos)))

(defun ein:markdown-inline-code-at-point ()
  "Return non-nil if the point is at an inline code fragment.
See `markdown-inline-code-at-pos' for details."
  (ein:markdown-inline-code-at-pos (point)))

(defun ein:markdown-inline-code-at-point-p (&optional pos)
  "Return non-nil if there is inline code at the POS.
This is a predicate function counterpart to
`markdown-inline-code-at-point' which does not modify the match
data.  See `markdown-code-block-at-point-p' for code blocks."
  (save-match-data (ein:markdown-inline-code-at-pos (or pos (point)))))

(make-obsolete 'ein:markdown-code-at-point-p 'ein:markdown-inline-code-at-point-p "v2.2")

(defun ein:markdown-code-block-at-pos (pos)
  "Return match data list if there is a code block at POS.
Uses text properties at the beginning of the line position.
This includes pre blocks, tilde-fenced code blocks, and GFM
quoted code blocks.  Return nil otherwise."
  (let ((bol (save-excursion (goto-char pos) (line-beginning-position))))
    (or (get-text-property bol 'ein:markdown-pre)
        (let* ((bounds (ein:markdown-get-enclosing-fenced-block-construct pos))
               (second (cl-second bounds)))
          (if second
              ;; chunks are right open
              (when (< pos second)
                bounds)
            bounds)))))

;; Function was renamed to emphasize that it does not modify match-data.
(defalias 'ein:markdown-code-block-at-point 'ein:markdown-code-block-at-point-p)

(defun ein:markdown-code-block-at-point-p (&optional pos)
  "Return non-nil if there is a code block at the POS.
This includes pre blocks, tilde-fenced code blocks, and GFM
quoted code blocks.  This function does not modify the match
data.  See `markdown-inline-code-at-point-p' for inline code."
  (save-match-data (ein:markdown-code-block-at-pos (or pos (point)))))

(defun ein:markdown-heading-at-point (&optional pos)
  "Return non-nil if there is a heading at the POS.
Set match data for `markdown-regex-header'."
  (let ((match-data (get-text-property (or pos (point)) 'ein:markdown-heading)))
    (when match-data
      (set-match-data match-data)
      t)))

(defun ein:markdown-pipe-at-bol-p ()
  "Return non-nil if the line begins with a pipe symbol.
This may be useful for tables and Pandoc's line_blocks extension."
  (char-equal (char-after (line-beginning-position)) ?|))


;;; ein:markdown Font Lock Matching Functions =====================================

(defun ein:markdown-range-property-any (begin end prop prop-values)
  "Return t if PROP from BEGIN to END is equal to one of the given PROP-VALUES.
Also returns t if PROP is a list containing one of the PROP-VALUES.
Return nil otherwise."
  (let (props)
    (catch 'found
      (dolist (loc (number-sequence begin end))
        (when (setq props (get-text-property loc prop))
          (cond ((listp props)
                 ;; props is a list, check for membership
                 (dolist (val prop-values)
                   (when (memq val props) (throw 'found loc))))
                (t
                 ;; props is a scalar, check for equality
                 (dolist (val prop-values)
                   (when (eq val props) (throw 'found loc))))))))))

(defun ein:markdown-range-properties-exist (begin end props)
  (cl-loop
   for loc in (number-sequence begin end)
   with result = nil
   while (not
          (setq result
                (cl-some (lambda (prop) (get-text-property loc prop)) props)))
   finally return result))

(defun ein:markdown-match-inline-generic (regex last &optional faceless)
  "Match inline REGEX from the point to LAST.
When FACELESS is non-nil, do not return matches where faces have been applied."
  (when (re-search-forward regex last t)
    (let ((bounds (ein:markdown-code-block-at-pos (match-beginning 1)))
          (face (and faceless (text-property-not-all
                               (match-beginning 0) (match-end 0) 'face nil))))
      (cond
       ;; In code block: move past it and recursively search again
       (bounds
        (when (< (goto-char (cl-second bounds)) last)
          (ein:markdown-match-inline-generic regex last faceless)))
       ;; When faces are found in the match range, skip over the match and
       ;; recursively search again.
       (face
        (when (< (goto-char (match-end 0)) last)
          (ein:markdown-match-inline-generic regex last faceless)))
       ;; Keep match data and return t when in bounds.
       (t
        (<= (match-end 0) last))))))

(defun ein:markdown-match-code (last)
  "Match inline code fragments from point to LAST."
  (unless (bobp)
    (backward-char 1))
  (when (ein:markdown-search-until-condition
         (lambda ()
           (and
            ;; Advance point in case of failure, but without exceeding last.
            (goto-char (min (1+ (match-beginning 1)) last))
            (not (ein:markdown-in-comment-p (match-beginning 1)))
            (not (ein:markdown-in-comment-p (match-end 1)))
            (not (ein:markdown-code-block-at-pos (match-beginning 1)))))
         ein:markdown-regex-code last t)
      (set-match-data (list (match-beginning 1) (match-end 1)
                            (match-beginning 2) (match-end 2)
                            (match-beginning 3) (match-end 3)
                            (match-beginning 4) (match-end 4)))
      (goto-char (min (1+ (match-end 0)) last (point-max)))
      t))

(defun ein:markdown-match-bold (last)
  "Match inline bold from the point to LAST."
  (when (ein:markdown-match-inline-generic ein:markdown-regex-bold last)
    (let ((begin (match-beginning 2))
          (end (match-end 2)))
      (if (or (ein:markdown-inline-code-at-pos-p begin)
              (ein:markdown-inline-code-at-pos-p end)
              (ein:markdown-in-comment-p)
              (ein:markdown-range-property-any
               begin begin 'face '(ein:markdown-url-face
                                   ein:markdown-plain-url-face))
              (ein:markdown-range-property-any
               begin end 'face '(ein:markdown-hr-face
                                 ein:markdown-math-face)))
          (progn (goto-char (min (1+ begin) last))
                 (when (< (point) last)
                   (ein:markdown-match-italic last)))
        (set-match-data (list (match-beginning 2) (match-end 2)
                              (match-beginning 3) (match-end 3)
                              (match-beginning 4) (match-end 4)
                              (match-beginning 5) (match-end 5)))
        t))))

(defun ein:markdown-match-italic (last)
  "Match inline italics from the point to LAST."
  (let ((regex ein:markdown-regex-italic))
    (when (ein:markdown-match-inline-generic regex last)
      (let ((begin (match-beginning 1))
            (end (match-end 1)))
        (if (or (ein:markdown-inline-code-at-pos-p begin)
                (ein:markdown-inline-code-at-pos-p end)
                (ein:markdown-in-comment-p)
                (ein:markdown-range-property-any
                 begin begin 'face '(ein:markdown-url-face
                                     ein:markdown-plain-url-face))
                (ein:markdown-range-property-any
                 begin end 'face '(ein:markdown-bold-face
                                   ein:markdown-list-face
                                   ein:markdown-hr-face
                                   ein:markdown-math-face)))
            (progn (goto-char (min (1+ begin) last))
                   (when (< (point) last)
                     (ein:markdown-match-italic last)))
          (set-match-data (list (match-beginning 1) (match-end 1)
                                (match-beginning 2) (match-end 2)
                                (match-beginning 3) (match-end 3)
                                (match-beginning 4) (match-end 4)))
          t)))))

(defun ein:markdown-match-math-generic (regex last)
  "Match REGEX from point to LAST.
REGEX is either `markdown-regex-math-inline-single' for matching
$..$ or `markdown-regex-math-inline-double' for matching $$..$$."
  (when (and ein:markdown-enable-math (ein:markdown-match-inline-generic regex last))
    (let ((begin (match-beginning 1)) (end (match-end 1)))
      (prog1
          (if (or (ein:markdown-range-property-any
                   begin end 'face
                   '(ein:markdown-inline-code-face ein:markdown-bold-face))
                  (ein:markdown-range-properties-exist
                   begin end
                   (ein:markdown-get-fenced-block-middle-properties)))
              (ein:markdown-match-math-generic regex last)
            t)
        (goto-char (1+ (match-end 0)))))))

(defun ein:markdown-match-list-items (last)
  "Match list items from point to LAST."
  (let* ((first (point))
         (pos first)
         (prop 'ein:markdown-list-item)
         (bounds (car (get-text-property pos prop))))
    (while
        (and (or (null (setq bounds (car (get-text-property pos prop))))
                 (< (cl-first bounds) pos))
             (< (point) last)
             (setq pos (next-single-property-change pos prop nil last))
             (goto-char pos)))
    (when bounds
      (set-match-data (cl-seventh bounds))
      ;; Step at least one character beyond point. Otherwise
      ;; `font-lock-fontify-keywords-region' infloops.
      (goto-char (min (1+ (max (line-end-position) first))
                      (point-max)))
      t)))

(defun ein:markdown-match-math-single (last)
  "Match single quoted $..$ math from point to LAST."
  (ein:markdown-match-math-generic ein:markdown-regex-math-inline-single last))

(defun ein:markdown-match-math-double (last)
  "Match double quoted $$..$$ math from point to LAST."
  (ein:markdown-match-math-generic ein:markdown-regex-math-inline-double last))

(defun ein:markdown-match-math-display (last)
  "Match bracketed display math \[..\] and \\[..\\] from point to LAST."
  (ein:markdown-match-math-generic ein:markdown-regex-math-display last))

(defun ein:markdown-match-propertized-text (property last)
  "Match text with PROPERTY from point to LAST.
Restore match data previously stored in PROPERTY."
  (let ((saved (get-text-property (point) property))
        pos)
    (unless saved
      (setq pos (next-single-property-change (point) property nil last))
      (unless (= pos last)
        (setq saved (get-text-property pos property))))
    (when saved
      (set-match-data saved)
      ;; Step at least one character beyond point. Otherwise
      ;; `font-lock-fontify-keywords-region' infloops.
      (goto-char (min (1+ (max (match-end 0) (point)))
                      (point-max)))
      saved)))

(defun ein:markdown-match-pre-blocks (last)
  "Match preformatted blocks from point to LAST.
Use data stored in \\='ein:markdown-pre text property during syntax
analysis."
  (ein:markdown-match-propertized-text 'ein:markdown-pre last))

(defun ein:markdown-match-fenced-code-blocks (last)
  "Match fenced code blocks from the point to LAST."
  (ein:markdown-match-propertized-text 'ein:markdown-fenced-code last))

(defun ein:markdown-match-fenced-start-code-block (last)
  (ein:markdown-match-propertized-text 'ein:markdown-tilde-fence-begin last))

(defun ein:markdown-match-fenced-end-code-block (last)
  (ein:markdown-match-propertized-text 'ein:markdown-tilde-fence-end last))

(defun ein:markdown-match-blockquotes (last)
  "Match blockquotes from point to LAST.
Use data stored in \\='ein:markdown-blockquote text property during syntax
analysis."
  (ein:markdown-match-propertized-text 'ein:markdown-blockquote last))

(defun ein:markdown-match-hr (last)
  "Match horizontal rules comments from the point to LAST."
  (ein:markdown-match-propertized-text 'ein:markdown-hr last))

(defun ein:markdown-match-comments (last)
  "Match HTML comments from the point to LAST."
  (when (and (skip-syntax-forward "^<" last))
    (let ((beg (point)))
      (when (and (skip-syntax-forward "^>" last) (< (point) last))
        (forward-char)
        (set-match-data (list beg (point)))
        t))))

(defun ein:markdown-match-generic-links (last ref)
  "Match inline links from point to LAST.
When REF is non-nil, match reference links instead of standard
links with URLs.
This function should only be used during font-lock, as it
determines syntax based on the presence of faces for previously
processed elements."
  ;; Search for the next potential link (not in a code block).
  (let ((prohibited-faces '(ein:markdown-pre-face
                            ein:markdown-code-face
                            ein:markdown-inline-code-face
                            ein:markdown-comment-face))
        found)
    (while
        (and (not found) (< (point) last)
             (progn
               ;; Clear match data to test for a match after functions returns.
               (set-match-data nil)
               ;; Preliminary regular expression search so we can return
               ;; quickly upon failure.  This doesn't handle malformed links
               ;; or nested square brackets well, so if it passes we back up
               ;; continue with a more precise search.
               (re-search-forward
                (if ref
                    ein:markdown-regex-link-reference
                  ein:markdown-regex-link-inline)
                last 'limit)))
      ;; Keep searching if this is in a code block, inline code, or a
      ;; comment, or if it is include syntax. The link text portion
      ;; (group 3) may contain inline code or comments, but the
      ;; markup, URL, and title should not be part of such elements.
      (if (or (ein:markdown-range-property-any
               (match-beginning 0) (match-end 2) 'face prohibited-faces)
              (ein:markdown-range-property-any
               (match-beginning 4) (match-end 0) 'face prohibited-faces)
              (and (char-equal (char-after (line-beginning-position)) ?<)
                   (char-equal (char-after (1+ (line-beginning-position))) ?<)))
          (set-match-data nil)
        (setq found t))))
  ;; Match opening exclamation point (optional) and left bracket.
  (when (match-beginning 2)
    (let* ((bang (match-beginning 1))
           (first-begin (match-beginning 2))
           ;; Find end of block to prevent matching across blocks.
           (end-of-block (save-excursion
                           (progn
                             (goto-char (match-beginning 2))
                             (ein:markdown-end-of-text-block)
                             (point))))
           ;; Move over balanced expressions to closing right bracket.
           ;; Catch unbalanced expression errors and return nil.
           (first-end (condition-case nil
                           (and (goto-char first-begin)
                                (scan-sexps (point) 1))
                         (error nil)))
           ;; Continue with point at CONT-POINT upon failure.
           (cont-point (min (1+ first-begin) last))
           second-begin second-end url-begin url-end
           title-begin title-end)
      ;; When bracket found, in range, and followed by a left paren/bracket...
      (when (and first-end (< first-end end-of-block) (goto-char first-end)
                 (char-equal (char-after (point)) (if ref ?\[ ?\()))
        ;; Scan across balanced expressions for closing parenthesis/bracket.
        (setq second-begin (point)
              second-end (condition-case nil
                            (scan-sexps (point) 1)
                          (error nil)))
        ;; Check that closing parenthesis/bracket is in range.
        (if (and second-end (<= second-end end-of-block) (<= second-end last))
            (progn
              ;; Search for (optional) title inside closing parenthesis
              (when (and (not ref) (search-forward "\"" second-end t))
                (setq title-begin (1- (point))
                      title-end (and (goto-char second-end)
                                     (search-backward "\"" (1+ title-begin) t))
                      title-end (and title-end (1+ title-end))))
              ;; Store URL/reference range
              (setq url-begin (1+ second-begin)
                    url-end (1- (or title-begin second-end)))
              ;; Set match data, move point beyond link, and return
              (set-match-data
               (list (or bang first-begin) second-end  ; 0 - all
                     bang (and bang (1+ bang))         ; 1 - bang
                     first-begin (1+ first-begin)      ; 2 - markup
                     (1+ first-begin) (1- first-end)   ; 3 - link text
                     (1- first-end) first-end          ; 4 - markup
                     second-begin (1+ second-begin)    ; 5 - markup
                     url-begin url-end                 ; 6 - url/reference
                     title-begin title-end             ; 7 - title
                     (1- second-end) second-end))      ; 8 - markup
              ;; Nullify cont-point and leave point at end and
              (setq cont-point nil)
              (goto-char second-end))
          ;; If no closing parenthesis in range, update continuation point
          (setq cont-point (min end-of-block second-begin))))
      (cond
       ;; On failure, continue searching at cont-point
       ((and cont-point (< cont-point last))
        (goto-char cont-point)
        (ein:markdown-match-generic-links last ref))
       ;; No more text, return nil
       ((and cont-point (= cont-point last))
        nil)
       ;; Return t if a match occurred
       (t t)))))

(defun ein:markdown-match-angle-uris (last)
  "Match angle bracket URIs from point to LAST."
  (when (ein:markdown-match-inline-generic ein:markdown-regex-angle-uri last)
    (goto-char (1+ (match-end 0)))))

(defun ein:markdown-match-plain-uris (last)
  "Match plain URIs from point to LAST."
  (when (ein:markdown-match-inline-generic ein:markdown-regex-uri last t)
    (goto-char (1+ (match-end 0)))))

(defvar ein:markdown-conditional-search-function #'re-search-forward
  "Conditional search function used in `markdown-search-until-condition'.
Made into a variable to allow for dynamic let-binding.")

(defun ein:markdown-search-until-condition (condition &rest args)
  (let (ret)
    (while (and (not ret) (apply ein:markdown-conditional-search-function args))
      (setq ret (funcall condition)))
    ret))

(defun ein:markdown-match-generic-metadata (regexp last)
  "Match metadata declarations specified by REGEXP from point to LAST.
These declarations must appear inside a metadata block that begins at
the beginning of the buffer and ends with a blank line (or the end of
the buffer)."
  (let* ((first (point))
         (end-re "\n[ \t]*\n\\|\n\\'\\|\\'")
         (block-begin (goto-char 1))
         (block-end (re-search-forward end-re nil t)))
    (if (and block-end (> first block-end))
        ;; Don't match declarations if there is no metadata block or if
        ;; the point is beyond the block.  Move point to point-max to
        ;; prevent additional searches and return return nil since nothing
        ;; was found.
        (progn (goto-char (point-max)) nil)
      ;; If a block was found that begins before LAST and ends after
      ;; point, search for declarations inside it.  If the starting is
      ;; before the beginning of the block, start there. Otherwise,
      ;; move back to FIRST.
      (goto-char (if (< first block-begin) block-begin first))
      (if (re-search-forward regexp (min last block-end) t)
          ;; If a metadata declaration is found, set match-data and return t.
          (let ((key-beginning (match-beginning 1))
                (key-end (match-end 1))
                (markup-begin (match-beginning 2))
                (markup-end (match-end 2))
                (value-beginning (match-beginning 3)))
            (set-match-data (list key-beginning (point) ; complete metadata
                                  key-beginning key-end ; key
                                  markup-begin markup-end ; markup
                                  value-beginning (point))) ; value
            t)
        ;; Otherwise, move the point to last and return nil
        (goto-char last)
        nil))))

(defun ein:markdown-match-declarative-metadata (last)
  "Match declarative metadata from the point to LAST."
  (ein:markdown-match-generic-metadata ein:markdown-regex-declarative-metadata last))

(defun ein:markdown-match-pandoc-metadata (last)
  "Match Pandoc metadata from the point to LAST."
  (ein:markdown-match-generic-metadata ein:markdown-regex-pandoc-metadata last))

(defun ein:markdown-match-yaml-metadata-begin (last)
  (ein:markdown-match-propertized-text 'ein:markdown-yaml-metadata-begin last))

(defun ein:markdown-match-yaml-metadata-end (last)
  (ein:markdown-match-propertized-text 'ein:markdown-yaml-metadata-end last))

(defun ein:markdown-match-yaml-metadata-key (last)
  (ein:markdown-match-propertized-text 'ein:markdown-metadata-key last))

(defun ein:markdown-match-inline-attributes (last)
  "Match inline attributes from point to LAST."
  (when (ein:markdown-match-inline-generic ein:markdown-regex-inline-attributes last)
    (unless (or (ein:markdown-inline-code-at-pos-p (match-beginning 0))
                (ein:markdown-inline-code-at-pos-p (match-end 0))
                (ein:markdown-in-comment-p))
      t)))

(defun ein:markdown-match-leanpub-sections (last)
  "Match Leanpub section markers from point to LAST."
  (when (ein:markdown-match-inline-generic ein:markdown-regex-leanpub-sections last)
    (unless (or (ein:markdown-inline-code-at-pos-p (match-beginning 0))
                (ein:markdown-inline-code-at-pos-p (match-end 0))
                (ein:markdown-in-comment-p))
      t)))

(defun ein:markdown-match-includes (last)
  "Match include statements from point to LAST.
Sets match data for the following seven groups:
Group 1: opening two angle brackets
Group 2: opening title delimiter (optional)
Group 3: title text (optional)
Group 4: closing title delimiter (optional)
Group 5: opening filename delimiter
Group 6: filename
Group 7: closing filename delimiter"
  (when (ein:markdown-match-inline-generic ein:markdown-regex-include last)
    (let ((valid (not (or (ein:markdown-in-comment-p (match-beginning 0))
                          (ein:markdown-in-comment-p (match-end 0))
                          (ein:markdown-code-block-at-pos (match-beginning 0))))))
      (cond
       ;; Parentheses and maybe square brackets, but no curly braces:
       ;; match optional title in square brackets and file in parentheses.
       ((and valid (match-beginning 5)
             (not (match-beginning 8)))
        (set-match-data (list (match-beginning 1) (match-end 7)
                              (match-beginning 1) (match-end 1)
                              (match-beginning 2) (match-end 2)
                              (match-beginning 3) (match-end 3)
                              (match-beginning 4) (match-end 4)
                              (match-beginning 5) (match-end 5)
                              (match-beginning 6) (match-end 6)
                              (match-beginning 7) (match-end 7))))
       ;; Only square brackets present: match file in square brackets.
       ((and valid (match-beginning 2)
             (not (match-beginning 5))
             (not (match-beginning 7)))
        (set-match-data (list (match-beginning 1) (match-end 4)
                              (match-beginning 1) (match-end 1)
                              nil nil
                              nil nil
                              nil nil
                              (match-beginning 2) (match-end 2)
                              (match-beginning 3) (match-end 3)
                              (match-beginning 4) (match-end 4))))
       ;; Only curly braces present: match file in curly braces.
       ((and valid (match-beginning 8)
             (not (match-beginning 2))
             (not (match-beginning 5)))
        (set-match-data (list (match-beginning 1) (match-end 10)
                              (match-beginning 1) (match-end 1)
                              nil nil
                              nil nil
                              nil nil
                              (match-beginning 8) (match-end 8)
                              (match-beginning 9) (match-end 9)
                              (match-beginning 10) (match-end 10))))
       (t
        ;; Not a valid match, move to next line and search again.
        (forward-line)
        (when (< (point) last)
          (setq valid (ein:markdown-match-includes last)))))
      valid)))

(defun ein:markdown-match-html-tag (last)
  "Match HTML tags from point to LAST."
  (when (and ein:markdown-enable-html
             (ein:markdown-match-inline-generic ein:markdown-regex-html-tag last t))
    (set-match-data (list (match-beginning 0) (match-end 0)
                          (match-beginning 1) (match-end 1)
                          (match-beginning 2) (match-end 2)
                          (match-beginning 9) (match-end 9)))
    t))


;;; ein:markdown Font Fontification Functions =====================================

(defun ein:markdown--first-displayable (seq)
  "Return the first displayable character or string in SEQ.
SEQ may be an atom or a sequence."
  (let ((seq (if (listp seq) seq (list seq))))
    (cond ((stringp (car seq))
           (cl-find-if
            (lambda (str)
              (and (mapcar #'char-displayable-p (string-to-list str))))
            seq))
          ((characterp (car seq))
           (cl-find-if #'char-displayable-p seq)))))

(defun ein:markdown--marginalize-string (level)
  "Generate atx markup string of given LEVEL for left margin."
  (let ((margin-left-space-count
         (- ein:markdown-marginalize-headers-margin-width level)))
    (concat (make-string margin-left-space-count ? )
                           (make-string level ?#))))

(defun ein:markdown-fontify-headings (last)
  "Add text properties to headings from point to LAST."
  (when (ein:markdown-match-propertized-text 'ein:markdown-heading last)
    (let* ((level (ein:markdown-outline-level))
           (heading-face
            (intern (format "ein:markdown-header-face-%d" level)))
           (heading-props `(face ,heading-face))
           (left-markup-props
            `(face ein:markdown-header-delimiter-face
                   ,@(cond
                      (ein:markdown-marginalize-headers
                       `(display ((margin left-margin)
                                  ,(ein:markdown--marginalize-string level)))))))
           (right-markup-props
            `(face ein:markdown-header-delimiter-face))
           (rule-props `(face ein:markdown-header-rule-face)))
      (if (match-end 1)
          ;; Setext heading
          (progn (add-text-properties
                  (match-beginning 1) (match-end 1) heading-props)
                 (if (= level 1)
                     (add-text-properties
                      (match-beginning 2) (match-end 2) rule-props)
                   (add-text-properties
                    (match-beginning 3) (match-end 3) rule-props)))
        ;; atx heading
        (add-text-properties
         (match-beginning 4) (match-end 4) left-markup-props)
        (add-text-properties
         (match-beginning 5) (match-end 5) heading-props)
        (when (match-end 6)
          (add-text-properties
           (match-beginning 6) (match-end 6) right-markup-props))))
    t))

(defun ein:markdown-fontify-tables (last)
  (when (and (re-search-forward "|" last t)
             (ein:markdown-table-at-point-p))
    (font-lock-append-text-property
     (line-beginning-position) (min (1+ (line-end-position)) (point-max))
     'face 'ein:markdown-table-face)
    (forward-line 1)
    t))

(defun ein:markdown-fontify-blockquotes (last)
  "Apply font-lock properties to blockquotes from point to LAST."
  (when (ein:markdown-match-blockquotes last)
    (add-text-properties
     (match-beginning 1) (match-end 1)
     `(face ein:markdown-markup-face))
    (font-lock-append-text-property
     (match-beginning 0) (match-end 0) 'face 'ein:markdown-blockquote-face)
    t))

(defun ein:markdown-fontify-list-items (last)
  "Apply font-lock properties to list markers from point to LAST."
  (when (ein:markdown-match-list-items last)
    (add-text-properties
     (match-beginning 2) (match-end 2) '(face ein:markdown-list-face))
    t))

(defun ein:markdown-fontify-hrs (last)
  "Add text properties to horizontal rules from point to LAST."
  (when (ein:markdown-match-hr last)
    (add-text-properties
     (match-beginning 0) (match-end 0)
     `(face ein:markdown-hr-face
            font-lock-multiline t))
    t))

(defun ein:markdown-fontify-sub-superscripts (last)
  "Apply text properties to sub- and superscripts from point to LAST."
  (when (ein:markdown-search-until-condition
         (lambda () (and (not (ein:markdown-code-block-at-point-p))
                         (not (ein:markdown-inline-code-at-point-p))
                         (not (ein:markdown-in-comment-p))))
         ein:markdown-regex-sub-superscript last t)
    (let ((mp (list 'face 'ein:markdown-markup-face)))
      (add-text-properties (match-beginning 2) (match-end 2) mp)
      (add-text-properties (match-beginning 4) (match-end 4) mp)
      t)))


;;; Syntax Table ==============================================================

(defvar ein:markdown-mode-syntax-table
  (let ((tab (make-syntax-table text-mode-syntax-table)))
    (modify-syntax-entry ?\" "." tab)
    tab)
  "Syntax table for `ein:markdown-mode'.")


;;; Element Insertion =========================================================

(defun ein:markdown-ensure-blank-line-before ()
  "If previous line is not already blank, insert a blank line before point."
  (unless (bolp) (insert "\n"))
  (unless (or (bobp) (looking-back "\n\\s-*\n" nil)) (insert "\n")))

(defun ein:markdown-ensure-blank-line-after ()
  "If following line is not already blank, insert a blank line after point.
Return the point where it was originally."
  (save-excursion
    (unless (eolp) (insert "\n"))
    (unless (or (eobp) (looking-at-p "\n\\s-*\n")) (insert "\n"))))

(defun ein:markdown-wrap-or-insert (s1 s2 &optional thing beg end)
  "Insert the strings S1 and S2, wrapping around region or THING.
If a region is specified by the optional BEG and END arguments,
wrap the strings S1 and S2 around that region.
If there is an active region, wrap the strings S1 and S2 around
the region.  If there is not an active region but the point is at
THING, wrap that thing (which defaults to word).  Otherwise, just
insert S1 and S2 and place the point in between.  Return the
bounds of the entire wrapped string, or nil if nothing was wrapped
and S1 and S2 were only inserted."
  (let (a b bounds new-point)
    (cond
     ;; Given region
     ((and beg end)
      (setq a beg
            b end
            new-point (+ (point) (length s1))))
     ;; Active region
     ((ein:markdown-use-region-p)
      (setq a (region-beginning)
            b (region-end)
            new-point (+ (point) (length s1))))
     ;; Thing (word) at point
     ((setq bounds (ein:markdown-bounds-of-thing-at-point (or thing 'word)))
      (setq a (car bounds)
            b (cdr bounds)
            new-point (+ (point) (length s1))))
     ;; No active region and no word
     (t
      (setq a (point)
            b (point))))
    (goto-char b)
    (insert s2)
    (goto-char a)
    (insert s1)
    (when new-point (goto-char new-point))
    (if (= a b)
        nil
      (setq b (+ b (length s1) (length s2)))
      (cons a b))))

(defun ein:markdown-point-after-unwrap (cur prefix suffix)
  "Return desired position of point after an unwrapping operation.
CUR gives the position of the point before the operation.
Additionally, two cons cells must be provided.  PREFIX gives the
bounds of the prefix string and SUFFIX gives the bounds of the
suffix string."
  (cond ((< cur (cdr prefix)) (car prefix))
        ((< cur (car suffix)) (- cur (- (cdr prefix) (car prefix))))
        ((<= cur (cdr suffix))
         (- cur (+ (- (cdr prefix) (car prefix))
                   (- cur (car suffix)))))
        (t cur)))

(defun ein:markdown-unwrap-thing-at-point (regexp all text)
  "Remove prefix and suffix of thing at point and reposition the point.
When the thing at point matches REGEXP, replace the subexpression
ALL with the string in subexpression TEXT.  Reposition the point
in an appropriate location accounting for the removal of prefix
and suffix strings.  Return new bounds of string from group TEXT.
When REGEXP is nil, assumes match data is already set."
  (when (or (null regexp)
            (thing-at-point-looking-at regexp))
    (let ((cur (point))
          (prefix (cons (match-beginning all) (match-beginning text)))
          (suffix (cons (match-end text) (match-end all)))
          (bounds (cons (match-beginning text) (match-end text))))
      ;; Replace the thing at point
      (replace-match (match-string text) t t nil all)
      ;; Reposition the point
      (goto-char (ein:markdown-point-after-unwrap cur prefix suffix))
      ;; Adjust bounds
      (setq bounds (cons (car prefix)
                         (- (cdr bounds) (- (cdr prefix) (car prefix))))))))

(defun ein:markdown-unwrap-things-in-region (beg end regexp all text)
  "Remove prefix and suffix of all things in region from BEG to END.
When a thing in the region matches REGEXP, replace the
subexpression ALL with the string in subexpression TEXT.
Return a cons cell containing updated bounds for the region."
  (save-excursion
    (goto-char beg)
    (let ((removed 0) len-all len-text)
      (while (re-search-forward regexp (- end removed) t)
        (setq len-all (length (match-string-no-properties all)))
        (setq len-text (length (match-string-no-properties text)))
        (setq removed (+ removed (- len-all len-text)))
        (replace-match (match-string text) t t nil all))
      (cons beg (- end removed)))))

(defun ein:markdown-insert-hr (arg)
  "Insert or replace a horizonal rule.
By default, use the first element of `markdown-hr-strings'.  When
ARG is non-nil, as when given a prefix, select a different
element as follows.  When prefixed with \\[universal-argument],
use the last element of `markdown-hr-strings' instead.  When
prefixed with an integer from 1 to the length of
`markdown-hr-strings', use the element in that position instead."
  (interactive "*P")
  (when (thing-at-point-looking-at ein:markdown-regex-hr)
    (delete-region (match-beginning 0) (match-end 0)))
  (ein:markdown-ensure-blank-line-before)
  (cond ((equal arg '(4))
         (insert (car (reverse ein:markdown-hr-strings))))
        ((and (integerp arg) (> arg 0)
              (<= arg (length ein:markdown-hr-strings)))
         (insert (nth (1- arg) ein:markdown-hr-strings)))
        (t
         (insert (car ein:markdown-hr-strings))))
  (ein:markdown-ensure-blank-line-after))

(defun ein:markdown-insert-bold ()
  "Insert markup to make a region or word bold.
If there is an active region, make the region bold.  If the point
is at a non-bold word, make the word bold.  If the point is at a
bold word or phrase, remove the bold markup.  Otherwise, simply
insert bold delimiters and place the point in between them."
  (interactive)
  (let ((delim (if ein:markdown-bold-underscore "__" "**")))
    (if (ein:markdown-use-region-p)
        ;; Active region
        (let ((bounds (ein:markdown-unwrap-things-in-region
                       (region-beginning) (region-end)
                       ein:markdown-regex-bold 2 4)))
          (ein:markdown-wrap-or-insert delim delim nil (car bounds) (cdr bounds)))
      ;; Bold markup removal, bold word at point, or empty markup insertion
      (if (thing-at-point-looking-at ein:markdown-regex-bold)
          (ein:markdown-unwrap-thing-at-point nil 2 4)
        (ein:markdown-wrap-or-insert delim delim 'word nil nil)))))

(defun ein:markdown-insert-italic ()
  "Insert markup to make a region or word italic.
If there is an active region, make the region italic.  If the point
is at a non-italic word, make the word italic.  If the point is at an
italic word or phrase, remove the italic markup.  Otherwise, simply
insert italic delimiters and place the point in between them."
  (interactive)
  (let ((delim (if ein:markdown-italic-underscore "_" "*")))
    (if (ein:markdown-use-region-p)
        ;; Active region
        (let ((bounds (ein:markdown-unwrap-things-in-region
                       (region-beginning) (region-end)
                       ein:markdown-regex-italic 1 3)))
          (ein:markdown-wrap-or-insert delim delim nil (car bounds) (cdr bounds)))
      ;; Italic markup removal, italic word at point, or empty markup insertion
      (if (thing-at-point-looking-at ein:markdown-regex-italic)
          (ein:markdown-unwrap-thing-at-point nil 1 3)
        (ein:markdown-wrap-or-insert delim delim 'word nil nil)))))

(defun ein:markdown-insert-strike-through ()
  "Insert markup to make a region or word strikethrough.
If there is an active region, make the region strikethrough.  If the point
is at a non-bold word, make the word strikethrough.  If the point is at a
strikethrough word or phrase, remove the strikethrough markup.  Otherwise,
simply insert bold delimiters and place the point in between them."
  (interactive)
  (let ((delim "~~"))
    (if (ein:markdown-use-region-p)
        ;; Active region
        (let ((bounds (ein:markdown-unwrap-things-in-region
                       (region-beginning) (region-end)
                       ein:markdown-regex-strike-through 2 4)))
          (ein:markdown-wrap-or-insert delim delim nil (car bounds) (cdr bounds)))
      ;; Strikethrough markup removal, strikethrough word at point, or empty markup insertion
      (if (thing-at-point-looking-at ein:markdown-regex-strike-through)
          (ein:markdown-unwrap-thing-at-point nil 2 4)
        (ein:markdown-wrap-or-insert delim delim 'word nil nil)))))

(defun ein:markdown-insert-code ()
  "Insert markup to make a region or word an inline code fragment.
If there is an active region, make the region an inline code
fragment.  If the point is at a word, make the word an inline
code fragment.  Otherwise, simply insert code delimiters and
place the point in between them."
  (interactive)
  (if (ein:markdown-use-region-p)
      ;; Active region
      (let ((bounds (ein:markdown-unwrap-things-in-region
                     (region-beginning) (region-end)
                     ein:markdown-regex-code 1 3)))
        (ein:markdown-wrap-or-insert "`" "`" nil (car bounds) (cdr bounds)))
    ;; Code markup removal, code markup for word, or empty markup insertion
    (if (ein:markdown-inline-code-at-point)
        (ein:markdown-unwrap-thing-at-point nil 0 2)
      (ein:markdown-wrap-or-insert "`" "`" 'word nil nil))))

(defun ein:markdown-insert-kbd ()
  "Insert markup to wrap region or word in <kbd> tags.
If there is an active region, use the region.  If the point is at
a word, use the word.  Otherwise, simply insert <kbd> tags and
place the point in between them."
  (interactive)
  (if (ein:markdown-use-region-p)
      ;; Active region
      (let ((bounds (ein:markdown-unwrap-things-in-region
                     (region-beginning) (region-end)
                     ein:markdown-regex-kbd 0 2)))
        (ein:markdown-wrap-or-insert "<kbd>" "</kbd>" nil (car bounds) (cdr bounds)))
    ;; Markup removal, markup for word, or empty markup insertion
    (if (thing-at-point-looking-at ein:markdown-regex-kbd)
        (ein:markdown-unwrap-thing-at-point nil 0 2)
      (ein:markdown-wrap-or-insert "<kbd>" "</kbd>" 'word nil nil))))

(defun ein:markdown-insert-inline-link (text url &optional title)
  "Insert an inline link with TEXT pointing to URL.
Optionally, the user can provide a TITLE."
  (let ((cur (point)))
    (setq title (and title (concat " \"" title "\"")))
    (insert (concat "[" text "](" url title ")"))
    (cond ((not text) (goto-char (+ 1 cur)))
          ((not url) (goto-char (+ 3 (length text) cur))))))

(defun ein:markdown-insert-inline-image (text url &optional title)
  "Insert an inline link with alt TEXT pointing to URL.
Optionally, also provide a TITLE."
  (let ((cur (point)))
    (setq title (and title (concat " \"" title "\"")))
    (insert (concat "![" text "](" url title ")"))
    (cond ((not text) (goto-char (+ 2 cur)))
          ((not url) (goto-char (+ 4 (length text) cur))))))

(defun ein:markdown-insert-reference-link (text label &optional url title)
  "Insert a reference link and, optionally, a reference definition.
The link TEXT will be inserted followed by the optional LABEL.
If a URL is given, also insert a definition for the reference
LABEL according to `markdown-reference-location'.  If a TITLE is
given, it will be added to the end of the reference definition
and will be used to populate the title attribute when converted
to XHTML.  If URL is nil, insert only the link portion (for
example, when a reference label is already defined)."
  (insert (concat "[" text "][" label "]"))
  (when url
    (ein:markdown-insert-reference-definition
     (if (string-equal label "") text label)
     url title)))

(defun ein:markdown-insert-reference-image (text label &optional url title)
  "Insert a reference image and, optionally, a reference definition.
The alt TEXT will be inserted followed by the optional LABEL.
If a URL is given, also insert a definition for the reference
LABEL according to `markdown-reference-location'.  If a TITLE is
given, it will be added to the end of the reference definition
and will be used to populate the title attribute when converted
to XHTML.  If URL is nil, insert only the link portion (for
example, when a reference label is already defined)."
  (insert (concat "![" text "][" label "]"))
  (when url
    (ein:markdown-insert-reference-definition
     (if (string-equal label "") text label)
     url title)))

(defun ein:markdown-insert-reference-definition (label &optional url title)
  "Add definition for reference LABEL with URL and TITLE.
LABEL is a ein:markdown reference label without square brackets.
URL and TITLE are optional.  When given, the TITLE will
be used to populate the title attribute when converted to XHTML."
  ;; END specifies where to leave the point upon return
  (let ((end (point)))
    (cl-case ein:markdown-reference-location
      (end         (goto-char (point-max)))
      (immediately (ein:markdown-end-of-text-block))
      (subtree     (ein:markdown-end-of-subtree))
      (header      (ein:markdown-end-of-defun)))
    ;; Skip backwards over local variables.  This logic is similar to the one
    ;; used in ‘hack-local-variables’.
    (when (and enable-local-variables (eobp))
      (search-backward "\n\f" (max (- (point) 3000) (point-min)) :move)
      (when (let ((case-fold-search t))
              (search-forward "Local Variables:" nil :move))
        (beginning-of-line 0)
        (when (eq (char-before) ?\n) (backward-char))))
    (unless (or (ein:markdown-cur-line-blank-p)
                (thing-at-point-looking-at ein:markdown-regex-reference-definition))
      (insert "\n"))
    (insert "\n[" label "]: ")
    (if url
        (insert url)
      ;; When no URL is given, leave point at END following the colon
      (setq end (point)))
    (when (> (length title) 0)
      (insert " \"" title "\""))
    (unless (looking-at-p "\n")
      (insert "\n"))
    (goto-char end)
    (when url
      (message
       (ein:markdown--substitute-command-keys
        "Reference [%s] was defined, press \\[ein:markdown-do] to jump there")
       label))))

(define-obsolete-function-alias
  'ein:markdown-insert-inline-link-dwim 'ein:markdown-insert-link "v2.3")
(define-obsolete-function-alias
  'ein:markdown-insert-reference-link-dwim 'ein:markdown-insert-link "v2.3")

(defun ein:markdown--insert-link-or-image (image)
  "Interactively insert new or update an existing link or image.
When IMAGE is non-nil, insert an image.  Otherwise, insert a link.
This is an internal function called by
`markdown-insert-link' and `markdown-insert-image'."
  (cl-multiple-value-bind (begin end text uri ref title)
      (if (ein:markdown-use-region-p)
          ;; Use region as either link text or URL as appropriate.
          (let ((region (buffer-substring-no-properties
                         (region-beginning) (region-end))))
            (if (string-match ein:markdown-regex-uri region)
                ;; Region contains a URL; use it as such.
                (list (region-beginning) (region-end)
                      nil (match-string 0 region) nil nil)
              ;; Region doesn't contain a URL, so use it as text.
              (list (region-beginning) (region-end)
                    region nil nil nil)))
        ;; Extract and use properties of existing link, if any.
        (ein:markdown-link-at-pos (point)))
    (let* ((ref (when ref (concat "[" ref "]")))
           (defined-refs (append
                          (mapcar (lambda (ref) (concat "[" ref "]"))
                                  (mapcar #'car (ein:markdown-get-defined-references)))))
           (used-uris (ein:markdown-get-used-uris))
           (uri-or-ref (completing-read
                        "URL or [reference]: "
                        (append defined-refs used-uris)
                        nil nil (or uri ref)))
           (ref (cond ((string-match "\\`\\[\\(.*\\)\\]\\'" uri-or-ref)
                       (match-string 1 uri-or-ref))
                      ((string-equal "" uri-or-ref)
                       "")))
           (uri (unless ref uri-or-ref))
           (text-prompt (if image
                            "Alt text: "
                          (if ref
                              "Link text: "
                            "Link text (blank for plain URL): ")))
           (text (read-string text-prompt text))
           (text (if (= (length text) 0) nil text))
           (plainp (and uri (not text)))
           (implicitp (string-equal ref ""))
           (ref (if implicitp text ref))
           (definedp (and ref (ein:markdown-reference-definition ref)))
           (ref-url (unless (or uri definedp)
                      (completing-read "Reference URL: " used-uris)))
           (title (unless (or plainp definedp)
                    (read-string "Title (tooltip text, optional): " title)))
           (title (if (= (length title) 0) nil title)))
      (when (and image implicitp)
        (user-error "Reference required: implicit image references are invalid"))
      (when (and begin end)
        (delete-region begin end))
      (cond
       ((and (not image) uri text)
        (ein:markdown-insert-inline-link text uri title))
       ((and image uri text)
        (ein:markdown-insert-inline-image text uri title))
       ((and ref text)
        (if image
            (ein:markdown-insert-reference-image text (unless implicitp ref) nil title)
          (ein:markdown-insert-reference-link text (unless implicitp ref) nil title))
        (unless definedp
          (ein:markdown-insert-reference-definition ref ref-url title)))
       ((and (not image) uri)
        (ein:markdown-insert-uri uri))))))

(defun ein:markdown-insert-link ()
  "Insert new or update an existing link, with interactive prompts.
If the point is at an existing link or URL, update the link text,
URL, reference label, and/or title.  Otherwise, insert a new link.
The type of link inserted (inline, reference, or plain URL)
depends on which values are provided:

*   If a URL and TEXT are given, insert an inline link: [TEXT](URL).
*   If [REF] and TEXT are given, insert a reference link: [TEXT][REF].
*   If only TEXT is given, insert an implicit reference link: [TEXT][].
*   If only a URL is given, insert a plain link: <URL>.

In other words, to create an implicit reference link, leave the
URL prompt empty and to create a plain URL link, leave the link
text empty.

If there is an active region, use the text as the default URL, if
it seems to be a URL, or link text value otherwise.

If a given reference is not defined, this function will
additionally prompt for the URL and optional title.  In this case,
the reference definition is placed at the location determined by
`markdown-reference-location'.

Through updating the link, this function can be used to convert a
link of one type (inline, reference, or plain) to another type by
selectively adding or removing information via the prompts."
  (interactive)
  (ein:markdown--insert-link-or-image nil))

(defun ein:markdown-insert-image ()
  "Insert new or update an existing image, with interactive prompts.
If the point is at an existing image, update the alt text, URL,
reference label, and/or title. Otherwise, insert a new image.
The type of image inserted (inline or reference) depends on which
values are provided:

*   If a URL and ALT-TEXT are given, insert an inline image:
    ![ALT-TEXT](URL).
*   If [REF] and ALT-TEXT are given, insert a reference image:
    ![ALT-TEXT][REF].

If there is an active region, use the text as the default URL, if
it seems to be a URL, or alt text value otherwise.

If a given reference is not defined, this function will
additionally prompt for the URL and optional title.  In this case,
the reference definition is placed at the location determined by
`markdown-reference-location'.

Through updating the image, this function can be used to convert an
image of one type (inline or reference) to another type by
selectively adding or removing information via the prompts."
  (interactive)
  (ein:markdown--insert-link-or-image t))

(defun ein:markdown-insert-uri (&optional uri)
  "Insert markup for an inline URI.
If there is an active region, use it as the URI.  If the point is
at a URI, wrap it with angle brackets.  If the point is at an
inline URI, remove the angle brackets.  Otherwise, simply insert
angle brackets place the point between them."
  (interactive)
  (if (ein:markdown-use-region-p)
      ;; Active region
      (let ((bounds (ein:markdown-unwrap-things-in-region
                     (region-beginning) (region-end)
                     ein:markdown-regex-angle-uri 0 2)))
        (ein:markdown-wrap-or-insert "<" ">" nil (car bounds) (cdr bounds)))
    ;; Markup removal, URI at point, new URI, or empty markup insertion
    (if (thing-at-point-looking-at ein:markdown-regex-angle-uri)
        (ein:markdown-unwrap-thing-at-point nil 0 2)
      (if uri
          (insert "<" uri ">")
        (ein:markdown-wrap-or-insert "<" ">" 'url nil nil)))))

(defun ein:markdown-remove-header ()
  "Remove header markup if point is at a header.
Return bounds of remaining header text if a header was removed
and nil otherwise."
  (interactive "*")
  (or (ein:markdown-unwrap-thing-at-point ein:markdown-regex-header-atx 0 2)
      (ein:markdown-unwrap-thing-at-point ein:markdown-regex-header-setext 0 1)))

(defun ein:markdown-insert-header (&optional level text setext)
  "Insert or replace header markup.
The level of the header is specified by LEVEL and header text is
given by TEXT.  LEVEL must be an integer from 1 and 6, and the
default value is 1.
When TEXT is nil, the header text is obtained as follows.
If there is an active region, it is used as the header text.
Otherwise, the current line will be used as the header text.
If there is not an active region and the point is at a header,
remove the header markup and replace with level N header.
Otherwise, insert empty header markup and place the point in
between.
The style of the header will be atx (hash marks) unless
SETEXT is non-nil, in which case a setext-style (underlined)
header will be inserted."
  (interactive "p\nsHeader text: ")
  (setq level (min (max (or level 1) 1) (if setext 2 6)))
  ;; Determine header text if not given
  (when (null text)
    (if (ein:markdown-use-region-p)
        ;; Active region
        (setq text (delete-and-extract-region (region-beginning) (region-end)))
      ;; No active region
      (ein:markdown-remove-header)
      (setq text (delete-and-extract-region
                  (line-beginning-position) (line-end-position)))
      (when (and setext (string-match-p "^[ \t]*$" text))
        (setq text (read-string "Header text: "))))
    (setq text (ein:markdown-compress-whitespace-string text)))
  ;; Insertion with given text
  (ein:markdown-ensure-blank-line-before)
  (let (hdr)
    (cond (setext
           (setq hdr (make-string (string-width text) (if (= level 2) ?- ?=)))
           (insert text "\n" hdr))
          (t
           (setq hdr (make-string level ?#))
           (insert hdr " " text)
           (when (null ein:markdown-asymmetric-header) (insert " " hdr)))))
  (ein:markdown-ensure-blank-line-after)
  ;; Leave point at end of text
  (cond (setext
         (backward-char (1+ (string-width text))))
        ((null ein:markdown-asymmetric-header)
         (backward-char (1+ level)))))

(defun ein:markdown-insert-header-dwim (&optional arg setext)
  "Insert or replace header markup.
The level and type of the header are determined automatically by
the type and level of the previous header, unless a prefix
argument is given via ARG.
With a numeric prefix valued 1 to 6, insert a header of the given
level, with the type being determined automatically (note that
only level 1 or 2 setext headers are possible).

With a \\[universal-argument] prefix (i.e., when ARG is (4)),
promote the heading by one level.
With two \\[universal-argument] prefixes (i.e., when ARG is (16)),
demote the heading by one level.
When SETEXT is non-nil, prefer setext-style headers when
possible (levels one and two).

When there is an active region, use it for the header text.  When
the point is at an existing header, change the type and level
according to the rules above.
Otherwise, if the line is not empty, create a header using the
text on the current line as the header text.
Finally, if the point is on a blank line, insert empty header
markup (atx) or prompt for text (setext).
See `markdown-insert-header' for more details about how the
header text is determined."
  (interactive "*P")
  (let (level)
    (save-excursion
      (when (or (thing-at-point-looking-at ein:markdown-regex-header)
                (re-search-backward ein:markdown-regex-header nil t))
        ;; level of current or previous header
        (setq level (ein:markdown-outline-level))
        ;; match group 1 indicates a setext header
        (setq setext (match-end 1))))
    ;; check prefix argument
    (cond
     ((and (equal arg '(4)) level (> level 1)) ;; C-u
      (cl-decf level))
     ((and (equal arg '(16)) level (< level 6)) ;; C-u C-u
      (cl-incf level))
     (arg ;; numeric prefix
      (setq level (prefix-numeric-value arg))))
    ;; setext headers must be level one or two
    (and level (setq setext (and setext (<= level 2))))
    ;; insert the heading
    (ein:markdown-insert-header level nil setext)))

(defun ein:markdown-insert-header-setext-dwim (&optional arg)
  "Insert or replace header markup, with preference for setext.
See `markdown-insert-header-dwim' for details, including how ARG is handled."
  (interactive "*P")
  (ein:markdown-insert-header-dwim arg t))

(defun ein:markdown-insert-header-atx-1 ()
  "Insert a first level atx-style (hash mark) header.
See `markdown-insert-header'."
  (interactive "*")
  (ein:markdown-insert-header 1 nil nil))

(defun ein:markdown-insert-header-atx-2 ()
  "Insert a level two atx-style (hash mark) header.
See `markdown-insert-header'."
  (interactive "*")
  (ein:markdown-insert-header 2 nil nil))

(defun ein:markdown-insert-header-atx-3 ()
  "Insert a level three atx-style (hash mark) header.
See `markdown-insert-header'."
  (interactive "*")
  (ein:markdown-insert-header 3 nil nil))

(defun ein:markdown-insert-header-atx-4 ()
  "Insert a level four atx-style (hash mark) header.
See `markdown-insert-header'."
  (interactive "*")
  (ein:markdown-insert-header 4 nil nil))

(defun ein:markdown-insert-header-atx-5 ()
  "Insert a level five atx-style (hash mark) header.
See `markdown-insert-header'."
  (interactive "*")
  (ein:markdown-insert-header 5 nil nil))

(defun ein:markdown-insert-header-atx-6 ()
  "Insert a sixth level atx-style (hash mark) header.
See `markdown-insert-header'."
  (interactive "*")
  (ein:markdown-insert-header 6 nil nil))

(defun ein:markdown-insert-header-setext-1 ()
  "Insert a setext-style (underlined) first-level header.
See `markdown-insert-header'."
  (interactive "*")
  (ein:markdown-insert-header 1 nil t))

(defun ein:markdown-insert-header-setext-2 ()
  "Insert a setext-style (underlined) second-level header.
See `markdown-insert-header'."
  (interactive "*")
  (ein:markdown-insert-header 2 nil t))

(defun ein:markdown-blockquote-indentation (loc)
  "Return string containing necessary indentation for a blockquote at LOC.
Also see `markdown-pre-indentation'."
  (save-excursion
    (goto-char loc)
    (let* ((list-level (length (ein:markdown-calculate-list-levels)))
           (indent ""))
      (dotimes (_ list-level indent)
        (setq indent (concat indent "    "))))))

(defun ein:markdown-insert-blockquote ()
  "Start a blockquote section (or blockquote the region).
If Transient Mark mode is on and a region is active, it is used as
the blockquote text."
  (interactive)
  (if (ein:markdown-use-region-p)
      (ein:markdown-blockquote-region (region-beginning) (region-end))
    (ein:markdown-ensure-blank-line-before)
    (insert (ein:markdown-blockquote-indentation (point)) "> ")
    (ein:markdown-ensure-blank-line-after)))

(defun ein:markdown-block-region (beg end prefix)
  "Format the region using a block prefix.
Arguments BEG and END specify the beginning and end of the
region.  The characters PREFIX will appear at the beginning
of each line."
  (save-excursion
    (let* ((end-marker (make-marker))
           (beg-marker (make-marker))
           (prefix-without-trailing-whitespace
            (replace-regexp-in-string (rx (+ blank) eos) "" prefix)))
      ;; Ensure blank line after and remove extra whitespace
      (goto-char end)
      (skip-syntax-backward "-")
      (set-marker end-marker (point))
      (delete-horizontal-space)
      (ein:markdown-ensure-blank-line-after)
      ;; Ensure blank line before and remove extra whitespace
      (goto-char beg)
      (skip-syntax-forward "-")
      (delete-horizontal-space)
      (ein:markdown-ensure-blank-line-before)
      (set-marker beg-marker (point))
      ;; Insert PREFIX before each line
      (goto-char beg-marker)
      (while (and (< (line-beginning-position) end-marker)
                  (not (eobp)))
        ;; Don’t insert trailing whitespace.
        (insert (if (eolp) prefix-without-trailing-whitespace prefix))
        (forward-line)))))

(defun ein:markdown-blockquote-region (beg end)
  "Blockquote the region.
Arguments BEG and END specify the beginning and end of the region."
  (interactive "*r")
  (ein:markdown-block-region
   beg end (concat (ein:markdown-blockquote-indentation
                    (max (point-min) (1- beg))) "> ")))

(defun ein:markdown-pre-indentation (loc)
  "Return string containing necessary whitespace for a pre block at LOC.
Also see `markdown-blockquote-indentation'."
  (save-excursion
    (goto-char loc)
    (let* ((list-level (length (ein:markdown-calculate-list-levels)))
           indent)
      (dotimes (_ (1+ list-level) indent)
        (setq indent (concat indent "    "))))))

(defun ein:markdown-insert-pre ()
  "Start a preformatted section (or apply to the region).
If Transient Mark mode is on and a region is active, it is marked
as preformatted text."
  (interactive)
  (if (ein:markdown-use-region-p)
      (ein:markdown-pre-region (region-beginning) (region-end))
    (ein:markdown-ensure-blank-line-before)
    (insert (ein:markdown-pre-indentation (point)))
    (ein:markdown-ensure-blank-line-after)))

(defun ein:markdown-pre-region (beg end)
  "Format the region as preformatted text.
Arguments BEG and END specify the beginning and end of the region."
  (interactive "*r")
  (let ((indent (ein:markdown-pre-indentation (max (point-min) (1- beg)))))
    (ein:markdown-block-region beg end indent)))

(defun ein:markdown-electric-backquote (arg)
  "Insert a backquote.
The numeric prefix argument ARG says how many times to repeat the insertion.
Call `markdown-insert-gfm-code-block' interactively
if three backquotes inserted at the beginning of line."
  (interactive "*P")
  (self-insert-command (prefix-numeric-value arg)))

(defun ein:markdown-trim-whitespace (str)
  (ein:markdown-replace-regexp-in-string
   "\\(?:[[:space:]\r\n]+\\'\\|\\`[[:space:]\r\n]+\\)" "" str))

(defun ein:markdown-clean-language-string (str)
  (ein:markdown-replace-regexp-in-string
   "{\\.?\\|}" "" (ein:markdown-trim-whitespace str)))

(defun ein:markdown-validate-language-string (widget)
  (let ((str (widget-value widget)))
    (unless (string= str (ein:markdown-clean-language-string str))
      (widget-put widget :error (format "Invalid language spec: '%s'" str))
      widget)))

(defcustom ein:markdown-spaces-after-code-fence 1
  "Number of space characters to insert after a code fence.
\\<gfm-mode-map>\\[ein:markdown-insert-gfm-code-block] inserts this many spaces between an
opening code fence and an info string."
  :group 'ein:markdown
  :type 'integer
  :safe #'natnump
  :package-version '(ein:markdown-mode . "2.3"))

(defun ein:markdown-code-block-lang (&optional pos-prop)
  "Return the language name for a GFM or tilde fenced code block.
The beginning of the block may be described by POS-PROP,
a cons of (pos . prop) giving the position and property
at the beginning of the block."
  (or pos-prop
      (setq pos-prop
            (ein:markdown-max-of-seq
             #'car
             (cl-remove-if
              #'null
              (cl-mapcar
               #'ein:markdown-find-previous-prop
               (ein:markdown-get-fenced-block-begin-properties))))))
  (when pos-prop
    (goto-char (car pos-prop))
    (set-match-data (get-text-property (point) (cdr pos-prop)))
    ;; Note: Hard-coded group number assumes tilde
    ;; and GFM fenced code regexp groups agree.
    (let ((begin (match-beginning 3))
          (end (match-end 3)))
      (when (and begin end)
        ;; Fix language strings beginning with periods, like ".ruby".
        (when (eq (char-after begin) ?.)
          (setq begin (1+ begin)))
        (buffer-substring-no-properties begin end)))))

;;; Footnotes =================================================================

(defun ein:markdown-footnote-counter-inc ()
  "Increment `markdown-footnote-counter' and return the new value."
  (when (= ein:markdown-footnote-counter 0) ; hasn't been updated in this buffer yet.
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward (concat "^\\[\\^\\(" ein:markdown-footnote-chars "*?\\)\\]:")
                                (point-max) t)
        (let ((fn (string-to-number (match-string 1))))
          (when (> fn ein:markdown-footnote-counter)
            (setq ein:markdown-footnote-counter fn))))))
  (cl-incf ein:markdown-footnote-counter))

(defun ein:markdown-insert-footnote ()
  "Insert footnote with a new number and move point to footnote definition."
  (interactive)
  (let ((fn (ein:markdown-footnote-counter-inc)))
    (insert (format "[^%d]" fn))
    (ein:markdown-footnote-text-find-new-location)
    (ein:markdown-ensure-blank-line-before)
    (unless (ein:markdown-cur-line-blank-p)
      (insert "\n"))
    (insert (format "[^%d]: " fn))
    (ein:markdown-ensure-blank-line-after)))

(defun ein:markdown-footnote-text-find-new-location ()
  "Position the point at the proper location for a new footnote text."
  (cond
   ((eq ein:markdown-footnote-location 'end) (goto-char (point-max)))
   ((eq ein:markdown-footnote-location 'immediately) (ein:markdown-end-of-text-block))
   ((eq ein:markdown-footnote-location 'subtree) (ein:markdown-end-of-subtree))
   ((eq ein:markdown-footnote-location 'header) (ein:markdown-end-of-defun))))

(defun ein:markdown-footnote-kill ()
  "Kill the footnote at point.
The footnote text is killed (and added to the kill ring), the
footnote marker is deleted.  Point has to be either at the
footnote marker or in the footnote text."
  (interactive)
  (let ((marker-pos nil)
        (skip-deleting-marker nil)
        (starting-footnote-text-positions
         (ein:markdown-footnote-text-positions)))
    (when starting-footnote-text-positions
      ;; We're starting in footnote text, so mark our return position and jump
      ;; to the marker if possible.
      (let ((marker-pos (ein:markdown-footnote-find-marker
                         (cl-first starting-footnote-text-positions))))
        (if marker-pos
            (goto-char (1- marker-pos))
          ;; If there isn't a marker, we still want to kill the text.
          (setq skip-deleting-marker t))))
    ;; Either we didn't start in the text, or we started in the text and jumped
    ;; to the marker. We want to assume we're at the marker now and error if
    ;; we're not.
    (unless skip-deleting-marker
      (let ((marker (ein:markdown-footnote-delete-marker)))
        (unless marker
          (error "Not at a footnote"))
        ;; Even if we knew the text position before, it changed when we deleted
        ;; the label.
        (setq marker-pos (cl-second marker))
        (let ((new-text-pos (ein:markdown-footnote-find-text (cl-first marker))))
          (unless new-text-pos
            (error "No text for footnote `%s'" (cl-first marker)))
          (goto-char new-text-pos))))
    (let ((pos (ein:markdown-footnote-kill-text)))
      (goto-char (if starting-footnote-text-positions
                     pos
                   marker-pos)))))

(defun ein:markdown-footnote-delete-marker ()
  "Delete a footnote marker at point.
Returns a list (ID START) containing the footnote ID and the
start position of the marker before deletion.  If no footnote
marker was deleted, this function returns NIL."
  (let ((marker (ein:markdown-footnote-marker-positions)))
    (when marker
      (delete-region (cl-second marker) (cl-third marker))
      (butlast marker))))

(defun ein:markdown-footnote-kill-text ()
  "Kill footnote text at point.
Returns the start position of the footnote text before deletion,
or NIL if point was not inside a footnote text.

The killed text is placed in the kill ring (without the footnote
number)."
  (let ((fn (ein:markdown-footnote-text-positions)))
    (when fn
      (let ((text (delete-and-extract-region (cl-second fn) (cl-third fn))))
        (string-match (concat "\\[\\" (cl-first fn) "\\]:[[:space:]]*\\(\\(.*\n?\\)*\\)") text)
        (kill-new (match-string 1 text))
        (when (and (ein:markdown-cur-line-blank-p)
                   (ein:markdown-prev-line-blank-p)
                   (not (bobp)))
          (delete-region (1- (point)) (point)))
        (cl-second fn)))))

(defun ein:markdown-footnote-goto-text ()
  "Jump to the text of the footnote at point."
  (interactive)
  (let ((fn (car (ein:markdown-footnote-marker-positions))))
    (unless fn
      (user-error "Not at a footnote marker"))
    (let ((new-pos (ein:markdown-footnote-find-text fn)))
      (unless new-pos
        (error "No definition found for footnote `%s'" fn))
      (goto-char new-pos))))

(defun ein:markdown-footnote-return ()
  "Return from a footnote to its footnote number in the main text."
  (interactive)
  (let ((fn (save-excursion
              (car (ein:markdown-footnote-text-positions)))))
    (unless fn
      (user-error "Not in a footnote"))
    (let ((new-pos (ein:markdown-footnote-find-marker fn)))
      (unless new-pos
        (error "Footnote marker `%s' not found" fn))
      (goto-char new-pos))))

(defun ein:markdown-footnote-find-marker (id)
  "Find the location of the footnote marker with ID.
The actual buffer position returned is the position directly
following the marker's closing bracket.  If no marker is found,
NIL is returned."
  (save-excursion
    (goto-char (point-min))
    (when (re-search-forward (concat "\\[" id "\\]\\([^:]\\|\\'\\)") nil t)
      (skip-chars-backward "^]")
      (point))))

(defun ein:markdown-footnote-find-text (id)
  "Find the location of the text of footnote ID.
The actual buffer position returned is the position of the first
character of the text, after the footnote's identifier.  If no
footnote text is found, NIL is returned."
  (save-excursion
    (goto-char (point-min))
    (when (re-search-forward (concat "^ \\{0,3\\}\\[" id "\\]:") nil t)
      (skip-chars-forward "[ \t]")
      (point))))

(defun ein:markdown-footnote-marker-positions ()
  "Return the position and ID of the footnote marker point is on.
The return value is a list (ID START END).  If point is not on a
footnote, NIL is returned."
  ;; first make sure we're at a footnote marker
  (if (or (looking-back (concat "\\[\\^" ein:markdown-footnote-chars "*\\]?") (line-beginning-position))
          (looking-at-p (concat "\\[?\\^" ein:markdown-footnote-chars "*?\\]")))
      (save-excursion
        ;; move point between [ and ^:
        (if (looking-at-p "\\[")
            (forward-char 1)
          (skip-chars-backward "^["))
        (looking-at (concat "\\(\\^" ein:markdown-footnote-chars "*?\\)\\]"))
        (list (match-string 1) (1- (match-beginning 1)) (1+ (match-end 1))))))

(defun ein:markdown-footnote-text-positions ()
  "Return the start and end positions of the footnote text point is in.
The exact return value is a list of three elements: (ID START END).
The start position is the position of the opening bracket
of the footnote id.  The end position is directly after the
newline that ends the footnote.  If point is not in a footnote,
NIL is returned instead."
  (save-excursion
    (let (result)
      (move-beginning-of-line 1)
      ;; Try to find the label. If we haven't found the label and we're at a blank
      ;; or indented line, back up if possible.
      (while (and
              (not (and (looking-at ein:markdown-regex-footnote-definition)
                        (setq result (list (match-string 1) (point)))))
              (and (not (bobp))
                   (or (ein:markdown-cur-line-blank-p)
                       (>= (current-indentation) 4))))
        (forward-line -1))
      (when result
        ;; Advance if there is a next line that is either blank or indented.
        ;; (Need to check if we're on the last line, because
        ;; ein:markdown-next-line-blank-p returns true for last line in buffer.)
        (while (and (/= (line-end-position) (point-max))
                    (or (ein:markdown-next-line-blank-p)
                        (>= (ein:markdown-next-line-indent) 4)))
          (forward-line))
        ;; Move back while the current line is blank.
        (while (ein:markdown-cur-line-blank-p)
          (forward-line -1))
        ;; Advance to capture this line and a single trailing newline (if there
        ;; is one).
        (forward-line)
        (append result (list (point)))))))

(defun ein:markdown-get-defined-footnotes ()
  "Return a list of all defined footnotes.
Result is an alist of pairs (MARKER . LINE), where MARKER is the
footnote marker, a string, and LINE is the line number containing
the footnote definition.

For example, suppose the following footnotes are defined at positions
448 and 475:

\[^1]: First footnote here.
\[^marker]: Second footnote.

Then the returned list is: ((\"^1\" . 478) (\"^marker\" . 475))"
  (save-excursion
    (goto-char (point-min))
    (let (footnotes)
      (while (ein:markdown-search-until-condition
              (lambda () (and (not (ein:markdown-code-block-at-point-p))
                              (not (ein:markdown-inline-code-at-point-p))
                              (not (ein:markdown-in-comment-p))))
              ein:markdown-regex-footnote-definition nil t)
        (let ((marker (match-string-no-properties 1))
              (pos (match-beginning 0)))
          (unless (zerop (length marker))
            (cl-pushnew (cons marker pos) footnotes :test #'equal))))
      (reverse footnotes))))


;;; Element Removal ===========================================================

(defun ein:markdown-kill-thing-at-point ()
  "Kill thing at point and add important text, without markup, to kill ring.
Possible things to kill include (roughly in order of precedence):
inline code, headers, horizonal rules, links (add link text to
kill ring), images (add alt text to kill ring), angle uri, email
addresses, bold, italics, reference definition (add URI to kill
ring), footnote markers and text (kill both marker and text, add
text to kill ring), and list items."
  (interactive "*")
  (let (val)
    (cond
     ;; Inline code
     ((ein:markdown-inline-code-at-point)
      (kill-new (match-string 2))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; ATX header
     ((thing-at-point-looking-at ein:markdown-regex-header-atx)
      (kill-new (match-string 2))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; Setext header
     ((thing-at-point-looking-at ein:markdown-regex-header-setext)
      (kill-new (match-string 1))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; Horizonal rule
     ((thing-at-point-looking-at ein:markdown-regex-hr)
      (kill-new (match-string 0))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; Inline link or image (add link or alt text to kill ring)
     ((thing-at-point-looking-at ein:markdown-regex-link-inline)
      (kill-new (match-string 3))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; Reference link or image (add link or alt text to kill ring)
     ((thing-at-point-looking-at ein:markdown-regex-link-reference)
      (kill-new (match-string 3))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; Angle URI (add URL to kill ring)
     ((thing-at-point-looking-at ein:markdown-regex-angle-uri)
      (kill-new (match-string 2))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; Email address in angle brackets (add email address to kill ring)
     ((thing-at-point-looking-at ein:markdown-regex-email)
      (kill-new (match-string 1))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; Bold
     ((thing-at-point-looking-at ein:markdown-regex-bold)
      (kill-new (match-string 4))
      (delete-region (match-beginning 2) (match-end 2)))
     ;; Italics
     ((thing-at-point-looking-at ein:markdown-regex-italic)
      (kill-new (match-string 3))
      (delete-region (match-beginning 1) (match-end 1)))
     ;; Strikethrough
     ((thing-at-point-looking-at ein:markdown-regex-strike-through)
      (kill-new (match-string 4))
      (delete-region (match-beginning 2) (match-end 2)))
     ;; Footnote marker (add footnote text to kill ring)
     ((thing-at-point-looking-at ein:markdown-regex-footnote)
      (ein:markdown-footnote-kill))
     ;; Footnote text (add footnote text to kill ring)
     ((setq val (ein:markdown-footnote-text-positions))
      (ein:markdown-footnote-kill))
     ;; Reference definition (add URL to kill ring)
     ((thing-at-point-looking-at ein:markdown-regex-reference-definition)
      (kill-new (match-string 5))
      (delete-region (match-beginning 0) (match-end 0)))
     ;; List item
     ((setq val (ein:markdown-cur-list-item-bounds))
      (kill-new (delete-and-extract-region (cl-first val) (cl-second val))))
     (t
      (user-error "Nothing found at point to kill")))))

(defun ein:markdown-kill-outline ()
  "Kill visible heading and add it to `kill-ring'."
  (interactive)
  (save-excursion
    (ein:markdown-outline-previous)
    (kill-region (point) (progn (ein:markdown-outline-next) (point)))))

(defun ein:markdown-kill-block ()
  "Kill visible code block, list item, or blockquote and add it to `kill-ring'."
  (interactive)
  (save-excursion
    (ein:markdown-backward-block)
    (kill-region (point) (progn (ein:markdown-forward-block) (point)))))


;;; Indentation ===============================================================

(defun ein:markdown-indent-find-next-position (cur-pos positions)
  "Return the position after the index of CUR-POS in POSITIONS.
Positions are calculated by `markdown-calc-indents'."
  (while (and positions
              (not (equal cur-pos (car positions))))
    (setq positions (cdr positions)))
  (or (cadr positions) 0))

(define-obsolete-function-alias 'ein:markdown-exdent-find-next-position
  'ein:markdown-outdent-find-next-position "v2.3")

(defun ein:markdown-outdent-find-next-position (cur-pos positions)
  "Return the maximal element that precedes CUR-POS from POSITIONS.
Positions are calculated by `markdown-calc-indents'."
  (let ((result 0))
    (dolist (i positions)
      (when (< i cur-pos)
        (setq result (max result i))))
    result))

(defun ein:markdown-indent-line ()
  "Indent the current line using some heuristics.
If the _previous_ command was either `markdown-enter-key' or
`markdown-cycle', then we should cycle to the next
reasonable indentation position.  Otherwise, we could have been
called directly by `markdown-enter-key', by an initial call of
`markdown-cycle', or indirectly by `auto-fill-mode'.  In
these cases, indent to the default position.
Positions are calculated by `markdown-calc-indents'."
  (interactive)
  (let ((positions (ein:markdown-calc-indents))
        (point-pos (current-column))
        (_ (back-to-indentation))
        (cur-pos (current-column)))
    (if (not (equal this-command 'ein:markdown-cycle))
        (indent-line-to (car positions))
      (setq positions (sort (delete-dups positions) '<))
      (let* ((next-pos (ein:markdown-indent-find-next-position cur-pos positions))
             (new-point-pos (max (+ point-pos (- next-pos cur-pos)) 0)))
        (indent-line-to next-pos)
        (move-to-column new-point-pos)))))

(defun ein:markdown-calc-indents ()
  "Return a list of indentation columns to cycle through.
The first element in the returned list should be considered the
default indentation level.  This function does not worry about
duplicate positions, which are handled up by calling functions."
  (let (pos prev-line-pos positions)

    ;; Indentation of previous line
    (setq prev-line-pos (ein:markdown-prev-line-indent))
    (setq positions (cons prev-line-pos positions))

    ;; Indentation of previous non-list-marker text
    (when (setq pos (save-excursion
                      (forward-line -1)
                      (when (looking-at ein:markdown-regex-list)
                        (- (match-end 3) (match-beginning 0)))))
      (setq positions (cons pos positions)))

    ;; Indentation required for a pre block in current context
    (setq pos (length (ein:markdown-pre-indentation (point))))
    (setq positions (cons pos positions))

    ;; Indentation of the previous line + tab-width
    (if prev-line-pos
        (setq positions (cons (+ prev-line-pos tab-width) positions))
      (setq positions (cons tab-width positions)))

    ;; Indentation of the previous line - tab-width
    (if (and prev-line-pos (> prev-line-pos tab-width))
        (setq positions (cons (- prev-line-pos tab-width) positions)))

    ;; Indentation of all preceeding list markers (when in a list)
    (when (setq pos (ein:markdown-calculate-list-levels))
      (setq positions (append pos positions)))

    ;; First column
    (setq positions (cons 0 positions))

    ;; Return reversed list
    (reverse positions)))

(defun ein:markdown-enter-key ()
  "Handle RET depending on the context.
If the point is at a table, move to the next row.  Otherwise,
indent according to value of `markdown-indent-on-enter'.
When it is nil, simply call `newline'.  Otherwise, indent the next line
following RET using `markdown-indent-line'.  Furthermore, when it
is set to \\='indent-and-new-item and the point is in a list item,
start a new item with the same indentation. If the point is in an
empty list item, remove it (so that pressing RET twice when in a
list simply adds a blank line)."
  (interactive)
  (cond
   ;; Table
   ((ein:markdown-table-at-point-p)
    (call-interactively #'ein:markdown-table-next-row))
   ;; Indent non-table text
   (ein:markdown-indent-on-enter
    (let (bounds)
      (if (and (memq ein:markdown-indent-on-enter '(indent-and-new-item))
               (setq bounds (ein:markdown-cur-list-item-bounds)))
          (let ((beg (cl-first bounds))
                (end (cl-second bounds))
                (length (cl-fourth bounds)))
            ;; Point is in a list item
            (if (= (- end beg) length)
                ;; Delete blank list
                (progn
                  (delete-region beg end)
                  (newline)
                  (ein:markdown-indent-line))
              (call-interactively #'ein:markdown-insert-list-item)))
        ;; Point is not in a list
        (newline)
        (ein:markdown-indent-line))))
   ;; Insert a raw newline
   (t (newline))))

(define-obsolete-function-alias 'ein:markdown-exdent-or-delete
  'ein:markdown-outdent-or-delete "v2.3")

(defun ein:markdown-outdent-or-delete (arg)
  "Handle BACKSPACE by cycling through indentation points.
When BACKSPACE is pressed, if there is only whitespace
before the current point, then outdent the line one level.
Otherwise, do normal delete by repeating
`backward-delete-char-untabify' ARG times."
  (interactive "*p")
  (if (use-region-p)
      (backward-delete-char-untabify arg)
    (let ((cur-pos (current-column))
          (start-of-indention (save-excursion
                                (back-to-indentation)
                                (current-column)))
          (positions (ein:markdown-calc-indents)))
      (if (and (> cur-pos 0) (= cur-pos start-of-indention))
          (indent-line-to (ein:markdown-outdent-find-next-position cur-pos positions))
        (backward-delete-char-untabify arg)))))

(defun ein:markdown-find-leftmost-column (beg end)
  "Find the leftmost column in the region from BEG to END."
  (let ((mincol 1000))
    (save-excursion
      (goto-char beg)
      (while (< (point) end)
        (back-to-indentation)
        (unless (looking-at-p "[ \t]*$")
          (setq mincol (min mincol (current-column))))
        (forward-line 1)
        ))
    mincol))

(defun ein:markdown-indent-region (beg end arg)
  "Indent the region from BEG to END using some heuristics.
When ARG is non-nil, outdent the region instead.
See `markdown-indent-line' and `markdown-indent-line'."
  (interactive "*r\nP")
  (let* ((positions (sort (delete-dups (ein:markdown-calc-indents)) '<))
         (leftmostcol (ein:markdown-find-leftmost-column beg end))
         (next-pos (if arg
                       (ein:markdown-outdent-find-next-position leftmostcol positions)
                     (ein:markdown-indent-find-next-position leftmostcol positions))))
    (indent-rigidly beg end (- next-pos leftmostcol))
    (setq deactivate-mark nil)))

(define-obsolete-function-alias 'ein:markdown-exdent-region
  'ein:markdown-outdent-region "v2.3")

(defun ein:markdown-outdent-region (beg end)
  "Call `markdown-indent-region' on region from BEG to END with prefix."
  (interactive "*r")
  (ein:markdown-indent-region beg end t))


;;; Markup Completion =========================================================

(defconst ein:markdown-complete-alist
  '((ein:markdown-regex-header-atx . ein:markdown-complete-atx)
    (ein:markdown-regex-header-setext . ein:markdown-complete-setext)
    (ein:markdown-regex-hr . ein:markdown-complete-hr))
  "Association list of form (regexp . function) for markup completion.")

(defun ein:markdown-incomplete-atx-p ()
  "Return t if ATX header markup is incomplete and nil otherwise.
Assumes match data is available for `markdown-regex-header-atx'.
Checks that the number of trailing hash marks equals the number of leading
hash marks, that there is only a single space before and after the text,
and that there is no extraneous whitespace in the text."
  (or
   ;; Number of starting and ending hash marks differs
   (not (= (length (match-string 1)) (length (match-string 3))))
   ;; When the header text is not empty...
   (and (> (length (match-string 2)) 0)
        ;; ...if there are extra leading, trailing, or interior spaces
        (or (not (= (match-beginning 2) (1+ (match-end 1))))
            (not (= (match-beginning 3) (1+ (match-end 2))))
            (string-match-p "[ \t\n]\\{2\\}" (match-string 2))))
   ;; When the header text is empty...
   (and (= (length (match-string 2)) 0)
        ;; ...if there are too many or too few spaces
        (not (= (match-beginning 3) (+ (match-end 1) 2))))))

(defun ein:markdown-complete-atx ()
  "Complete and normalize ATX headers.
Add or remove hash marks to the end of the header to match the
beginning.  Ensure that there is only a single space between hash
marks and header text.  Removes extraneous whitespace from header text.
Assumes match data is available for `markdown-regex-header-atx'.
Return nil if markup was complete and non-nil if markup was completed."
  (when (ein:markdown-incomplete-atx-p)
    (let* ((new-marker (make-marker))
           (new-marker (set-marker new-marker (match-end 2))))
      ;; Hash marks and spacing at end
      (goto-char (match-end 2))
      (delete-region (match-end 2) (match-end 3))
      (insert " " (match-string 1))
      ;; Remove extraneous whitespace from title
      (replace-match (ein:markdown-compress-whitespace-string (match-string 2))
                     t t nil 2)
      ;; Spacing at beginning
      (goto-char (match-end 1))
      (delete-region (match-end 1) (match-beginning 2))
      (insert " ")
      ;; Leave point at end of text
      (goto-char new-marker))))

(defun ein:markdown-incomplete-setext-p ()
  "Return t if setext header markup is incomplete and nil otherwise.
Assumes match data is available for `markdown-regex-header-setext'.
Checks that length of underline matches text and that there is no
extraneous whitespace in the text."
  (or (not (= (length (match-string 1)) (length (match-string 2))))
      (string-match-p "[ \t\n]\\{2\\}" (match-string 1))))

(defun ein:markdown-complete-setext ()
  "Complete and normalize setext headers.
Add or remove underline characters to match length of header
text.  Removes extraneous whitespace from header text.  Assumes
match data is available for `markdown-regex-header-setext'.
Return nil if markup was complete and non-nil if markup was completed."
  (when (ein:markdown-incomplete-setext-p)
    (let* ((text (ein:markdown-compress-whitespace-string (match-string 1)))
           (char (char-after (match-beginning 2)))
           (level (if (char-equal char ?-) 2 1)))
      (goto-char (match-beginning 0))
      (delete-region (match-beginning 0) (match-end 0))
      (ein:markdown-insert-header level text t)
      t)))

(defun ein:markdown-incomplete-hr-p ()
  "Return non-nil if hr is not in `markdown-hr-strings' and nil otherwise.
Assumes match data is available for `markdown-regex-hr'."
  (not (member (match-string 0) ein:markdown-hr-strings)))

(defun ein:markdown-complete-hr ()
  "Complete horizontal rules.
If horizontal rule string is a member of `markdown-hr-strings',
do nothing.  Otherwise, replace with the car of
`markdown-hr-strings'.
Assumes match data is available for `markdown-regex-hr'.
Return nil if markup was complete and non-nil if markup was completed."
  (when (ein:markdown-incomplete-hr-p)
    (replace-match (car ein:markdown-hr-strings))
    t))

(defun ein:markdown-complete ()
  "Complete markup of object near point or in region when active.
Handle all objects in `markdown-complete-alist', in order.
See `markdown-complete-at-point' and `markdown-complete-region'."
  (interactive "*")
  (if (ein:markdown-use-region-p)
      (ein:markdown-complete-region (region-beginning) (region-end))
    (ein:markdown-complete-at-point)))

(defun ein:markdown-complete-at-point ()
  "Complete markup of object near point.
Handle all elements of `markdown-complete-alist' in order."
  (interactive "*")
  (let ((list ein:markdown-complete-alist) found changed)
    (while list
      (let ((regexp (eval (caar list)))
            (function (cdar list)))
        (setq list (cdr list))
        (when (thing-at-point-looking-at regexp)
          (setq found t)
          (setq changed (funcall function))
          (setq list nil))))
    (if found
        (or changed (user-error "Markup at point is complete"))
      (user-error "Nothing to complete at point"))))

(defun ein:markdown-complete-region (beg end)
  "Complete markup of objects in region from BEG to END.
Handle all objects in `markdown-complete-alist', in order.  Each
match is checked to ensure that a previous regexp does not also
match."
  (interactive "*r")
  (let ((end-marker (set-marker (make-marker) end))
        previous)
    (dolist (element ein:markdown-complete-alist)
      (let ((regexp (eval (car element)))
            (function (cdr element)))
        (goto-char beg)
        (while (re-search-forward regexp end-marker 'limit)
          (when (match-string 0)
            ;; Make sure this is not a match for any of the preceding regexps.
            ;; This prevents mistaking an HR for a Setext subheading.
            (let (match)
              (save-match-data
                (dolist (prev-regexp previous)
                  (or match (setq match (looking-back prev-regexp nil)))))
              (unless match
                (save-excursion (funcall function))))))
        (cl-pushnew regexp previous :test #'equal)))
    previous))

(defun ein:markdown-complete-buffer ()
  "Complete markup for all objects in the current buffer."
  (interactive "*")
  (ein:markdown-complete-region (point-min) (point-max)))


;;; Markup Cycling ============================================================

(defun ein:markdown-cycle-atx (arg &optional remove)
  "Cycle ATX header markup.
Promote header (decrease level) when ARG is 1 and demote
header (increase level) if arg is -1.  When REMOVE is non-nil,
remove the header when the level reaches zero and stop cycling
when it reaches six.  Otherwise, perform a proper cycling through
levels one through six.  Assumes match data is available for
`markdown-regex-header-atx'."
  (let* ((old-level (length (match-string 1)))
         (new-level (+ old-level arg))
         (text (match-string 2)))
    (when (not remove)
      (setq new-level (% new-level 6))
      (setq new-level (cond ((= new-level 0) 6)
                            ((< new-level 0) (+ new-level 6))
                            (t new-level))))
    (cond
     ((= new-level 0)
      (ein:markdown-unwrap-thing-at-point nil 0 2))
     ((<= new-level 6)
      (goto-char (match-beginning 0))
      (delete-region (match-beginning 0) (match-end 0))
      (ein:markdown-insert-header new-level text nil)))))

(defun ein:markdown-cycle-setext (arg &optional remove)
  "Cycle setext header markup.
Promote header (increase level) when ARG is 1 and demote
header (decrease level or remove) if arg is -1.  When demoting a
level-two setext header, replace with a level-three atx header.
When REMOVE is non-nil, remove the header when the level reaches
zero.  Otherwise, cycle back to a level six atx header.  Assumes
match data is available for `markdown-regex-header-setext'."
  (let* ((char (char-after (match-beginning 2)))
         (old-level (if (char-equal char ?=) 1 2))
         (new-level (+ old-level arg)))
    (when (and (not remove) (= new-level 0))
      (setq new-level 6))
    (cond
     ((= new-level 0)
      (ein:markdown-unwrap-thing-at-point nil 0 1))
     ((<= new-level 2)
      (ein:markdown-insert-header new-level nil t))
     ((<= new-level 6)
      (ein:markdown-insert-header new-level nil nil)))))

(defun ein:markdown-cycle-hr (arg &optional remove)
  "Cycle string used for horizontal rule from `markdown-hr-strings'.
When ARG is 1, cycle forward (demote), and when ARG is -1, cycle
backwards (promote).  When REMOVE is non-nil, remove the hr instead
of cycling when the end of the list is reached.
Assumes match data is available for `markdown-regex-hr'."
  (let* ((strings (if (= arg -1)
                      (reverse ein:markdown-hr-strings)
                    ein:markdown-hr-strings))
         (tail (member (match-string 0) strings))
         (new (or (cadr tail)
                  (if remove
                      (if (= arg 1)
                          ""
                        (car tail))
                    (car strings)))))
    (replace-match new)))

(defun ein:markdown-cycle-bold ()
  "Cycle bold markup between underscores and asterisks.
Assumes match data is available for `markdown-regex-bold'."
  (save-excursion
    (let* ((old-delim (match-string 3))
           (new-delim (if (string-equal old-delim "**") "__" "**")))
      (replace-match new-delim t t nil 3)
      (replace-match new-delim t t nil 5))))

(defun ein:markdown-cycle-italic ()
  "Cycle italic markup between underscores and asterisks.
Assumes match data is available for `markdown-regex-italic'."
  (save-excursion
    (let* ((old-delim (match-string 2))
           (new-delim (if (string-equal old-delim "*") "_" "*")))
      (replace-match new-delim t t nil 2)
      (replace-match new-delim t t nil 4))))


;;; Keymap ====================================================================

(defun ein:markdown--style-map-prompt ()
  "Return a formatted prompt for ein:markdown markup insertion."
  (when ein:markdown-enable-prefix-prompts
    (concat
     "ein:markdown: "
     (propertize "bold" 'face 'ein:markdown-bold-face) ", "
     (propertize "italic" 'face 'ein:markdown-italic-face) ", "
     (propertize "code" 'face 'ein:markdown-inline-code-face) ", "
     (propertize "C = GFM code" 'face 'ein:markdown-code-face) ", "
     (propertize "pre" 'face 'ein:markdown-pre-face) ", "
     (propertize "footnote" 'face 'ein:markdown-footnote-text-face) ", "
     (propertize "q = blockquote" 'face 'ein:markdown-blockquote-face) ", "
     (propertize "h & 1-6 = heading" 'face 'ein:markdown-header-face) ", "
     (propertize "- = hr" 'face 'ein:markdown-hr-face) ", "
     "C-h = more")))

(defun ein:markdown--command-map-prompt ()
  "Return prompt for ein:markdown buffer-wide commands."
  (when ein:markdown-enable-prefix-prompts
    (concat
     "Command: "
     (propertize "m" 'face 'ein:markdown-bold-face) "arkdown, "
     (propertize "o" 'face 'ein:markdown-bold-face) "pen, "
     (propertize "c" 'face 'ein:markdown-bold-face) "heck refs, "
     (propertize "u" 'face 'ein:markdown-bold-face) "nused refs, "
     "C-h = more")))

(defvar ein:markdown-mode-style-map
  (let ((map (make-keymap (ein:markdown--style-map-prompt))))
    (define-key map (kbd "1") 'ein:markdown-insert-header-atx-1)
    (define-key map (kbd "2") 'ein:markdown-insert-header-atx-2)
    (define-key map (kbd "3") 'ein:markdown-insert-header-atx-3)
    (define-key map (kbd "4") 'ein:markdown-insert-header-atx-4)
    (define-key map (kbd "5") 'ein:markdown-insert-header-atx-5)
    (define-key map (kbd "6") 'ein:markdown-insert-header-atx-6)
    (define-key map (kbd "!") 'ein:markdown-insert-header-setext-1)
    (define-key map (kbd "@") 'ein:markdown-insert-header-setext-2)
    (define-key map (kbd "b") 'ein:markdown-insert-bold)
    (define-key map (kbd "c") 'ein:markdown-insert-code)
    (define-key map (kbd "f") 'ein:markdown-insert-footnote)
    (define-key map (kbd "h") 'ein:markdown-insert-header-dwim)
    (define-key map (kbd "H") 'ein:markdown-insert-header-setext-dwim)
    (define-key map (kbd "i") 'ein:markdown-insert-italic)
    (define-key map (kbd "k") 'ein:markdown-insert-kbd)
    (define-key map (kbd "l") 'ein:markdown-insert-link)
    (define-key map (kbd "p") 'ein:markdown-insert-pre)
    (define-key map (kbd "P") 'ein:markdown-pre-region)
    (define-key map (kbd "q") 'ein:markdown-insert-blockquote)
    (define-key map (kbd "s") 'ein:markdown-insert-strike-through)
    (define-key map (kbd "t") 'ein:markdown-insert-table)
    (define-key map (kbd "Q") 'ein:markdown-blockquote-region)
    (define-key map (kbd "-") 'ein:markdown-insert-hr)
    ;; Deprecated keys that may be removed in a future version
    (define-key map (kbd "e") 'ein:markdown-insert-italic)
    map)
  "Keymap for ein:markdown text styling commands.")

(defvar ein:markdown-mode-command-map
  (let ((map (make-keymap (ein:markdown--command-map-prompt))))
    (define-key map (kbd "m") 'ein:markdown-other-window)
    (define-key map (kbd "o") 'ein:markdown-open)
    (define-key map (kbd "w") 'ein:markdown-kill-ring-save)
    (define-key map (kbd "c") 'ein:markdown-check-refs)
    (define-key map (kbd "u") 'ein:markdown-unused-refs)
    (define-key map (kbd "n") 'ein:markdown-cleanup-list-numbers)
    (define-key map (kbd "]") 'ein:markdown-complete-buffer)
    (define-key map (kbd "^") 'ein:markdown-table-sort-lines)
    (define-key map (kbd "|") 'ein:markdown-table-convert-region)
    (define-key map (kbd "t") 'ein:markdown-table-transpose)
    map)
  "Keymap for ein:markdown buffer-wide commands.")

(defvar ein:markdown-mode-map
  (let ((map (make-keymap)))
    ;; Markup insertion & removal
    (define-key map (kbd "C-c C-s") ein:markdown-mode-style-map)
    (define-key map (kbd "C-c C-l") 'ein:markdown-insert-link)
    (define-key map (kbd "C-c C-k") 'ein:markdown-kill-thing-at-point)
    ;; Promotion, demotion, and cycling
    (define-key map (kbd "C-c C--") 'ein:markdown-promote)
    (define-key map (kbd "C-c C-=") 'ein:markdown-demote)
    (define-key map (kbd "C-c C-]") 'ein:markdown-complete)
    ;; Following and doing things
    (define-key map (kbd "C-c C-o") 'ein:markdown-follow-thing-at-point)
    (define-key map (kbd "C-c C-d") 'ein:markdown-do)
    (define-key map (kbd "C-c '") 'ein:markdown-edit-code-block)
    ;; Indentation
    (define-key map (kbd "C-m") 'ein:markdown-enter-key)
    (define-key map (kbd "DEL") 'ein:markdown-outdent-or-delete)
    (define-key map (kbd "C-c >") 'ein:markdown-indent-region)
    (define-key map (kbd "C-c <") 'ein:markdown-outdent-region)
    ;; Visibility cycling
    (define-key map (kbd "TAB") 'ein:markdown-cycle)
    (define-key map (kbd "<S-iso-lefttab>") 'ein:markdown-shifttab)
    (define-key map (kbd "<S-tab>")  'ein:markdown-shifttab)
    (define-key map (kbd "<backtab>") 'ein:markdown-shifttab)
    ;; Heading and list navigation
    (define-key map (kbd "C-c C-n") 'ein:markdown-outline-next)
    (define-key map (kbd "C-c C-p") 'ein:markdown-outline-previous)
    (define-key map (kbd "C-c C-f") 'ein:markdown-outline-next-same-level)
    (define-key map (kbd "C-c C-b") 'ein:markdown-outline-previous-same-level)
    (define-key map (kbd "C-c C-u") 'ein:markdown-outline-up)
    ;; Buffer-wide commands
    (define-key map (kbd "C-c C-c") ein:markdown-mode-command-map)
    ;; Subtree, list, and table editing
    (define-key map (kbd "C-c <up>") 'ein:markdown-move-up)
    (define-key map (kbd "C-c <down>") 'ein:markdown-move-down)
    (define-key map (kbd "C-c <left>") 'ein:markdown-promote)
    (define-key map (kbd "C-c <right>") 'ein:markdown-demote)
    (define-key map (kbd "C-c S-<up>") 'ein:markdown-table-delete-row)
    (define-key map (kbd "C-c S-<down>") 'ein:markdown-table-insert-row)
    (define-key map (kbd "C-c S-<left>") 'ein:markdown-table-delete-column)
    (define-key map (kbd "C-c S-<right>") 'ein:markdown-table-insert-column)
    (define-key map (kbd "C-c C-M-h") 'ein:markdown-mark-subtree)
    (define-key map (kbd "C-x n s") 'ein:markdown-narrow-to-subtree)
    (define-key map (kbd "M-RET") 'ein:markdown-insert-list-item)
    (define-key map (kbd "C-c C-j") 'ein:markdown-insert-list-item)
    ;; Paragraphs (ein:markdown context aware)
    (define-key map [remap backward-paragraph] 'ein:markdown-backward-paragraph)
    (define-key map [remap forward-paragraph] 'ein:markdown-forward-paragraph)
    (define-key map [remap mark-paragraph] 'ein:markdown-mark-paragraph)
    ;; Blocks (one or more paragraphs)
    (define-key map (kbd "C-M-{") 'ein:markdown-backward-block)
    (define-key map (kbd "C-M-}") 'ein:markdown-forward-block)
    (define-key map (kbd "C-c M-h") 'ein:markdown-mark-block)
    (define-key map (kbd "C-x n b") 'ein:markdown-narrow-to-block)
    ;; Pages (top-level sections)
    (define-key map [remap backward-page] 'ein:markdown-backward-page)
    (define-key map [remap forward-page] 'ein:markdown-forward-page)
    (define-key map [remap mark-page] 'ein:markdown-mark-page)
    (define-key map [remap narrow-to-page] 'ein:markdown-narrow-to-page)
    ;; Link Movement
    (define-key map (kbd "M-n") 'ein:markdown-next-link)
    (define-key map (kbd "M-p") 'ein:markdown-previous-link)
    ;; Toggling functionality
    (define-key map (kbd "C-c C-x C-e") 'ein:markdown-toggle-math)
    ;; Alternative keys (in case of problems with the arrow keys)
    (define-key map (kbd "C-c C-x u") 'ein:markdown-move-up)
    (define-key map (kbd "C-c C-x d") 'ein:markdown-move-down)
    (define-key map (kbd "C-c C-x l") 'ein:markdown-promote)
    (define-key map (kbd "C-c C-x r") 'ein:markdown-demote)
    ;; Deprecated keys that may be removed in a future version
    (define-key map (kbd "C-c C-a L") 'ein:markdown-insert-link) ;; C-c C-l
    (define-key map (kbd "C-c C-a l") 'ein:markdown-insert-link) ;; C-c C-l
    (define-key map (kbd "C-c C-a r") 'ein:markdown-insert-link) ;; C-c C-l
    (define-key map (kbd "C-c C-a u") 'ein:markdown-insert-uri) ;; C-c C-l
    (define-key map (kbd "C-c C-a f") 'ein:markdown-insert-footnote)
    (define-key map (kbd "C-c C-t 1") 'ein:markdown-insert-header-atx-1)
    (define-key map (kbd "C-c C-t 2") 'ein:markdown-insert-header-atx-2)
    (define-key map (kbd "C-c C-t 3") 'ein:markdown-insert-header-atx-3)
    (define-key map (kbd "C-c C-t 4") 'ein:markdown-insert-header-atx-4)
    (define-key map (kbd "C-c C-t 5") 'ein:markdown-insert-header-atx-5)
    (define-key map (kbd "C-c C-t 6") 'ein:markdown-insert-header-atx-6)
    (define-key map (kbd "C-c C-t !") 'ein:markdown-insert-header-setext-1)
    (define-key map (kbd "C-c C-t @") 'ein:markdown-insert-header-setext-2)
    (define-key map (kbd "C-c C-t h") 'ein:markdown-insert-header-dwim)
    (define-key map (kbd "C-c C-t H") 'ein:markdown-insert-header-setext-dwim)
    (define-key map (kbd "C-c C-t s") 'ein:markdown-insert-header-setext-2)
    (define-key map (kbd "C-c C-t t") 'ein:markdown-insert-header-setext-1)
    (define-key map (kbd "C-c C-i") 'ein:markdown-insert-image)
    (define-key map (kbd "C-c C-x m") 'ein:markdown-insert-list-item) ;; C-c C-j
    (define-key map (kbd "C-c -") 'ein:markdown-insert-hr)
    map)
  "Keymap for ein:markdown major mode.")

(defvar ein:markdown-mode-mouse-map
  (let ((map (make-sparse-keymap)))
    (define-key map [follow-link] 'mouse-face)
    (define-key map [mouse-2] 'ein:markdown-follow-link-at-point)
    map)
  "Keymap for following links with mouse.")

;;; Menu ======================================================================

(easy-menu-define ein:markdown-mode-menu ein:markdown-mode-map
  "Menu for ein:markdown mode"
  '("ein:markdown"
    "---"
    ("Movement"
     ["Jump" ein:markdown-do]
     ["Follow Link" ein:markdown-follow-thing-at-point]
     ["Next Link" ein:markdown-next-link]
     ["Previous Link" ein:markdown-previous-link]
     "---"
     ["Next Heading or List Item" ein:markdown-outline-next]
     ["Previous Heading or List Item" ein:markdown-outline-previous]
     ["Next at Same Level" ein:markdown-outline-next-same-level]
     ["Previous at Same Level" ein:markdown-outline-previous-same-level]
     ["Up to Parent" ein:markdown-outline-up]
     "---"
     ["Forward Paragraph" ein:markdown-forward-paragraph]
     ["Backward Paragraph" ein:markdown-backward-paragraph]
     ["Forward Block" ein:markdown-forward-block]
     ["Backward Block" ein:markdown-backward-block])
    ("Show & Hide"
     ["Cycle Heading Visibility" ein:markdown-cycle
      :enable (ein:markdown-on-heading-p)]
     ["Cycle Heading Visibility (Global)" ein:markdown-shifttab]
     "---"
     ["Narrow to Region" narrow-to-region]
     ["Narrow to Block" ein:markdown-narrow-to-block]
     ["Narrow to Section" narrow-to-defun]
     ["Narrow to Subtree" ein:markdown-narrow-to-subtree]
     ["Widen" widen (buffer-narrowed-p)])
    "---"
    ("Headings & Structure"
     ["Automatic Heading" ein:markdown-insert-header-dwim
      :keys "C-c C-s h"]
     ["Automatic Heading (Setext)" ein:markdown-insert-header-setext-dwim
      :keys "C-c C-s H"]
     ("Specific Heading (atx)"
      ["First Level atx" ein:markdown-insert-header-atx-1
       :keys "C-c C-s 1"]
      ["Second Level atx" ein:markdown-insert-header-atx-2
       :keys "C-c C-s 2"]
      ["Third Level atx" ein:markdown-insert-header-atx-3
       :keys "C-c C-s 3"]
      ["Fourth Level atx" ein:markdown-insert-header-atx-4
       :keys "C-c C-s 4"]
      ["Fifth Level atx" ein:markdown-insert-header-atx-5
       :keys "C-c C-s 5"]
      ["Sixth Level atx" ein:markdown-insert-header-atx-6
       :keys "C-c C-s 6"])
     ("Specific Heading (Setext)"
      ["First Level Setext" ein:markdown-insert-header-setext-1
       :keys "C-c C-s !"]
      ["Second Level Setext" ein:markdown-insert-header-setext-2
       :keys "C-c C-s @"])
     ["Horizontal Rule" ein:markdown-insert-hr
      :keys "C-c C-s -"]
     "---"
     ["Move Subtree Up" ein:markdown-move-up
      :keys "C-c <up>"]
     ["Move Subtree Down" ein:markdown-move-down
      :keys "C-c <down>"]
     ["Promote Subtree" ein:markdown-promote
      :keys "C-c <left>"]
     ["Demote Subtree" ein:markdown-demote
      :keys "C-c <right>"])
    ("Region & Mark"
     ["Indent Region" ein:markdown-indent-region]
     ["Outdent Region" ein:markdown-outdent-region]
     "--"
     ["Mark Paragraph" mark-paragraph]
     ["Mark Block" ein:markdown-mark-block]
     ["Mark Section" mark-defun]
     ["Mark Subtree" ein:markdown-mark-subtree])
    ("Tables"
     ["Move Row Up" ein:markdown-move-up
      :enable (ein:markdown-table-at-point-p)
      :keys "C-c <up>"]
     ["Move Row Down" ein:markdown-move-down
      :enable (ein:markdown-table-at-point-p)
      :keys "C-c <down>"]
     ["Move Column Left" ein:markdown-demote
      :enable (ein:markdown-table-at-point-p)
      :keys "C-c <left>"]
     ["Move Column Right" ein:markdown-promote
      :enable (ein:markdown-table-at-point-p)
      :keys "C-c <right>"]
     ["Delete Row" ein:markdown-table-delete-row
      :enable (ein:markdown-table-at-point-p)]
     ["Insert Row" ein:markdown-table-insert-row
      :enable (ein:markdown-table-at-point-p)]
     ["Delete Column" ein:markdown-table-delete-column
      :enable (ein:markdown-table-at-point-p)]
     ["Insert Column" ein:markdown-table-insert-column
      :enable (ein:markdown-table-at-point-p)]
     ["Insert Table" ein:markdown-insert-table]
     "--"
     ["Convert Region to Table" ein:markdown-table-convert-region]
     ["Sort Table Lines" ein:markdown-table-sort-lines
      :enable (ein:markdown-table-at-point-p)]
     ["Transpose Table" ein:markdown-table-transpose
      :enable (ein:markdown-table-at-point-p)])
    ("Lists"
     ["Insert List Item" ein:markdown-insert-list-item]
     ["Move Subtree Up" ein:markdown-move-up
      :keys "C-c <up>"]
     ["Move Subtree Down" ein:markdown-move-down
      :keys "C-c <down>"]
     ["Indent Subtree" ein:markdown-demote
      :keys "C-c <right>"]
     ["Outdent Subtree" ein:markdown-promote
      :keys "C-c <left>"]
     ["Renumber List" ein:markdown-cleanup-list-numbers])
    ("Links & Images"
     ["Insert Link" ein:markdown-insert-link]
     ["Insert Image" ein:markdown-insert-image]
     ["Insert Footnote" ein:markdown-insert-footnote
      :keys "C-c C-s f"]
     "---"
     ["Check References" ein:markdown-check-refs]
     ["Find Unused References" ein:markdown-unused-refs])
    ("Styles"
     ["Bold" ein:markdown-insert-bold]
     ["Italic" ein:markdown-insert-italic]
     ["Code" ein:markdown-insert-code]
     ["Strikethrough" ein:markdown-insert-strike-through]
     ["Keyboard" ein:markdown-insert-kbd]
     "---"
     ["Blockquote" ein:markdown-insert-blockquote]
     ["Preformatted" ein:markdown-insert-pre]
     ["Edit Code Block" ein:markdown-edit-code-block
      :enable (ein:markdown-code-block-at-point-p)]
     "---"
     ["Blockquote Region" ein:markdown-blockquote-region]
     ["Preformatted Region" ein:markdown-pre-region]
     "---"
     ["LaTeX Math Support" ein:markdown-toggle-math
      :style radio
      :selected ein:markdown-enable-math])
    "---"
    ("Markup Completion and Cycling"
     ["Complete Markup" ein:markdown-complete]
     ["Promote Element" ein:markdown-promote
      :keys "C-c C--"]
     ["Demote Element" ein:markdown-demote
      :keys "C-c C-="])
    "---"
    ["Kill Element" ein:markdown-kill-thing-at-point]
    "---"
    ("Documentation"
     ["Version" ein:markdown-show-version]
     ["Homepage" ein:markdown-mode-info]
     ["Describe Mode" (describe-function 'ein:markdown-mode)]
     ["Guide" (browse-url "https://leanpub.com/ein:markdown-mode")])))


;;; imenu =====================================================================

(defun ein:markdown-imenu-create-nested-index ()
  "Create and return a nested imenu index alist for the current buffer.
See `imenu-create-index-function' and `imenu--index-alist' for details."
  (let* ((root '(nil . nil))
         cur-alist
         (cur-level 0)
         (empty-heading "-")
         (self-heading ".")
         hashes pos level heading)
    (save-excursion
      ;; Headings
      (goto-char (point-min))
      (while (re-search-forward ein:markdown-regex-header (point-max) t)
        (unless (ein:markdown-code-block-at-point-p)
          (cond
           ((match-string-no-properties 2) ;; level 1 setext
            (setq heading (match-string-no-properties 1))
            (setq pos (match-beginning 1)
                  level 1))
           ((match-string-no-properties 3) ;; level 2 setext
            (setq heading (match-string-no-properties 1))
            (setq pos (match-beginning 1)
                  level 2))
           ((setq hashes (ein:markdown-trim-whitespace
                          (match-string-no-properties 4)))
            (setq heading (match-string-no-properties 5)
                  pos (match-beginning 4)
                  level (length hashes))))
          (let ((alist (list (cons heading pos))))
            (cond
             ((= cur-level level)       ; new sibling
              (setq cur-alist alist))
             ((< cur-level level)       ; first child
              (dotimes (_ (- level cur-level 1))
                (setq alist (list (cons empty-heading alist))))
              (if cur-alist
                  (let* ((parent (car cur-alist))
                         (self-pos (cdr parent)))
                    (setcdr parent (cons (cons self-heading self-pos) alist)))
                (setq root (cons (car root) alist)))    ; primogenitor
              (setq cur-alist alist)
              (setq cur-level level))
             (t                         ; new sibling of an ancestor
              (let ((sibling-alist (last (cdr root))))
                (dotimes (_ (1- level))
                  (setq sibling-alist (last (cdar sibling-alist))))
                (setcdr sibling-alist alist)
                (setq cur-alist alist))
              (setq cur-level level))))))
      ;; Footnotes
      (let ((fn (ein:markdown-get-defined-footnotes)))
        (if (or (zerop (length fn))
                (null ein:markdown-add-footnotes-to-imenu))
            (cdr root)
          (nconc (cdr root) (list (cons "Footnotes" fn))))))))

(defun ein:markdown-imenu-create-flat-index ()
  "Create and return a flat imenu index alist for the current buffer.
See `imenu-create-index-function' and `imenu--index-alist' for details."
  (let* ((empty-heading "-") index heading pos)
    (save-excursion
      ;; Headings
      (goto-char (point-min))
      (while (re-search-forward ein:markdown-regex-header (point-max) t)
        (when (and (not (ein:markdown-code-block-at-point-p (line-beginning-position)))
                   (not (ein:markdown-text-property-at-point 'ein:markdown-yaml-metadata-begin)))
          (cond
           ((setq heading (match-string-no-properties 1))
            (setq pos (match-beginning 1)))
           ((setq heading (match-string-no-properties 5))
            (setq pos (match-beginning 4))))
          (or (> (length heading) 0)
              (setq heading empty-heading))
          (setq index (append index (list (cons heading pos))))))
      ;; Footnotes
      (when ein:markdown-add-footnotes-to-imenu
        (nconc index (ein:markdown-get-defined-footnotes)))
      index)))


;;; References ================================================================

(defun ein:markdown-reference-goto-definition ()
  "Jump to the definition of the reference at point or create it."
  (interactive)
  (when (thing-at-point-looking-at ein:markdown-regex-link-reference)
    (let* ((text (match-string-no-properties 3))
           (reference (match-string-no-properties 6))
           (target (downcase (if (string= reference "") text reference)))
           (loc (cadr (save-match-data (ein:markdown-reference-definition target)))))
      (if loc
          (goto-char loc)
        (goto-char (match-beginning 0))
        (ein:markdown-insert-reference-definition target)))))

(defun ein:markdown-reference-find-links (reference)
  "Return a list of all links for REFERENCE.
REFERENCE should not include the surrounding square brackets.
Elements of the list have the form (text start line), where
text is the link text, start is the location at the beginning of
the link, and line is the line number on which the link appears."
  (let* ((ref-quote (regexp-quote reference))
         (regexp (format "!?\\(?:\\[\\(%s\\)\\][ ]?\\[\\]\\|\\[\\([^]]+?\\)\\][ ]?\\[%s\\]\\)"
                         ref-quote ref-quote))
         links)
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward regexp nil t)
        (let* ((text (or (match-string-no-properties 1)
                         (match-string-no-properties 2)))
               (start (match-beginning 0))
               (line (ein:markdown-line-number-at-pos)))
          (cl-pushnew (list text start line) links :test #'equal))))
    links))

(defmacro ein:markdown-for-all-refs (f)
  `(let ((result))
     (save-excursion
       (goto-char (point-min))
       (while
           (re-search-forward ein:markdown-regex-link-reference nil t)
         (let* ((text (match-string-no-properties 3))
                (reference (match-string-no-properties 6))
                (target (downcase (if (string= reference "") text reference))))
          (,f text target result))))
     (reverse result)))

(defmacro ein:markdown-collect-always (_ target result)
  `(cl-pushnew ,target ,result :test #'equal))

(defmacro ein:markdown-collect-undefined (text target result)
  `(unless (ein:markdown-reference-definition target)
     (let ((entry (assoc ,target ,result)))
       (if (not entry)
           (cl-pushnew
            (cons ,target (list (cons ,text (ein:markdown-line-number-at-pos))))
            ,result :test #'equal)
         (setcdr entry
                 (append (cdr entry) (list (cons ,text (ein:markdown-line-number-at-pos)))))))))

(defun ein:markdown-get-all-refs ()
  "Return a list of all ein:markdown references."
  (ein:markdown-for-all-refs ein:markdown-collect-always))

(defun ein:markdown-get-undefined-refs ()
  "Return a list of undefined ein:markdown references.
Result is an alist of pairs (reference . occurrences), where
occurrences is itself another alist of pairs (label . line-number).
For example, an alist corresponding to [Nice editor][Emacs] at line 12,
\[GNU Emacs][Emacs] at line 45 and [manual][elisp] at line 127 is
\((\"emacs\" (\"Nice editor\" . 12) (\"GNU Emacs\" . 45)) (\"elisp\" (\"manual\" . 127)))."
  (ein:markdown-for-all-refs ein:markdown-collect-undefined))

(defun ein:markdown-get-unused-refs ()
  (cl-sort
   (cl-set-difference
    (ein:markdown-get-defined-references) (ein:markdown-get-all-refs)
    :test (lambda (e1 e2) (equal (car e1) e2)))
   #'< :key #'cdr))

(defmacro defun-markdown-buffer (name docstring)
  "Define a function to name and return a buffer.

By convention, NAME must be a name of a string constant with
%buffer% placeholder used to name the buffer, and will also be
used as a name of the function defined.

DOCSTRING will be used as the first part of the docstring."
  `(defun ,name (&optional buffer-name)
     ,(concat docstring "\n\nBUFFER-NAME is the name of the main buffer being visited.")
     (or buffer-name (setq buffer-name (buffer-name)))
     (let ((refbuf (get-buffer-create (ein:markdown-replace-regexp-in-string
                                       "%buffer%" buffer-name
                                       ,name))))
       (with-current-buffer refbuf
         (when view-mode
           (View-exit-and-edit))
         (use-local-map button-buffer-map)
         (erase-buffer))
       refbuf)))

(defconst ein:markdown-reference-check-buffer
  "*Undefined references for %buffer%*"
  "Pattern for name of buffer for listing undefined references.
The string %buffer% will be replaced by the corresponding
`ein:markdown-mode' buffer name.")

(defun-markdown-buffer
  ein:markdown-reference-check-buffer
  "Name and return buffer for reference checking.")

(defconst ein:markdown-unused-references-buffer
  "*Unused references for %buffer%*"
  "Pattern for name of buffer for listing unused references.
The string %buffer% will be replaced by the corresponding
`ein:markdown-mode' buffer name.")

(defun-markdown-buffer
  ein:markdown-unused-references-buffer
  "Name and return buffer for unused reference checking.")

(defconst ein:markdown-reference-links-buffer
  "*Reference links for %buffer%*"
  "Pattern for name of buffer for listing references.
The string %buffer% will be replaced by the corresponding buffer name.")

(defun-markdown-buffer
  ein:markdown-reference-links-buffer
  "Name, setup, and return a buffer for listing links.")

;; Add an empty ein:markdown reference definition to buffer
;; specified in the 'target-buffer property.  The reference name is
;; the button's label.
(define-button-type 'ein:markdown-undefined-reference-button
  'help-echo "mouse-1, RET: create definition for undefined reference"
  'follow-link t
  'face 'bold
  'action (lambda (b)
            (let ((buffer (button-get b 'target-buffer))
                  (line (button-get b 'target-line))
                  (label (button-label b)))
              (switch-to-buffer-other-window buffer)
              (goto-char (point-min))
              (forward-line line)
              (ein:markdown-insert-reference-definition label)
              (ein:markdown-check-refs t))))

;; Jump to line in buffer specified by 'target-buffer property.
;; Line number is button's 'target-line property.
(define-button-type 'ein:markdown-goto-line-button
  'help-echo "mouse-1, RET: go to line"
  'follow-link t
  'face 'italic
  'action (lambda (b)
            (switch-to-buffer-other-window (button-get b 'target-buffer))
            ;; use call-interactively to silence compiler
            (let ((current-prefix-arg (button-get b 'target-line)))
              (call-interactively 'goto-line))))

;; Kill a line in buffer specified by 'target-buffer property.
;; Line number is button's 'target-line property.
(define-button-type 'ein:markdown-kill-line-button
  'help-echo "mouse-1, RET: kill line"
  'follow-link t
  'face 'italic
  'action (lambda (b)
            (switch-to-buffer-other-window (button-get b 'target-buffer))
            ;; use call-interactively to silence compiler
            (let ((current-prefix-arg (button-get b 'target-line)))
              (call-interactively 'goto-line))
            (kill-line 1)
            (ein:markdown-unused-refs t)))

;; Jumps to a particular link at location given by 'target-char
;; property in buffer given by 'target-buffer property.
(define-button-type 'ein:markdown-location-button
  'help-echo "mouse-1, RET: jump to location of link"
  'follow-link t
  'face 'bold
  'action (lambda (b)
            (let ((target (button-get b 'target-buffer))
                  (loc (button-get b 'target-char)))
              (kill-buffer-and-window)
              (switch-to-buffer target)
              (goto-char loc))))

(defun ein:markdown-insert-undefined-reference-button (reference oldbuf)
  "Insert a button for creating REFERENCE in buffer OLDBUF.
REFERENCE should be a list of the form (reference . occurrences),
as returned by `markdown-get-undefined-refs'."
  (let ((label (car reference)))
    ;; Create a reference button
    (insert-button label
                   :type 'ein:markdown-undefined-reference-button
                   'target-buffer oldbuf
                   'target-line (cdr (car (cdr reference))))
    (insert " (")
    (dolist (occurrence (cdr reference))
      (let ((line (cdr occurrence)))
        ;; Create a line number button
        (insert-button (number-to-string line)
                       :type 'ein:markdown-goto-line-button
                       'target-buffer oldbuf
                       'target-line line)
        (insert " ")))
    (delete-char -1)
    (insert ")")
    (newline)))

(defun ein:markdown-insert-unused-reference-button (reference oldbuf)
  "Insert a button for creating REFERENCE in buffer OLDBUF.
REFERENCE must be a pair of (ref . line-number)."
  (let ((label (car reference))
        (line (cdr reference)))
    ;; Create a reference button
    (insert-button label
                   :type 'ein:markdown-goto-line-button
                   'face 'bold
                   'target-buffer oldbuf
                   'target-line line)
    (insert (format " (%d) [" line))
    (insert-button "X"
                   :type 'ein:markdown-kill-line-button
                   'face 'bold
                   'target-buffer oldbuf
                   'target-line line)
    (insert "]")
    (newline)))

(defun ein:markdown-insert-link-button (link oldbuf)
  "Insert a button for jumping to LINK in buffer OLDBUF.
LINK should be a list of the form (text char line) containing
the link text, location, and line number."
  (let ((label (cl-first link))
        (char (cl-second link))
        (line (cl-third link)))
    ;; Create a reference button
    (insert-button label
                   :type 'ein:markdown-location-button
                   'target-buffer oldbuf
                   'target-char char)
    (insert (format " (line %d)\n" line))))

(defun ein:markdown-reference-goto-link (&optional reference)
  "Jump to the location of the first use of REFERENCE."
  (interactive)
  (unless reference
    (if (thing-at-point-looking-at ein:markdown-regex-reference-definition)
        (setq reference (match-string-no-properties 2))
      (user-error "No reference definition at point")))
  (let ((links (ein:markdown-reference-find-links reference)))
    (cond ((= (length links) 1)
           (goto-char (cadr (car links))))
          ((> (length links) 1)
           (let ((oldbuf (current-buffer))
                 (linkbuf (ein:markdown-reference-links-buffer)))
             (with-current-buffer linkbuf
               (insert "Links using reference " reference ":\n\n")
               (dolist (link (reverse links))
                 (ein:markdown-insert-link-button link oldbuf)))
             (view-buffer-other-window linkbuf)
             (goto-char (point-min))
             (forward-line 2)))
          (t
           (error "No links for reference %s" reference)))))

(defmacro defun-markdown-ref-checker
    (name docstring checker-function buffer-function none-message buffer-header insert-reference)
  "Define a function NAME acting on result of CHECKER-FUNCTION.

DOCSTRING is used as a docstring for the defined function.

BUFFER-FUNCTION should name and return an auxiliary buffer to put
results in.

NONE-MESSAGE is used when CHECKER-FUNCTION returns no results.

BUFFER-HEADER is put into the auxiliary buffer first, followed by
calling INSERT-REFERENCE for each element in the list returned by
CHECKER-FUNCTION."
  `(defun ,name (&optional silent)
     ,(concat
       docstring
       "\n\nIf SILENT is non-nil, do not message anything when no
such references found.")
     (interactive "P")
     (unless (memq major-mode '(ein:markdown-mode))
       (user-error "Not available in current mode"))
     (let ((oldbuf (current-buffer))
           (refs (,checker-function))
           (refbuf (,buffer-function)))
       (if (null refs)
           (progn
             (when (not silent)
               (message ,none-message))
             (kill-buffer refbuf))
         (with-current-buffer refbuf
           (insert ,buffer-header)
           (dolist (ref refs)
             (,insert-reference ref oldbuf))
           (view-buffer-other-window refbuf)
           (goto-char (point-min))
           (forward-line 2))))))

(defun-markdown-ref-checker
  ein:markdown-check-refs
  "Show all undefined ein:markdown references in current
`ein:markdown-mode' buffer.  Links which have empty reference
definitions are considered to be defined."
  ein:markdown-get-undefined-refs
  ein:markdown-reference-check-buffer
  "No undefined references found"
  "The following references are undefined:\n\n"
  ein:markdown-insert-undefined-reference-button)


(defun-markdown-ref-checker
  ein:markdown-unused-refs
  "Show all unused ein:markdown references in current `ein:markdown-mode' buffer."
  ein:markdown-get-unused-refs
  ein:markdown-unused-references-buffer
  "No unused references found"
  "The following references are unused:\n\n"
  ein:markdown-insert-unused-reference-button)



;;; Lists =====================================================================

(defun ein:markdown-insert-list-item (&optional arg)
  "Insert a new list item.
If the point is inside unordered list, insert a bullet mark.  If
the point is inside ordered list, insert the next number followed
by a period.  Use the previous list item to determine the amount
of whitespace to place before and after list markers.

With a \\[universal-argument] prefix (i.e., when ARG is (4)),
decrease the indentation by one level.

With two \\[universal-argument] prefixes (i.e., when ARG is (16)),
increase the indentation by one level."
  (interactive "p")
  (let (bounds cur-indent marker indent new-indent new-loc)
    (save-match-data
      ;; Look for a list item on current or previous non-blank line
      (save-excursion
        (while (and (not (setq bounds (ein:markdown-cur-list-item-bounds)))
                    (not (bobp))
                    (ein:markdown-cur-line-blank-p))
          (forward-line -1)))
      (when bounds
        (cond ((save-excursion
                 (skip-chars-backward " \t")
                 (looking-at-p ein:markdown-regex-list))
               (beginning-of-line)
               (insert "\n")
               (forward-line -1))
              ((not (ein:markdown-cur-line-blank-p))
               (newline)))
        (setq new-loc (point)))
      ;; Look ahead for a list item on next non-blank line
      (unless bounds
        (save-excursion
          (while (and (null bounds)
                      (not (eobp))
                      (ein:markdown-cur-line-blank-p))
            (forward-line)
            (setq bounds (ein:markdown-cur-list-item-bounds))))
        (when bounds
          (setq new-loc (point))
          (unless (ein:markdown-cur-line-blank-p)
            (newline))))
      (if (not bounds)
          ;; When not in a list, start a new unordered one
          (progn
            (unless (ein:markdown-cur-line-blank-p)
              (insert "\n"))
            (insert ein:markdown-unordered-list-item-prefix))
        ;; Compute indentation and marker for new list item
        (setq cur-indent (nth 2 bounds))
        (setq marker (nth 4 bounds))
        ;; If current item is a GFM checkbox, insert new unchecked checkbox.
        (when (nth 5 bounds)
          (setq marker
                (concat marker
                        (replace-regexp-in-string "[Xx]" " " (nth 5 bounds)))))
        (cond
         ;; Dedent: decrement indentation, find previous marker.
         ((= arg 4)
          (setq indent (max (- cur-indent 4) 0))
          (let ((prev-bounds
                 (save-excursion
                   (goto-char (nth 0 bounds))
                   (when (ein:markdown-up-list)
                     (ein:markdown-cur-list-item-bounds)))))
            (when prev-bounds
              (setq marker (nth 4 prev-bounds)))))
         ;; Indent: increment indentation by 4, use same marker.
         ((= arg 16) (setq indent (+ cur-indent 4)))
         ;; Same level: keep current indentation and marker.
         (t (setq indent cur-indent)))
        (setq new-indent (make-string indent 32))
        (goto-char new-loc)
        (cond
         ;; Ordered list
         ((string-match-p "[0-9]" marker)
          (if (= arg 16) ;; starting a new column indented one more level
              (insert (concat new-indent "1. "))
            ;; Don't use previous match-data
            (set-match-data nil)
            ;; travel up to the last item and pick the correct number.  If
            ;; the argument was nil, "new-indent = cur-indent" is the same,
            ;; so we don't need special treatment. Neat.
            (save-excursion
              (while (and (not (looking-at (concat new-indent "\\([0-9]+\\)\\(\\.[ \t]*\\)")))
                          (>= (forward-line -1) 0))))
            (let* ((old-prefix (match-string 1))
                   (old-spacing (match-string 2))
                   (new-prefix (if old-prefix
                                   (int-to-string (1+ (string-to-number old-prefix)))
                                 "1"))
                   (space-adjust (- (length old-prefix) (length new-prefix)))
                   (new-spacing (if (and (match-string 2)
                                         (not (string-match-p "\t" old-spacing))
                                         (< space-adjust 0)
                                         (> space-adjust (- 1 (length (match-string 2)))))
                                    (substring (match-string 2) 0 space-adjust)
                                  (or old-spacing ". "))))
              (insert (concat new-indent new-prefix new-spacing)))))
         ;; Unordered list, GFM task list, or ordered list with hash mark
         ((string-match-p "[\\*\\+-]\\|#\\." marker)
          (insert new-indent marker))))
      ;; Propertize the newly inserted list item now
      (ein:markdown-syntax-propertize-list-items (line-beginning-position) (line-end-position)))))

(defun ein:markdown-move-list-item-up ()
  "Move the current list item up in the list when possible.
In nested lists, move child items with the parent item."
  (interactive)
  (let (cur prev old)
    (when (setq cur (ein:markdown-cur-list-item-bounds))
      (setq old (point))
      (goto-char (nth 0 cur))
      (if (ein:markdown-prev-list-item (nth 3 cur))
          (progn
            (setq prev (ein:markdown-cur-list-item-bounds))
            (condition-case nil
                (progn
                  (transpose-regions (nth 0 prev) (nth 1 prev)
                                     (nth 0 cur) (nth 1 cur) t)
                  (goto-char (+ (nth 0 prev) (- old (nth 0 cur)))))
              ;; Catch error in case regions overlap.
              (error (goto-char old))))
        (goto-char old)))))

(defun ein:markdown-move-list-item-down ()
  "Move the current list item down in the list when possible.
In nested lists, move child items with the parent item."
  (interactive)
  (let (cur next old)
    (when (setq cur (ein:markdown-cur-list-item-bounds))
      (setq old (point))
      (if (ein:markdown-next-list-item (nth 3 cur))
          (progn
            (setq next (ein:markdown-cur-list-item-bounds))
            (condition-case nil
                (progn
                  (transpose-regions (nth 0 cur) (nth 1 cur)
                                     (nth 0 next) (nth 1 next) nil)
                  (goto-char (+ old (- (nth 1 next) (nth 1 cur)))))
              ;; Catch error in case regions overlap.
              (error (goto-char old))))
        (goto-char old)))))

(defun ein:markdown-demote-list-item (&optional bounds)
  "Indent (or demote) the current list item.
Optionally, BOUNDS of the current list item may be provided if available.
In nested lists, demote child items as well."
  (interactive)
  (when (or bounds (setq bounds (ein:markdown-cur-list-item-bounds)))
    (save-excursion
      (let* ((item-start (set-marker (make-marker) (nth 0 bounds)))
             (item-end (set-marker (make-marker) (nth 1 bounds)))
             (list-start (progn (ein:markdown-beginning-of-list)
                                (set-marker (make-marker) (point))))
             (list-end (progn (ein:markdown-end-of-list)
                              (set-marker (make-marker) (point)))))
        (goto-char item-start)
        (while (< (point) item-end)
          (unless (ein:markdown-cur-line-blank-p)
            (insert (make-string ein:markdown-list-indent-width ? )))
          (forward-line))
        (ein:markdown-syntax-propertize-list-items list-start list-end)))))

(defun ein:markdown-promote-list-item (&optional bounds)
  "Unindent (or promote) the current list item.
Optionally, BOUNDS of the current list item may be provided if available.
In nested lists, demote child items as well."
  (interactive)
  (when (or bounds (setq bounds (ein:markdown-cur-list-item-bounds)))
    (save-excursion
      (save-match-data
        (let ((item-start (set-marker (make-marker) (nth 0 bounds)))
              (item-end (set-marker (make-marker) (nth 1 bounds)))
              (list-start (progn (ein:markdown-beginning-of-list)
                                 (set-marker (make-marker) (point))))
              (list-end (progn (ein:markdown-end-of-list)
                               (set-marker (make-marker) (point))))
              num regexp)
          (goto-char item-start)
          (when (looking-at (format "^[ ]\\{1,%d\\}"
                                    ein:markdown-list-indent-width))
            (setq num (- (match-end 0) (match-beginning 0)))
            (setq regexp (format "^[ ]\\{1,%d\\}" num))
            (while (and (< (point) item-end)
                        (re-search-forward regexp item-end t))
              (replace-match "" nil nil)
              (forward-line))
            (ein:markdown-syntax-propertize-list-items list-start list-end)))))))

(defun ein:markdown-cleanup-list-numbers-level (&optional pfx)
  "Update the numbering for level PFX (as a string of spaces).

Assume that the previously found match was for a numbered item in
a list."
  (let ((cpfx pfx)
        (idx 0)
        (continue t)
        (step t)
        (sep nil))
    (while (and continue (not (eobp)))
      (setq step t)
      (cond
       ((looking-at "^\\([\s-]*\\)[0-9]+\\. ")
        (setq cpfx (match-string-no-properties 1))
        (cond
         ((string= cpfx pfx)
          (save-excursion
            (replace-match
             (concat pfx (number-to-string (setq idx (1+ idx))) ". ")))
          (setq sep nil))
         ;; indented a level
         ((string< pfx cpfx)
          (setq sep (ein:markdown-cleanup-list-numbers-level cpfx))
          (setq step nil))
         ;; exit the loop
         (t
          (setq step nil)
          (setq continue nil))))

       ((looking-at "^\\([\s-]*\\)[^ \t\n\r].*$")
        (setq cpfx (match-string-no-properties 1))
        (cond
         ;; reset if separated before
         ((string= cpfx pfx) (when sep (setq idx 0)))
         ((string< cpfx pfx)
          (setq step nil)
          (setq continue nil))))
       (t (setq sep t)))

      (when step
        (beginning-of-line)
        (setq continue (= (forward-line) 0))))
    sep))

(defun ein:markdown-cleanup-list-numbers ()
  "Update the numbering of ordered lists."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (ein:markdown-cleanup-list-numbers-level "")))


;;; Movement ==================================================================

(defun ein:markdown-beginning-of-defun (&optional arg)
  "`beginning-of-defun-function' for ein:markdown.
This is used to find the beginning of the defun and should behave
like ‘beginning-of-defun’, returning non-nil if it found the
beginning of a defun.  It moves the point backward, right before a
heading which defines a defun.  When ARG is non-nil, repeat that
many times.  When ARG is negative, move forward to the ARG-th
following section."
  (or arg (setq arg 1))
  (when (< arg 0) (end-of-line))
  ;; Adjust position for setext headings.
  (when (and (thing-at-point-looking-at ein:markdown-regex-header-setext)
             (not (= (point) (match-beginning 0)))
             (not (ein:markdown-code-block-at-point-p)))
    (goto-char (match-end 0)))
  (let (found)
    ;; Move backward with positive argument.
    (while (and (not (bobp)) (> arg 0))
      (setq found nil)
      (while (and (not found)
                  (not (bobp))
                  (re-search-backward ein:markdown-regex-header nil 'move))
        (unless (ein:markdown-code-block-at-pos (match-beginning 0))
          (setq found (match-beginning 0))))
      (setq arg (1- arg)))
    ;; Move forward with negative argument.
    (while (and (not (eobp)) (< arg 0))
      (setq found nil)
      (while (and (not found)
                  (not (eobp))
                  (re-search-forward ein:markdown-regex-header nil 'move))
        (unless (ein:markdown-code-block-at-pos (match-beginning 0))
          (setq found (match-beginning 0))))
      (setq arg (1+ arg)))
    (when found
      (beginning-of-line)
      t)))

(defun ein:markdown-end-of-defun ()
  "`end-of-defun-function’ for ein:markdown.
This is used to find the end of the defun at point.
It is called with no argument, right after calling ‘beginning-of-defun-raw’,
so it can assume that point is at the beginning of the defun body.
It should move point to the first position after the defun."
  (or (eobp) (forward-char 1))
  (let (found)
    (while (and (not found)
                (not (eobp))
                (re-search-forward ein:markdown-regex-header nil 'move))
      (when (not (ein:markdown-code-block-at-pos (match-beginning 0)))
        (setq found (match-beginning 0))))
    (when found
      (goto-char found)
      (skip-syntax-backward "-"))))

(make-obsolete 'ein:markdown-beginning-of-block 'ein:markdown-beginning-of-text-block "v2.2")

(defun ein:markdown-beginning-of-text-block ()
  "Move backward to previous beginning of a plain text block.
This function simply looks for blank lines without considering
the surrounding context in light of ein:markdown syntax.  For that, see
`markdown-backward-block'."
  (interactive)
  (let ((start (point)))
    (if (re-search-backward ein:markdown-regex-block-separator nil t)
        (goto-char (match-end 0))
      (goto-char (point-min)))
    (when (and (= start (point)) (not (bobp)))
      (forward-line -1)
      (if (re-search-backward ein:markdown-regex-block-separator nil t)
          (goto-char (match-end 0))
        (goto-char (point-min))))))

(make-obsolete 'ein:markdown-end-of-block 'ein:markdown-end-of-text-block "v2.2")

(defun ein:markdown-end-of-text-block ()
  "Move forward to next beginning of a plain text block.
This function simply looks for blank lines without considering
the surrounding context in light of ein:markdown syntax.  For that, see
`markdown-forward-block'."
  (interactive)
  (beginning-of-line)
  (skip-chars-forward " \t\n")
  (when (= (point) (point-min))
    (forward-char))
  (if (re-search-forward ein:markdown-regex-block-separator nil t)
      (goto-char (match-end 0))
    (goto-char (point-max)))
  (skip-chars-backward " \t\n")
  (forward-line))

(defun ein:markdown-backward-paragraph (&optional arg)
  "Move the point to the start of the current paragraph.
With argument ARG, do it ARG times; a negative argument ARG = -N
means move forward N blocks."
  (interactive "^p")
  (or arg (setq arg 1))
  (if (< arg 0)
      (ein:markdown-forward-paragraph (- arg))
    (dotimes (_ arg)
      ;; Skip over whitespace in between paragraphs when moving backward.
      (skip-chars-backward " \t\n")
      (beginning-of-line)
      ;; Skip over code block endings.
      (when (ein:markdown-range-properties-exist
            (line-beginning-position) (line-end-position)
            '(ein:markdown-tilde-fence-end))
        (forward-line -1))
      ;; Skip over blank lines inside blockquotes.
      (while (and (not (eobp))
                  (looking-at ein:markdown-regex-blockquote)
                  (= (length (match-string 3)) 0))
        (forward-line -1))
      ;; Proceed forward based on the type of block of paragraph.
      (let (bounds skip)
        (cond
         ;; Blockquotes
         ((looking-at ein:markdown-regex-blockquote)
          (while (and (not (bobp))
                      (looking-at ein:markdown-regex-blockquote)
                      (> (length (match-string 3)) 0)) ;; not blank
            (forward-line -1))
          (forward-line))
         ;; List items
         ((setq bounds (ein:markdown-cur-list-item-bounds))
          (goto-char (nth 0 bounds)))
         ;; Other
         (t
          (while (and (not (bobp))
                      (not skip)
                      (not (ein:markdown-cur-line-blank-p))
                      (not (looking-at ein:markdown-regex-blockquote))
                      (not (ein:markdown-range-properties-exist
                            (line-beginning-position) (line-end-position)
                            '(ein:markdown-tilde-fence-end))))
            (setq skip (ein:markdown-range-properties-exist
                            (line-beginning-position) (line-end-position)
                            '(ein:markdown-tilde-fence-begin)))
            (forward-line -1))
          (unless (bobp)
            (forward-line 1))))))))

(defun ein:markdown-forward-paragraph (&optional arg)
  "Move forward to the next end of a paragraph.
With argument ARG, do it ARG times; a negative argument ARG = -N
means move backward N blocks."
  (interactive "^p")
  (or arg (setq arg 1))
  (if (< arg 0)
      (ein:markdown-backward-paragraph (- arg))
    (dotimes (_ arg)
      ;; Skip whitespace in between paragraphs.
      (when (ein:markdown-cur-line-blank-p)
        (skip-syntax-forward "-")
        (beginning-of-line))
      ;; Proceed forward based on the type of block.
      (let (bounds skip)
        (cond
         ;; Blockquotes
         ((looking-at ein:markdown-regex-blockquote)
          ;; Skip over blank lines inside blockquotes.
          (while (and (not (eobp))
                      (looking-at ein:markdown-regex-blockquote)
                      (= (length (match-string 3)) 0))
            (forward-line))
          ;; Move to end of quoted text block
          (while (and (not (eobp))
                      (looking-at ein:markdown-regex-blockquote)
                      (> (length (match-string 3)) 0)) ;; not blank
            (forward-line)))
         ;; List items
         ((and (ein:markdown-cur-list-item-bounds)
               (setq bounds (ein:markdown-next-list-item-bounds)))
          (goto-char (nth 0 bounds)))
         ;; Other
         (t
          (forward-line)
          (while (and (not (eobp))
                      (not skip)
                      (not (ein:markdown-cur-line-blank-p))
                      (not (looking-at ein:markdown-regex-blockquote))
                      (not (ein:markdown-range-properties-exist
                            (line-beginning-position) (line-end-position)
                            '(ein:markdown-tilde-fence-begin))))
            (setq skip (ein:markdown-range-properties-exist
                        (line-beginning-position) (line-end-position)
                        '(ein:markdown-tilde-fence-end)))
            (forward-line))))))))

(defun ein:markdown-backward-block (&optional arg)
  "Move the point to the start of the current ein:markdown block.
Moves across complete code blocks, list items, and blockquotes,
but otherwise stops at blank lines, headers, and horizontal
rules.  With argument ARG, do it ARG times; a negative argument
ARG = -N means move forward N blocks."
  (interactive "^p")
  (or arg (setq arg 1))
  (if (< arg 0)
      (ein:markdown-forward-block (- arg))
    (dotimes (_ arg)
      ;; Skip over whitespace in between blocks when moving backward,
      ;; unless at a block boundary with no whitespace.
      (skip-syntax-backward "-")
      (beginning-of-line)
      ;; Proceed forward based on the type of block.
      (cond
       ;; Code blocks
       ((and (ein:markdown-code-block-at-pos (point)) ;; this line
             (ein:markdown-code-block-at-pos (line-beginning-position 0))) ;; previous line
        (forward-line -1)
        (while (and (ein:markdown-code-block-at-point-p) (not (bobp)))
          (forward-line -1))
        (forward-line))
       ;; Headings
       ((ein:markdown-heading-at-point)
        (goto-char (match-beginning 0)))
       ;; Horizontal rules
       ((looking-at ein:markdown-regex-hr))
       ;; Blockquotes
       ((looking-at ein:markdown-regex-blockquote)
        (forward-line -1)
        (while (and (looking-at ein:markdown-regex-blockquote)
                    (not (bobp)))
          (forward-line -1))
        (forward-line))
       ;; List items
       ((ein:markdown-cur-list-item-bounds)
        (ein:markdown-beginning-of-list))
       ;; Other
       (t
        ;; Move forward in case it is a one line regular paragraph.
        (unless (ein:markdown-next-line-blank-p)
          (forward-line))
        (unless (ein:markdown-prev-line-blank-p)
          (ein:markdown-backward-paragraph)))))))

(defun ein:markdown-forward-block (&optional arg)
  "Move forward to the next end of a ein:markdown block.
Moves across complete code blocks, list items, and blockquotes,
but otherwise stops at blank lines, headers, and horizontal
rules.  With argument ARG, do it ARG times; a negative argument
ARG = -N means move backward N blocks."
  (interactive "^p")
  (or arg (setq arg 1))
  (if (< arg 0)
      (ein:markdown-backward-block (- arg))
    (dotimes (_ arg)
      ;; Skip over whitespace in between blocks when moving forward.
      (if (ein:markdown-cur-line-blank-p)
          (skip-syntax-forward "-")
        (beginning-of-line))
      ;; Proceed forward based on the type of block.
      (cond
       ;; Code blocks
       ((ein:markdown-code-block-at-point-p)
        (forward-line)
        (while (and (ein:markdown-code-block-at-point-p) (not (eobp)))
          (forward-line)))
       ;; Headings
       ((looking-at ein:markdown-regex-header)
        (goto-char (or (match-end 4) (match-end 2) (match-end 3)))
        (forward-line))
       ;; Horizontal rules
       ((looking-at ein:markdown-regex-hr)
        (forward-line))
       ;; Blockquotes
       ((looking-at ein:markdown-regex-blockquote)
        (forward-line)
        (while (and (looking-at ein:markdown-regex-blockquote) (not (eobp)))
          (forward-line)))
       ;; List items
       ((ein:markdown-cur-list-item-bounds)
        (ein:markdown-end-of-list)
        (forward-line))
       ;; Other
       (t (ein:markdown-forward-paragraph))))
    (skip-syntax-backward "-")
    (unless (eobp)
      (forward-char 1))))

(defun ein:markdown-backward-page (&optional count)
  "Move backward to boundary of the current toplevel section.
With COUNT, repeat, or go forward if negative."
  (interactive "p")
  (or count (setq count 1))
  (if (< count 0)
      (ein:markdown-forward-page (- count))
    (skip-syntax-backward "-")
    (or (ein:markdown-back-to-heading-over-code-block t t)
        (goto-char (point-min)))
    (when (looking-at ein:markdown-regex-header)
      (let ((level (ein:markdown-outline-level)))
        (when (> level 1) (ein:markdown-up-heading level))
        (when (> count 1)
          (condition-case nil
              (ein:markdown-backward-same-level (1- count))
            (error (goto-char (point-min)))))))))

(defun ein:markdown-forward-page (&optional count)
  "Move forward to boundary of the current toplevel section.
With COUNT, repeat, or go backward if negative."
  (interactive "p")
  (or count (setq count 1))
  (if (< count 0)
      (ein:markdown-backward-page (- count))
    (if (ein:markdown-back-to-heading-over-code-block t t)
        (let ((level (ein:markdown-outline-level)))
          (when (> level 1) (ein:markdown-up-heading level))
          (condition-case nil
              (ein:markdown-forward-same-level count)
            (error (goto-char (point-max)))))
      (ein:markdown-next-visible-heading 1))))

(defun ein:markdown-next-link ()
  "Jump to next inline, reference, or wiki link.
If successful, return point.  Otherwise, return nil.
See `markdown-wiki-link-p' and `markdown-previous-wiki-link'."
  (interactive)
  (let ((opoint (point)))
    (when (ein:markdown-link-p)
      ;; At a link already, move past it.
      (goto-char (+ (match-end 0) 1)))
    (while (and (re-search-forward (ein:markdown-make-regex-link-generic) nil t)
                (ein:markdown-code-block-at-point-p)
                (< (point) (point-max))))
    (if (and (not (eq (point) opoint)) (ein:markdown-link-p))
        ;; Group 1 will move past non-escape character in wiki link regexp.
        ;; Go to beginning of group zero for all other link types.
        (goto-char (or (match-beginning 1) (match-beginning 0)))
      (goto-char opoint)
      nil)))

(defun ein:markdown-previous-link ()
  "Jump to previous wiki link.
If successful, return point.  Otherwise, return nil.
See `markdown-wiki-link-p' and `markdown-next-wiki-link'."
  (interactive)
  (let ((opoint (point)))
    (while (and (re-search-backward (ein:markdown-make-regex-link-generic) nil t)
                (ein:markdown-code-block-at-point-p)
                (> (point) (point-min))))
    (if (and (not (eq (point) opoint)) (ein:markdown-link-p))
        (goto-char (or (match-beginning 1) (match-beginning 0)))
      (goto-char opoint)
      nil)))


;;; Outline ===================================================================

(defun ein:markdown-move-heading-common (move-fn &optional arg adjust)
  "Wrapper for `outline-mode' functions to skip false positives.
MOVE-FN is a function and ARG is its argument. For example,
headings inside preformatted code blocks may match
`outline-regexp' but should not be considered as headings.
When ADJUST is non-nil, adjust the point for interactive calls
to avoid leaving the point at invisible markup.  This adjustment
generally should only be done for interactive calls, since other
functions may expect the point to be at the beginning of the
regular expression."
  (let ((prev -1) (start (point)))
    (if arg (funcall move-fn arg) (funcall move-fn))
    (while (and (/= prev (point)) (ein:markdown-code-block-at-point-p))
      (setq prev (point))
      (if arg (funcall move-fn arg) (funcall move-fn)))
    ;; Adjust point for setext headings and invisible text.
    (save-match-data
      (when (and adjust (thing-at-point-looking-at ein:markdown-regex-header))
        (goto-char (or (match-beginning 1) (match-beginning 4)))))
    (if (= (point) start) nil (point))))

(defun ein:markdown-next-visible-heading (arg)
  "Move to the next visible heading line of any level.
With argument, repeats or can move backward if negative. ARG is
passed to `outline-next-visible-heading'."
  (interactive "p")
  (ein:markdown-move-heading-common #'outline-next-visible-heading arg 'adjust))

(defun ein:markdown-previous-visible-heading (arg)
  "Move to the previous visible heading line of any level.
With argument, repeats or can move backward if negative. ARG is
passed to `outline-previous-visible-heading'."
  (interactive "p")
  (ein:markdown-move-heading-common #'outline-previous-visible-heading arg 'adjust))

(defun ein:markdown-next-heading ()
  "Move to the next heading line of any level."
  (ein:markdown-move-heading-common #'outline-next-heading))

(defun ein:markdown-previous-heading ()
  "Move to the previous heading line of any level."
  (ein:markdown-move-heading-common #'outline-previous-heading))

(defun ein:markdown-back-to-heading-over-code-block (&optional invisible-ok no-error)
  "Move back to the beginning of the previous heading.
Returns t if the point is at a heading, the location if a heading
was found, and nil otherwise.
Only visible heading lines are considered, unless INVISIBLE-OK is
non-nil.  Throw an error if there is no previous heading unless
NO-ERROR is non-nil.
Leaves match data intact for `markdown-regex-header'."
  (beginning-of-line)
  (or (and (ein:markdown-heading-at-point)
           (not (ein:markdown-code-block-at-point-p)))
      (let (found)
        (save-excursion
          (while (and (not found)
                      (re-search-backward ein:markdown-regex-header nil t))
            (when (and (or invisible-ok (not (outline-invisible-p)))
                       (not (ein:markdown-code-block-at-point-p)))
              (setq found (point))))
          (if (not found)
              (unless no-error (user-error "Before first heading"))
            (setq found (point))))
        (when found (goto-char found)))))

(defun ein:markdown-forward-same-level (arg)
  "Move forward to the ARG'th heading at same level as this one.
Stop at the first and last headings of a superior heading."
  (interactive "p")
  (ein:markdown-back-to-heading-over-code-block)
  (ein:markdown-move-heading-common #'outline-forward-same-level arg 'adjust))

(defun ein:markdown-backward-same-level (arg)
  "Move backward to the ARG'th heading at same level as this one.
Stop at the first and last headings of a superior heading."
  (interactive "p")
  (ein:markdown-back-to-heading-over-code-block)
  (while (> arg 0)
    (let ((point-to-move-to
           (save-excursion
             (ein:markdown-move-heading-common #'outline-get-last-sibling nil 'adjust))))
      (if point-to-move-to
          (progn
            (goto-char point-to-move-to)
            (setq arg (1- arg)))
        (user-error "No previous same-level heading")))))

(defun ein:markdown-up-heading (arg)
  "Move to the visible heading line of which the present line is a subheading.
With argument, move up ARG levels."
  (interactive "p")
  (and (called-interactively-p 'any)
       (not (eq last-command 'ein:markdown-up-heading)) (push-mark))
  (ein:markdown-move-heading-common #'outline-up-heading arg 'adjust))

(defun ein:markdown-back-to-heading (&optional invisible-ok)
  "Move to previous heading line, or beg of this line if it's a heading.
Only visible heading lines are considered, unless INVISIBLE-OK is non-nil."
  (ein:markdown-move-heading-common #'outline-back-to-heading invisible-ok))

(defalias 'ein:markdown-end-of-heading 'outline-end-of-heading)

(defun ein:markdown-on-heading-p ()
  "Return non-nil if point is on a heading line."
  (get-text-property (line-beginning-position) 'ein:markdown-heading))

(defun ein:markdown-end-of-subtree (&optional invisible-OK)
  "Move to the end of the current subtree.
Only visible heading lines are considered, unless INVISIBLE-OK is
non-nil.
Derived from `org-end-of-subtree'."
  (ein:markdown-back-to-heading invisible-OK)
  (let ((first t)
        (level (ein:markdown-outline-level)))
    (while (and (not (eobp))
                (or first (> (ein:markdown-outline-level) level)))
      (setq first nil)
      (ein:markdown-next-heading))
    (if (memq (preceding-char) '(?\n ?\^M))
        (progn
          ;; Go to end of line before heading
          (forward-char -1)
          (if (memq (preceding-char) '(?\n ?\^M))
              ;; leave blank line before heading
              (forward-char -1)))))
  (point))

(defun ein:markdown-outline-fix-visibility ()
  "Hide any false positive headings that should not be shown.
For example, headings inside preformatted code blocks may match
`outline-regexp' but should not be shown as headings when cycling.
Also, the ending --- line in metadata blocks appears to be a
setext header, but should not be folded."
  (save-excursion
    (goto-char (point-min))
    ;; Unhide any false positives in metadata blocks
    (when (ein:markdown-text-property-at-point 'ein:markdown-yaml-metadata-begin)
      (let ((body (progn (forward-line)
                         (ein:markdown-text-property-at-point
                          'ein:markdown-yaml-metadata-section))))
        (when body
          (let ((end (progn (goto-char (cl-second body))
                            (ein:markdown-text-property-at-point
                             'ein:markdown-yaml-metadata-end))))
            (outline-flag-region (point-min) (1+ (cl-second end)) nil)))))
    ;; Hide any false positives in code blocks
    (unless (outline-on-heading-p)
      (outline-next-visible-heading 1))
    (while (< (point) (point-max))
      (when (ein:markdown-code-block-at-point-p)
        (outline-flag-region (1- (line-beginning-position)) (line-end-position) t))
      (outline-next-visible-heading 1))))

(defvar ein:markdown-cycle-global-status 1)
(defvar ein:markdown-cycle-subtree-status nil)

(defun ein:markdown-next-preface ()
  (let (finish)
    (while (and (not finish) (re-search-forward (concat "\n\\(?:" outline-regexp "\\)")
                                                nil 'move))
      (unless (ein:markdown-code-block-at-point-p)
        (goto-char (match-beginning 0))
        (setq finish t))))
  (when (and (bolp) (or outline-blank-line (eobp)) (not (bobp)))
    (forward-char -1)))

(defun ein:markdown-show-entry ()
  (save-excursion
    (outline-back-to-heading t)
    (outline-flag-region (1- (point))
                         (progn
                           (ein:markdown-next-preface)
                           (if (= 1 (- (point-max) (point)))
                               (point-max)
                             (point)))
                         nil)))

;; This function was originally derived from `org-cycle' from org.el.
(defun ein:markdown-cycle (&optional arg)
  "Visibility cycling for ein:markdown mode.
If ARG is t, perform global visibility cycling.  If the point is
at an atx-style header, cycle visibility of the corresponding
subtree.  Otherwise, indent the current line or insert a tab,
as appropriate, by calling `indent-for-tab-command'."
  (interactive "P")
  (cond

   ;; Global cycling
   ((eq arg t)
    (cond
     ;; Move from overview to contents
     ((and (eq last-command this-command)
           (eq ein:markdown-cycle-global-status 2))
      (ein:markdown-hide-sublevels 1)
      (message "CONTENTS")
      (setq ein:markdown-cycle-global-status 3)
      (ein:markdown-outline-fix-visibility))
     ;; Move from contents to all
     ((and (eq last-command this-command)
           (eq ein:markdown-cycle-global-status 3))
      (ein:markdown-show-all)
      (message "SHOW ALL")
      (setq ein:markdown-cycle-global-status 1))
     ;; Defaults to overview
     (t
      (ein:markdown-hide-body)
      (message "OVERVIEW")
      (setq ein:markdown-cycle-global-status 2)
      (ein:markdown-outline-fix-visibility))))

   ;; At a heading: rotate between three different views
   ((save-excursion (beginning-of-line 1) (ein:markdown-on-heading-p))
    (ein:markdown-back-to-heading)
    (let ((goal-column 0) eoh eol eos)
      ;; Determine boundaries
      (save-excursion
        (ein:markdown-back-to-heading)
        (save-excursion
          (beginning-of-line 2)
          (while (and (not (eobp)) ;; this is like `next-line'
                      (get-char-property (1- (point)) 'invisible))
            (beginning-of-line 2)) (setq eol (point)))
        (ein:markdown-end-of-heading)   (setq eoh (point))
        (ein:markdown-end-of-subtree t)
        (skip-chars-forward " \t\n")
        (beginning-of-line 1) ; in case this is an item
        (setq eos (1- (point))))
      ;; Find out what to do next and set `this-command'
      (cond
       ;; Nothing is hidden behind this heading
       ((= eos eoh)
        (message "EMPTY ENTRY")
        (setq ein:markdown-cycle-subtree-status nil))
       ;; Entire subtree is hidden in one line: open it
       ((>= eol eos)
        (ein:markdown-show-entry)
        (ein:markdown-show-children)
        (message "CHILDREN")
        (setq ein:markdown-cycle-subtree-status 'children))
       ;; We just showed the children, now show everything.
       ((and (eq last-command this-command)
             (eq ein:markdown-cycle-subtree-status 'children))
        (ein:markdown-show-subtree)
        (message "SUBTREE")
        (setq ein:markdown-cycle-subtree-status 'subtree))
       ;; Default action: hide the subtree.
       (t
        (ein:markdown-hide-subtree)
        (message "FOLDED")
        (setq ein:markdown-cycle-subtree-status 'folded)))))

   ;; In a table, move forward by one cell
   ((ein:markdown-table-at-point-p)
    (call-interactively #'ein:markdown-table-forward-cell))

   ;; Otherwise, indent as appropriate
   (t
    (indent-for-tab-command))))

(defun ein:markdown-shifttab ()
  "Handle S-TAB keybinding based on context.
When in a table, move backward one cell.
Otherwise, cycle global heading visibility by calling
`markdown-cycle' with argument t."
  (interactive)
  (cond ((ein:markdown-table-at-point-p)
         (call-interactively #'ein:markdown-table-backward-cell))
        (t (ein:markdown-cycle t))))

(defun ein:markdown-outline-level ()
  "Return the depth to which a statement is nested in the outline."
  (cond
   ((and (match-beginning 0)
         (ein:markdown-code-block-at-pos (match-beginning 0)))
    7) ;; Only 6 header levels are defined.
   ((match-end 2) 1)
   ((match-end 3) 2)
   ((match-end 4)
    (length (ein:markdown-trim-whitespace (match-string-no-properties 4))))))

(defun ein:markdown-promote-subtree (&optional arg)
  "Promote the current subtree of ATX headings.
Note that ein:markdown does not support heading levels higher than
six and therefore level-six headings will not be promoted
further. If ARG is non-nil promote the heading, otherwise
demote."
  (interactive "*P")
  (save-excursion
    (when (and (or (thing-at-point-looking-at ein:markdown-regex-header-atx)
                   (re-search-backward ein:markdown-regex-header-atx nil t))
               (not (ein:markdown-code-block-at-point-p)))
      (let ((level (length (match-string 1)))
            (promote-or-demote (if arg 1 -1))
            (remove 't))
        (ein:markdown-cycle-atx promote-or-demote remove)
        (catch 'end-of-subtree
          (while (and (ein:markdown-next-heading)
                      (looking-at ein:markdown-regex-header-atx))
            ;; Exit if this not a higher level heading; promote otherwise.
            (if (and (looking-at ein:markdown-regex-header-atx)
                     (<= (length (match-string-no-properties 1)) level))
                (throw 'end-of-subtree nil)
              (ein:markdown-cycle-atx promote-or-demote remove))))))))

(defun ein:markdown-demote-subtree ()
  "Demote the current subtree of ATX headings."
  (interactive)
  (ein:markdown-promote-subtree t))

(defun ein:markdown-move-subtree-up ()
  "Move the current subtree of ATX headings up."
  (interactive)
  (outline-move-subtree-up 1))

(defun ein:markdown-move-subtree-down ()
  "Move the current subtree of ATX headings down."
  (interactive)
  (outline-move-subtree-down 1))

(defun ein:markdown-outline-next ()
  "Move to next list item, when in a list, or next visible heading."
  (interactive)
  (let ((bounds (ein:markdown-next-list-item-bounds)))
    (if bounds
        (goto-char (nth 0 bounds))
      (ein:markdown-next-visible-heading 1))))

(defun ein:markdown-outline-previous ()
  "Move to previous list item, when in a list, or previous visible heading."
  (interactive)
  (let ((bounds (ein:markdown-prev-list-item-bounds)))
    (if bounds
        (goto-char (nth 0 bounds))
      (ein:markdown-previous-visible-heading 1))))

(defun ein:markdown-outline-next-same-level ()
  "Move to next list item or heading of same level."
  (interactive)
  (let ((bounds (ein:markdown-cur-list-item-bounds)))
    (if bounds
        (ein:markdown-next-list-item (nth 3 bounds))
      (ein:markdown-forward-same-level 1))))

(defun ein:markdown-outline-previous-same-level ()
  "Move to previous list item or heading of same level."
  (interactive)
  (let ((bounds (ein:markdown-cur-list-item-bounds)))
    (if bounds
        (ein:markdown-prev-list-item (nth 3 bounds))
      (ein:markdown-backward-same-level 1))))

(defun ein:markdown-outline-up ()
  "Move to previous list item, when in a list, or next heading."
  (interactive)
  (unless (ein:markdown-up-list)
    (ein:markdown-up-heading 1)))


;;; Marking and Narrowing =====================================================

(defun ein:markdown-mark-paragraph ()
  "Put mark at end of this block, point at beginning.
The block marked is the one that contains point or follows point.

Interactively, if this command is repeated or (in Transient Mark
mode) if the mark is active, it marks the next block after the
ones already marked."
  (interactive)
  (if (or (and (eq last-command this-command) (mark t))
          (and transient-mark-mode mark-active))
      (set-mark
       (save-excursion
         (goto-char (mark))
         (ein:markdown-forward-paragraph)
         (point)))
    (let ((beginning-of-defun-function 'ein:markdown-backward-paragraph)
          (end-of-defun-function 'ein:markdown-forward-paragraph))
      (mark-defun))))

(defun ein:markdown-mark-block ()
  "Put mark at end of this block, point at beginning.
The block marked is the one that contains point or follows point.

Interactively, if this command is repeated or (in Transient Mark
mode) if the mark is active, it marks the next block after the
ones already marked."
  (interactive)
  (if (or (and (eq last-command this-command) (mark t))
          (and transient-mark-mode mark-active))
      (set-mark
       (save-excursion
         (goto-char (mark))
         (ein:markdown-forward-block)
         (point)))
    (let ((beginning-of-defun-function 'ein:markdown-backward-block)
          (end-of-defun-function 'ein:markdown-forward-block))
      (mark-defun))))

(defun ein:markdown-narrow-to-block ()
  "Make text outside current block invisible.
The current block is the one that contains point or follows point."
  (interactive)
  (let ((beginning-of-defun-function 'ein:markdown-backward-block)
        (end-of-defun-function 'ein:markdown-forward-block))
    (narrow-to-defun)))

(defun ein:markdown-mark-text-block ()
  "Put mark at end of this plain text block, point at beginning.
The block marked is the one that contains point or follows point.

Interactively, if this command is repeated or (in Transient Mark
mode) if the mark is active, it marks the next block after the
ones already marked."
  (interactive)
  (if (or (and (eq last-command this-command) (mark t))
          (and transient-mark-mode mark-active))
      (set-mark
       (save-excursion
         (goto-char (mark))
         (ein:markdown-end-of-text-block)
         (point)))
    (let ((beginning-of-defun-function 'ein:markdown-beginning-of-text-block)
          (end-of-defun-function 'ein:markdown-end-of-text-block))
      (mark-defun))))

(defun ein:markdown-mark-page ()
  "Put mark at end of this top level section, point at beginning.
The top level section marked is the one that contains point or
follows point.

Interactively, if this command is repeated or (in Transient Mark
mode) if the mark is active, it marks the next page after the
ones already marked."
  (interactive)
  (if (or (and (eq last-command this-command) (mark t))
          (and transient-mark-mode mark-active))
      (set-mark
       (save-excursion
         (goto-char (mark))
         (ein:markdown-forward-page)
         (point)))
    (let ((beginning-of-defun-function 'ein:markdown-backward-page)
          (end-of-defun-function 'ein:markdown-forward-page))
      (mark-defun))))

(defun ein:markdown-narrow-to-page ()
  "Make text outside current top level section invisible.
The current section is the one that contains point or follows point."
  (interactive)
  (let ((beginning-of-defun-function 'ein:markdown-backward-page)
        (end-of-defun-function 'ein:markdown-forward-page))
    (narrow-to-defun)))

(defun ein:markdown-mark-subtree ()
  "Mark the current subtree.
This puts point at the start of the current subtree, and mark at the end."
  (interactive)
  (let ((beg))
    (if (ein:markdown-heading-at-point)
        (beginning-of-line)
      (ein:markdown-previous-visible-heading 1))
    (setq beg (point))
    (ein:markdown-end-of-subtree)
    (push-mark (point) nil t)
    (goto-char beg)))

(defun ein:markdown-narrow-to-subtree ()
  "Narrow buffer to the current subtree."
  (interactive)
  (save-excursion
    (save-match-data
      (narrow-to-region
       (progn (ein:markdown-back-to-heading-over-code-block t) (point))
       (progn (ein:markdown-end-of-subtree)
          (if (and (ein:markdown-heading-at-point) (not (eobp)))
          (backward-char 1))
          (point))))))


;;; Generic Structure Editing, Completion, and Cycling Commands ===============

(defun ein:markdown-move-up ()
  "Move thing at point up.
When in a list item, call `markdown-move-list-item-up'.
When in a table, call `markdown-table-move-row-up'.
Otherwise, move the current heading subtree up with
`markdown-move-subtree-up'."
  (interactive)
  (cond
   ((ein:markdown-list-item-at-point-p)
    (call-interactively #'ein:markdown-move-list-item-up))
   ((ein:markdown-table-at-point-p)
    (call-interactively #'ein:markdown-table-move-row-up))
   (t
    (call-interactively #'ein:markdown-move-subtree-up))))

(defun ein:markdown-move-down ()
  "Move thing at point down.
When in a list item, call `markdown-move-list-item-down'.
Otherwise, move the current heading subtree up with
`markdown-move-subtree-down'."
  (interactive)
  (cond
   ((ein:markdown-list-item-at-point-p)
    (call-interactively #'ein:markdown-move-list-item-down))
   ((ein:markdown-table-at-point-p)
    (call-interactively #'ein:markdown-table-move-row-down))
   (t
    (call-interactively #'ein:markdown-move-subtree-down))))

(defun ein:markdown-promote ()
  "Promote or move element at point to the left.
Depending on the context, this function will promote a heading or
list item at the point, move a table column to the left, or cycle
markup."
  (interactive)
  (let (bounds)
    (cond
     ;; Promote atx heading subtree
     ((thing-at-point-looking-at ein:markdown-regex-header-atx)
      (ein:markdown-promote-subtree))
     ;; Promote setext heading
     ((thing-at-point-looking-at ein:markdown-regex-header-setext)
      (ein:markdown-cycle-setext -1))
     ;; Promote horizonal rule
     ((thing-at-point-looking-at ein:markdown-regex-hr)
      (ein:markdown-cycle-hr -1))
     ;; Promote list item
     ((setq bounds (ein:markdown-cur-list-item-bounds))
      (ein:markdown-promote-list-item bounds))
     ;; Move table column to the left
     ((ein:markdown-table-at-point-p)
      (call-interactively #'ein:markdown-table-move-column-left))
     ;; Promote bold
     ((thing-at-point-looking-at ein:markdown-regex-bold)
      (ein:markdown-cycle-bold))
     ;; Promote italic
     ((thing-at-point-looking-at ein:markdown-regex-italic)
      (ein:markdown-cycle-italic))
     (t
      (user-error "Nothing to promote at point")))))

(defun ein:markdown-demote ()
  "Demote or move element at point to the right.
Depending on the context, this function will demote a heading or
list item at the point, move a table column to the right, or cycle
or remove markup."
  (interactive)
  (let (bounds)
    (cond
     ;; Demote atx heading subtree
     ((thing-at-point-looking-at ein:markdown-regex-header-atx)
      (ein:markdown-demote-subtree))
     ;; Demote setext heading
     ((thing-at-point-looking-at ein:markdown-regex-header-setext)
      (ein:markdown-cycle-setext 1))
     ;; Demote horizonal rule
     ((thing-at-point-looking-at ein:markdown-regex-hr)
      (ein:markdown-cycle-hr 1))
     ;; Demote list item
     ((setq bounds (ein:markdown-cur-list-item-bounds))
      (ein:markdown-demote-list-item bounds))
     ;; Move table column to the right
     ((ein:markdown-table-at-point-p)
      (call-interactively #'ein:markdown-table-move-column-right))
     ;; Demote bold
     ((thing-at-point-looking-at ein:markdown-regex-bold)
      (ein:markdown-cycle-bold))
     ;; Demote italic
     ((thing-at-point-looking-at ein:markdown-regex-italic)
      (ein:markdown-cycle-italic))
     (t
      (user-error "Nothing to demote at point")))))


;;; Commands ==================================================================

(defun ein:markdown (&optional output-buffer-name)
  "Run `markdown-command' on buffer, sending output to OUTPUT-BUFFER-NAME.
The output buffer name defaults to `markdown-output-buffer-name'.
Return the name of the output buffer used."
  (interactive)
  (save-window-excursion
    (let ((begin-region)
          (end-region))
      (if (ein:markdown-use-region-p)
          (setq begin-region (region-beginning)
                end-region (region-end))
        (setq begin-region (point-min)
              end-region (point-max)))

      (unless output-buffer-name
        (setq output-buffer-name ein:markdown-output-buffer-name))
      (let ((exit-code
             (cond
              ;; Handle case when `markdown-command' does not read from stdin
              ((and (stringp ein:markdown-command) ein:markdown-command-needs-filename)
               (if (not buffer-file-name)
                   (user-error "Must be visiting a file")
                 ;; Don’t use ‘shell-command’ because it’s not guaranteed to
                 ;; return the exit code of the process.
                 (shell-command-on-region
                  ;; Pass an empty region so that stdin is empty.
                  (point) (point)
                  (concat ein:markdown-command " "
                          (shell-quote-argument buffer-file-name))
                  output-buffer-name)))
              ;; Pass region to `markdown-command' via stdin
              (t
               (let ((buf (get-buffer-create output-buffer-name)))
                 (with-current-buffer buf
                   (setq buffer-read-only nil)
                   (erase-buffer))
                 (if (stringp ein:markdown-command)
                     (call-process-region begin-region end-region
                                          shell-file-name nil buf nil
                                          shell-command-switch ein:markdown-command)
                   (funcall ein:markdown-command begin-region end-region buf)
                   ;; If the ‘markdown-command’ function didn’t signal an
                   ;; error, assume it succeeded by binding ‘exit-code’ to 0.
                   0))))))
        ;; The exit code can be a signal description string, so don’t use ‘=’
        ;; or ‘zerop’.
        (unless (eq exit-code 0)
          (user-error "%s failed with exit code %s"
                      ein:markdown-command exit-code))))
    output-buffer-name))

(defun ein:markdown-standalone (&optional output-buffer-name)
  "Special function to provide standalone HTML output.
Insert the output in the buffer named OUTPUT-BUFFER-NAME."
  (interactive)
  (setq output-buffer-name (ein:markdown output-buffer-name))
  (with-current-buffer output-buffer-name
    (set-buffer output-buffer-name)
    (unless (ein:markdown-output-standalone-p)
      (ein:markdown-add-xhtml-header-and-footer output-buffer-name))
    (goto-char (point-min))
    (html-mode))
  output-buffer-name)

(defun ein:markdown-other-window (&optional output-buffer-name)
  "Run `markdown-command' on current buffer and display in other window.
When OUTPUT-BUFFER-NAME is given, insert the output in the buffer with
that name."
  (interactive)
  (ein:markdown-display-buffer-other-window
   (ein:markdown-standalone output-buffer-name)))

(defun ein:markdown-output-standalone-p ()
  "Determine whether `markdown-command' output is standalone XHTML.
Standalone XHTML output is identified by an occurrence of
`markdown-xhtml-standalone-regexp' in the first five lines of output."
  (save-excursion
    (goto-char (point-min))
    (save-match-data
      (re-search-forward
       ein:markdown-xhtml-standalone-regexp
       (save-excursion (goto-char (point-min)) (forward-line 4) (point))
       t))))

(defun ein:markdown-stylesheet-link-string (stylesheet-path)
  (concat "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\""
          stylesheet-path
          "\"  />"))

(defun ein:markdown-add-xhtml-header-and-footer (title)
  "Wrap XHTML header and footer with given TITLE around current buffer."
  (goto-char (point-min))
  (insert "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
          "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n"
          "\t\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n\n"
          "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n\n"
          "<head>\n<title>")
  (insert title)
  (insert "</title>\n")
  (unless (= (length ein:markdown-content-type) 0)
    (insert
     (format
      "<meta http-equiv=\"Content-Type\" content=\"%s;charset=%s\"/>\n"
      ein:markdown-content-type
      (or (and ein:markdown-coding-system
               (fboundp 'coding-system-get)
               (coding-system-get ein:markdown-coding-system
                                  'mime-charset))
          (and (fboundp 'coding-system-get)
               (coding-system-get buffer-file-coding-system
                                  'mime-charset))
          "utf-8"))))
  (if (> (length ein:markdown-css-paths) 0)
      (insert (mapconcat #'ein:markdown-stylesheet-link-string
                         ein:markdown-css-paths "\n")))
  (when (> (length ein:markdown-xhtml-header-content) 0)
    (insert ein:markdown-xhtml-header-content))
  (insert "\n</head>\n\n"
          "<body>\n\n")
  (when (> (length ein:markdown-xhtml-body-preamble) 0)
    (insert ein:markdown-xhtml-body-preamble "\n"))
  (goto-char (point-max))
  (when (> (length ein:markdown-xhtml-body-epilogue) 0)
    (insert "\n" ein:markdown-xhtml-body-epilogue))
  (insert "\n"
          "</body>\n"
          "</html>\n"))

(defun ein:markdown-visual-lines-between-points (beg end)
  (save-excursion
    (goto-char beg)
    (cl-loop with count = 0
             while (progn (end-of-visual-line)
                          (and (< (point) end) (line-move-visual 1 t)))
             do (cl-incf count)
             finally return count)))

(defun ein:markdown-get-point-back-lines (pt num-lines)
  (save-excursion
    (goto-char pt)
    (line-move-visual (- num-lines) t)
    ;; in testing, can occasionally overshoot the number of lines to traverse
    (let ((actual-num-lines (ein:markdown-visual-lines-between-points (point) pt)))
      (when (> actual-num-lines num-lines)
        (line-move-visual (- actual-num-lines num-lines) t)))
    (point)))

(defun ein:markdown-get-other-window ()
  "Find another window to display preview or output content."
  (cond
   ((memq ein:markdown-split-window-direction '(vertical below))
    (or (window-in-direction 'below) (split-window-vertically)))
   ((memq ein:markdown-split-window-direction '(horizontal right))
    (or (window-in-direction 'right) (split-window-horizontally)))
   (t (split-window-sensibly (get-buffer-window)))))

(defun ein:markdown-display-buffer-other-window (buf)
  "Display preview or output buffer BUF in another window."
  (let ((cur-buf (current-buffer))
        (window (ein:markdown-get-other-window)))
    (set-window-buffer window buf)
    (set-buffer cur-buf)))

(defun ein:markdown-open ()
  "Open file for the current buffer with `markdown-open-command'."
  (interactive)
  (unless ein:markdown-open-command
    (user-error "Variable `markdown-open-command' must be set"))
  (if (stringp ein:markdown-open-command)
      (if (not buffer-file-name)
          (user-error "Must be visiting a file")
        (save-buffer)
        (let ((exit-code (call-process ein:markdown-open-command nil nil nil
                                       buffer-file-name)))
          ;; The exit code can be a signal description string, so don’t use ‘=’
          ;; or ‘zerop’.
          (unless (eq exit-code 0)
            (user-error "%s failed with exit code %s"
                        ein:markdown-open-command exit-code))))
    (funcall ein:markdown-open-command))
  nil)

(defun ein:markdown-kill-ring-save ()
  "Run ein:markdown on file and store output in the kill ring."
  (interactive)
  (save-window-excursion
    (ein:markdown)
    (with-current-buffer ein:markdown-output-buffer-name
      (kill-ring-save (point-min) (point-max)))))


;;; Links =====================================================================

(defun ein:markdown-link-p ()
  "Return non-nil when `point' is at a non-wiki link.
See `markdown-wiki-link-p' for more information."
  (let ((case-fold-search nil))
    (and (not (ein:markdown-code-block-at-point-p))
         (or (thing-at-point-looking-at ein:markdown-regex-link-inline)
             (thing-at-point-looking-at ein:markdown-regex-link-reference)
             (thing-at-point-looking-at ein:markdown-regex-uri)
             (thing-at-point-looking-at ein:markdown-regex-angle-uri)))))

(make-obsolete 'ein:markdown-link-link 'ein:markdown-link-url "v2.3")

(defun ein:markdown-link-at-pos (pos)
  "Return properties of link or image at position POS.
Value is a list of elements describing the link:
 0. beginning position
 1. end position
 2. link text
 3. URL
 4. reference label
 5. title text
 6. bang (nil or \"!\")"
  (save-excursion
    (goto-char pos)
    (let (begin end text url reference title bang)
      (cond
       ;; Inline or reference image or link at point.
       ((or (thing-at-point-looking-at ein:markdown-regex-link-inline)
            (thing-at-point-looking-at ein:markdown-regex-link-reference))
        (setq bang (match-string-no-properties 1)
              begin (match-beginning 0)
              end (match-end 0)
              text (match-string-no-properties 3))
        (if (char-equal (char-after (match-beginning 5)) ?\[)
            ;; Reference link
            (setq reference (match-string-no-properties 6))
          ;; Inline link
          (setq url (match-string-no-properties 6))
          (when (match-end 7)
            (setq title (substring (match-string-no-properties 7) 1 -1)))))
       ;; Angle bracket URI at point.
       ((thing-at-point-looking-at ein:markdown-regex-angle-uri)
        (setq begin (match-beginning 0)
              end (match-end 0)
              url (match-string-no-properties 2)))
       ;; Plain URI at point.
       ((thing-at-point-looking-at ein:markdown-regex-uri)
        (setq begin (match-beginning 0)
              end (match-end 0)
              url (match-string-no-properties 1))))
      (list begin end text url reference title bang))))

(defun ein:markdown-link-url ()
  "Return the URL part of the regular (non-wiki) link at point.
Works with both inline and reference style links, and with images.
If point is not at a link or the link reference is not defined
returns nil."
  (let* ((values (ein:markdown-link-at-pos (point)))
         (text (nth 2 values))
         (url (nth 3 values))
         (ref (nth 4 values)))
    (or url (and ref (car (ein:markdown-reference-definition
                           (downcase (if (string= ref "") text ref))))))))

(defun ein:markdown-follow-link-at-point ()
  "Open the current non-wiki link.
If the link is a complete URL, open in browser with `browse-url'.
Otherwise, open with `find-file' after stripping anchor and/or query string.
Translate filenames using `markdown-filename-translate-function'."
  (interactive)
  (if (ein:markdown-link-p)
      (let* ((url (ein:markdown-link-url))
             (struct (url-generic-parse-url url))
             (full (url-fullness struct))
             (file url))
        ;; Parse URL, determine fullness, strip query string
        (if (fboundp 'url-path-and-query)
            (setq file (car (url-path-and-query struct)))
          (when (and (setq file (url-filename struct))
                     (string-match "\\?" file))
            (setq file (substring file 0 (match-beginning 0)))))
        ;; Open full URLs in browser, files in Emacs
        (if full
            (browse-url url)
          (when (and file (> (length file) 0))
            (find-file (funcall ein:markdown-translate-filename-function file)))))
    (user-error "Point is not at a ein:markdown link or URL")))

(defun ein:markdown-fontify-inline-links (last)
  "Add text properties to next inline link from point to LAST."
  (when (ein:markdown-match-generic-links last nil)
    (let* ((link-start (match-beginning 3))
           (link-end (match-end 3))
           (url-start (match-beginning 6))
           (url-end (match-end 6))
           (url (match-string-no-properties 6))
           (title-start (match-beginning 7))
           (title-end (match-end 7))
           (title (match-string-no-properties 7))
           ;; Markup part
           (mp (list 'face 'ein:markdown-markup-face
                     'rear-nonsticky t
                     'font-lock-multiline t))
           ;; Link part (without face)
           (lp (list 'keymap ein:markdown-mode-mouse-map
                     'mouse-face 'ein:markdown-highlight-face
                     'font-lock-multiline t
                     'help-echo (if title (concat title "\n" url) url)))
           ;; URL part
           (up (list 'keymap ein:markdown-mode-mouse-map
                     'face 'ein:markdown-url-face
                     'mouse-face 'ein:markdown-highlight-face
                     'font-lock-multiline t))
           ;; Title part
           (tp (list 'face 'ein:markdown-link-title-face
                     'font-lock-multiline t)))
      (dolist (g '(1 2 4 5 8))
        (when (match-end g)
          (add-text-properties (match-beginning g) (match-end g) mp)))
      ;; Preserve existing faces applied to link part (e.g., inline code)
      (when link-start
        (add-text-properties link-start link-end lp)
        (add-face-text-property link-start link-end
                                'ein:markdown-link-face 'append))
      (when url-start (add-text-properties url-start url-end up))
      (when title-start (add-text-properties url-end title-end tp))
      t)))

(defun ein:markdown-fontify-reference-links (last)
  "Add text properties to next reference link from point to LAST."
  (when (ein:markdown-match-generic-links last t)
    (let* ((link-start (match-beginning 3))
           (link-end (match-end 3))
           (ref-start (match-beginning 6))
           (ref-end (match-end 6))
           ;; Markup part
           (mp (list 'face 'ein:markdown-markup-face
                     'rear-nonsticky t
                     'font-lock-multiline t))
           ;; Link part
           (lp (list 'keymap ein:markdown-mode-mouse-map
                     'face 'ein:markdown-link-face
                     'mouse-face 'ein:markdown-highlight-face
                     'font-lock-multiline t
                     'help-echo (lambda (_ __ pos)
                                  (save-match-data
                                    (save-excursion
                                      (goto-char pos)
                                      (or (ein:markdown-link-url)
                                          "Undefined reference"))))))
           ;; Reference part
           (rp (list 'face 'ein:markdown-reference-face
                     'font-lock-multiline t)))
      (dolist (g '(1 2 4 5 8))
        (when (match-end g)
          (add-text-properties (match-beginning g) (match-end g) mp)))
      (when link-start (add-text-properties link-start link-end lp))
      (when ref-start (add-text-properties ref-start ref-end rp))
      t)))

(defun ein:markdown-fontify-angle-uris (last)
  "Add text properties to angle URIs from point to LAST."
  (when (ein:markdown-match-angle-uris last)
    (let* ((url-start (match-beginning 2))
           (url-end (match-end 2))
           ;; Markup part
           (mp (list 'face 'ein:markdown-markup-face
                     'rear-nonsticky t
                     'font-lock-multiline t))
           ;; URI part
           (up (list 'keymap ein:markdown-mode-mouse-map
                     'face 'ein:markdown-plain-url-face
                     'mouse-face 'ein:markdown-highlight-face
                     'font-lock-multiline t)))
      (dolist (g '(1 3))
        (add-text-properties (match-beginning g) (match-end g) mp))
      (add-text-properties url-start url-end up)
      t)))

(defun ein:markdown-fontify-plain-uris (last)
  "Add text properties to plain URLs from point to LAST."
  (when (ein:markdown-match-plain-uris last)
    (let* ((start (match-beginning 0))
           (end (match-end 0))
           (props (list 'keymap ein:markdown-mode-mouse-map
                        'face 'ein:markdown-plain-url-face
                        'mouse-face 'ein:markdown-highlight-face
                        'rear-nonsticky t
                        'font-lock-multiline t)))
      (add-text-properties start end props)
      t)))

;;; Following & Doing =========================================================

(defun ein:markdown-follow-thing-at-point (_arg)
  "Follow thing at point if possible, such as a reference link or wiki link.
Opens inline and reference links in a browser.  Opens wiki links
to other files in the current window, or the another window if
ARG is non-nil.
See `markdown-follow-link-at-point' and
`markdown-follow-wiki-link-at-point'."
  (interactive "P")
  (cond ((ein:markdown-link-p)
         (ein:markdown-follow-link-at-point))
        (t
         (user-error "Nothing to follow at point"))))

(make-obsolete 'ein:markdown-jump 'ein:markdown-do "v2.3")

(defun ein:markdown-do ()
  "Do something sensible based on context at point.
Jumps between reference links and definitions; between footnote
markers and footnote text."
  (interactive)
  (cond
   ;; Footnote definition
   ((ein:markdown-footnote-text-positions)
    (ein:markdown-footnote-return))
   ;; Footnote marker
   ((ein:markdown-footnote-marker-positions)
    (ein:markdown-footnote-goto-text))
   ;; Reference link
   ((thing-at-point-looking-at ein:markdown-regex-link-reference)
    (ein:markdown-reference-goto-definition))
   ;; Reference definition
   ((thing-at-point-looking-at ein:markdown-regex-reference-definition)
    (ein:markdown-reference-goto-link (match-string-no-properties 2)))
   ;; Align table
   ((ein:markdown-table-at-point-p)
    (call-interactively #'ein:markdown-table-align))
   ;; Otherwise
   (t
    (error "ein:markdown-do: don't know what to do"))))


;;; Miscellaneous =============================================================

(defun ein:markdown-compress-whitespace-string (str)
  "Compress whitespace in STR and return result.
Leading and trailing whitespace is removed.  Sequences of multiple
spaces, tabs, and newlines are replaced with single spaces."
  (ein:markdown-replace-regexp-in-string "\\(^[ \t\n]+\\|[ \t\n]+$\\)" ""
                            (ein:markdown-replace-regexp-in-string "[ \t\n]+" " " str)))

(defun ein:markdown--substitute-command-keys (string)
  "Like `substitute-command-keys' but, but prefers control characters.
First pass STRING to `substitute-command-keys' and then
substitute `C-i` for `TAB` and `C-m` for `RET`."
  (replace-regexp-in-string
   "\\<TAB\\>" "C-i"
   (replace-regexp-in-string
    "\\<RET\\>" "C-m" (substitute-command-keys string) t) t))

(defun ein:markdown-line-number-at-pos (&optional pos)
  "Return (narrowed) buffer line number at position POS.
If POS is nil, use current buffer location.
This is an exact copy of `line-number-at-pos' for use in emacs21."
  (let ((opoint (or pos (point))) start)
    (save-excursion
      (goto-char (point-min))
      (setq start (point))
      (goto-char opoint)
      (forward-line 0)
      (1+ (count-lines start (point))))))

(defun ein:markdown-inside-link-p ()
  "Return t if point is within a link."
  (save-match-data
    (thing-at-point-looking-at (ein:markdown-make-regex-link-generic))))

(defun ein:markdown-line-is-reference-definition-p ()
  "Return whether the current line is a (non-footnote) reference defition."
  (save-excursion
    (move-beginning-of-line 1)
    (and (looking-at-p ein:markdown-regex-reference-definition)
         (not (looking-at-p "[ \t]*\\[^")))))

(defun ein:markdown-adaptive-fill-function ()
  "Return prefix for filling paragraph or nil if not determined."
  (cond
   ;; List item inside blockquote
   ((looking-at "^[ \t]*>[ \t]*\\(\\(?:[0-9]+\\|#\\)\\.\\|[*+:-]\\)[ \t]+")
    (ein:markdown-replace-regexp-in-string
     "[0-9\\.*+-]" " " (match-string-no-properties 0)))
   ;; Blockquote
   ((looking-at ein:markdown-regex-blockquote)
    (buffer-substring-no-properties (match-beginning 0) (match-end 2)))
   ;; List items
   ((looking-at ein:markdown-regex-list)
    (match-string-no-properties 0))
   ;; Footnote definition
   ((looking-at-p ein:markdown-regex-footnote-definition)
    "    ") ; four spaces
   ;; No match
   (t nil)))

(defun ein:markdown-fill-paragraph (&optional justify)
  "Fill paragraph at or after point.
This function is like \\[fill-paragraph], but it skips ein:markdown
code blocks.  If the point is in a code block, or just before one,
do not fill.  Otherwise, call `fill-paragraph' as usual. If
JUSTIFY is non-nil, justify text as well.  Since this function
handles filling itself, it always returns t so that
`fill-paragraph' doesn't run."
  (interactive "P")
  (unless (or (ein:markdown-code-block-at-point-p)
              (save-excursion
                (back-to-indentation)
                (skip-syntax-forward "-")
                (ein:markdown-code-block-at-point-p)))
    (fill-paragraph justify))
  t)

(make-obsolete 'ein:markdown-fill-forward-paragraph-function
               'ein:markdown-fill-forward-paragraph "v2.3")

(defun ein:markdown-fill-forward-paragraph (&optional arg)
  "Function used by `fill-paragraph' to move over ARG paragraphs.
This is a `fill-forward-paragraph-function' for `ein:markdown-mode'.
It is called with a single argument specifying the number of
paragraphs to move.  Just like `forward-paragraph', it should
return the number of paragraphs left to move."
  (or arg (setq arg 1))
  (if (> arg 0)
      ;; With positive ARG, move across ARG non-code-block paragraphs,
      ;; one at a time.  When passing a code block, don't decrement ARG.
      (while (and (not (eobp))
                  (> arg 0)
                  (= (forward-paragraph 1) 0)
                  (or (ein:markdown-code-block-at-pos (line-beginning-position 0))
                      (setq arg (1- arg)))))
    ;; Move backward by one paragraph with negative ARG (always -1).
    (let ((start (point)))
      (setq arg (forward-paragraph arg))
      (while (and (not (eobp))
                  (progn (move-to-left-margin) (not (eobp)))
                  (looking-at-p paragraph-separate))
        (forward-line 1))
      (cond
       ;; Move point past whitespace following list marker.
       ((looking-at ein:markdown-regex-list)
        (goto-char (match-end 0)))
       ;; Move point past whitespace following pipe at beginning of line
       ;; to handle Pandoc line blocks.
       ((looking-at "^|\\s-*")
        (goto-char (match-end 0)))
       ;; Return point if the paragraph passed was a code block.
       ((ein:markdown-code-block-at-pos (line-beginning-position 2))
        (goto-char start)))))
  arg)

(defun ein:markdown--inhibit-electric-quote ()
  "Function added to `electric-quote-inhibit-functions'.
Return non-nil if the quote has been inserted inside a code block
or span."
  (let ((pos (1- (point))))
    (or (ein:markdown-inline-code-at-pos pos)
        (ein:markdown-code-block-at-pos pos))))


;;; Extension Framework =======================================================

(defun ein:markdown-reload-extensions ()
  "Check settings, update font-lock keywords and hooks, and re-fontify buffer."
  (interactive)
  (when (member major-mode '(ein:markdown-mode))
    ;; Refontify buffer
    (if (eval-when-compile (fboundp 'font-lock-flush))
        ;; Use font-lock-flush in Emacs >= 25.1
        (font-lock-flush)
      ;; Backwards compatibility for Emacs 24.3-24.5
      (when (and font-lock-mode (fboundp 'font-lock-refresh-defaults))
        (font-lock-refresh-defaults)))
    ))

(defun ein:markdown-handle-local-variables ()
  "Run in `hack-local-variables-hook' to update font lock rules.
Checks to see if there is actually a ‘ein:markdown-mode’ file local variable
before regenerating font-lock rules for extensions."
  (when (and (boundp 'file-local-variables-alist)
             (assoc 'ein:markdown-enable-math file-local-variables-alist))
    (when (assoc 'ein:markdown-enable-math file-local-variables-alist)
      (ein:markdown-toggle-math ein:markdown-enable-math))
    (ein:markdown-reload-extensions)))


;;; Math Support ==============================================================

(make-obsolete 'ein:markdown-enable-math 'ein:markdown-toggle-math "v2.1")

(defconst ein:markdown-mode-font-lock-keywords-math
  (list
   ;; Equation reference (eq:foo)
   '("\\((eq:\\)\\([[:alnum:]:_]+\\)\\()\\)" . ((1 ein:markdown-markup-face)
                                                (2 ein:markdown-reference-face)
                                                (3 ein:markdown-markup-face)))
   ;; Equation reference \eqref{foo}
   '("\\(\\\\eqref{\\)\\([[:alnum:]:_]+\\)\\(}\\)" . ((1 ein:markdown-markup-face)
                                                      (2 ein:markdown-reference-face)
                                                      (3 ein:markdown-markup-face))))
  "Font lock keywords to add and remove when toggling math support.")

(defun ein:markdown-toggle-math (&optional arg)
  "Toggle support for inline and display LaTeX math expressions.
With a prefix argument ARG, enable math mode if ARG is positive,
and disable it otherwise.  If called from Lisp, enable the mode
if ARG is omitted or nil."
  (interactive (list (or current-prefix-arg 'toggle)))
  (setq ein:markdown-enable-math
        (if (eq arg 'toggle)
            (not ein:markdown-enable-math)
          (> (prefix-numeric-value arg) 0)))
  (if ein:markdown-enable-math
      (progn
        (font-lock-add-keywords
         'ein:markdown-mode ein:markdown-mode-font-lock-keywords-math)
        (message "ein:markdown-mode math support enabled"))
    (font-lock-remove-keywords
     'ein:markdown-mode ein:markdown-mode-font-lock-keywords-math)
    (message "ein:markdown-mode math support disabled"))
  (ein:markdown-reload-extensions))

;;; Display inline image ======================================================

(defcustom ein:markdown-fontify-code-block-default-mode nil
  "Default mode to use to fontify code blocks.
This mode is used when automatic detection fails, such as for GFM
code blocks with no language specified."
  :group 'ein:markdown
  :type '(choice function (const :tag "None" nil))
  :package-version '(ein:markdown-mode . "2.4"))

;; This is based on `org-src-lang-modes' from org-src.el
(defcustom ein:markdown-code-lang-modes
  '(("ocaml" . tuareg-mode) ("elisp" . emacs-lisp-mode) ("ditaa" . artist-mode)
    ("asymptote" . asy-mode) ("dot" . fundamental-mode) ("sqlite" . sql-mode)
    ("calc" . fundamental-mode) ("C" . c-mode) ("cpp" . c++-mode)
    ("C++" . c++-mode) ("screen" . shell-script-mode) ("shell" . sh-mode)
    ("bash" . sh-mode))
  "Alist mapping languages to their major mode.
The key is the language name, the value is the major mode.  For
many languages this is simple, but for language where this is not
the case, this variable provides a way to simplify things on the
user side.  For example, there is no ocaml-mode in Emacs, but the
mode to use is `tuareg-mode'."
  :group 'ein:markdown
  :type '(repeat
          (cons
           (string "Language name")
           (symbol "Major mode")))
  :package-version '(ein:markdown-mode . "2.3"))

(defun ein:markdown-get-lang-mode (lang)
  "Return major mode that should be used for LANG.
LANG is a string, and the returned major mode is a symbol."
  (cl-find-if
   'fboundp
   (list (cdr (assoc lang ein:markdown-code-lang-modes))
         (cdr (assoc (downcase lang) ein:markdown-code-lang-modes))
         (intern (concat lang "-mode"))
         (intern (concat (downcase lang) "-mode")))))

(defun ein:markdown-fontify-code-blocks-generic (matcher last)
  "Add text properties to next code block from point to LAST.
Use matching function MATCHER."
  (when (funcall matcher last)
    (save-excursion
      (save-match-data
        (let* ((start (match-beginning 0))
               (end (match-end 0))
               ;; Find positions outside opening and closing backquotes.
               (bol-prev (progn (goto-char start)
                                (if (bolp) (line-beginning-position 0) (line-beginning-position))))
               (eol-next (progn (goto-char end)
                                (if (bolp) (line-beginning-position 2) (line-beginning-position 3)))))
          (add-text-properties start end '(face ein:markdown-pre-face))
          ;; Set background for block as well as opening and closing lines.
          (font-lock-append-text-property
           bol-prev eol-next 'face 'ein:markdown-code-face))))
    t))

(defun ein:markdown-fontify-fenced-code-blocks (last)
  "Add text properties to next tilde fenced code block from point to LAST."
  (ein:markdown-fontify-code-blocks-generic 'ein:markdown-match-fenced-code-blocks last))

;;; Table Editing =============================================================

;; These functions were originally adapted from `org-table.el'.

;; General helper functions

(defmacro ein:markdown--with-gensyms (symbols &rest body)
  (declare (debug (sexp body)) (indent 1))
  `(let ,(mapcar (lambda (s)
                   `(,s (make-symbol (concat "--" (symbol-name ',s)))))
                 symbols)
     ,@body))

(defun ein:markdown--split-string (string &optional separators)
  "Splits STRING into substrings at SEPARATORS.
SEPARATORS is a regular expression. If nil it defaults to
`split-string-default-separators'. This version returns no empty
strings if there are matches at the beginning and end of string."
  (let ((start 0) notfirst list)
    (while (and (string-match
                 (or separators split-string-default-separators)
                 string
                 (if (and notfirst
                          (= start (match-beginning 0))
                          (< start (length string)))
                     (1+ start) start))
                (< (match-beginning 0) (length string)))
      (setq notfirst t)
      (or (eq (match-beginning 0) 0)
          (and (eq (match-beginning 0) (match-end 0))
               (eq (match-beginning 0) start))
          (push (substring string start (match-beginning 0)) list))
      (setq start (match-end 0)))
    (or (eq start (length string))
        (push (substring string start) list))
    (nreverse list)))

(defun ein:markdown--string-width (s)
  "Return width of string S.
This version ignores characters with invisibility property
`markdown-markup'."
  (let (b)
    (when (or (eq t buffer-invisibility-spec)
              (member 'ein:markdown-markup buffer-invisibility-spec))
      (while (setq b (text-property-any
                      0 (length s)
                      'invisible 'ein:markdown-markup s))
        (setq s (concat
                 (substring s 0 b)
                 (substring s (or (next-single-property-change
                                   b 'invisible s)
                                  (length s))))))))
  (string-width s))

(defun ein:markdown--remove-invisible-markup (s)
  "Remove ein:markdown markup from string S.
This version removes characters with invisibility property
`markdown-markup'."
  (let (b)
    (while (setq b (text-property-any
                    0 (length s)
                    'invisible 'ein:markdown-markup s))
      (setq s (concat
               (substring s 0 b)
               (substring s (or (next-single-property-change
                                 b 'invisible s)
                                (length s)))))))
  s)

;; Functions for maintaining tables

(defvar ein:markdown-table-at-point-p-function nil
  "Function to decide if point is inside a table.

The indirection serves to differentiate between standard ein:markdown
tables and gfm tables which are less strict about the markup.")

(defconst ein:markdown-table-line-regexp "^[ \t]*|"
  "Regexp matching any line inside a table.")

(defconst ein:markdown-table-hline-regexp "^[ \t]*|[-:]"
  "Regexp matching hline inside a table.")

(defconst ein:markdown-table-dline-regexp "^[ \t]*|[^-:]"
  "Regexp matching dline inside a table.")

(defun ein:markdown-table-at-point-p ()
  "Return non-nil when point is inside a table."
  (if (functionp ein:markdown-table-at-point-p-function)
      (funcall ein:markdown-table-at-point-p-function)
    (ein:markdown--table-at-point-p)))

(defun ein:markdown--table-at-point-p ()
  "Return non-nil when point is inside a table."
  (save-excursion
    (beginning-of-line)
    (and (looking-at-p ein:markdown-table-line-regexp)
         (not (ein:markdown-code-block-at-point-p)))))

(defun ein:markdown-table-hline-at-point-p ()
  "Return non-nil when point is on a hline in a table.
This function assumes point is on a table."
  (save-excursion
    (beginning-of-line)
    (looking-at-p ein:markdown-table-hline-regexp)))

(defun ein:markdown-table-begin ()
  "Find the beginning of the table and return its position.
This function assumes point is on a table."
  (save-excursion
    (while (and (not (bobp))
                (ein:markdown-table-at-point-p))
      (forward-line -1))
    (unless (or (eobp)
                (ein:markdown-table-at-point-p))
      (forward-line 1))
    (point)))

(defun ein:markdown-table-end ()
  "Find the end of the table and return its position.
This function assumes point is on a table."
  (save-excursion
    (while (and (not (eobp))
                (ein:markdown-table-at-point-p))
      (forward-line 1))
    (point)))

(defun ein:markdown-table-get-dline ()
  "Return index of the table data line at point.
This function assumes point is on a table."
  (let ((pos (point)) (end (ein:markdown-table-end)) (cnt 0))
    (save-excursion
      (goto-char (ein:markdown-table-begin))
      (while (and (re-search-forward
                   ein:markdown-table-dline-regexp end t)
                  (setq cnt (1+ cnt))
                  (< (line-end-position) pos))))
    cnt))

(defun ein:markdown-table-get-column ()
  "Return table column at point.
This function assumes point is on a table."
  (let ((pos (point)) (cnt 0))
    (save-excursion
      (beginning-of-line)
      (while (search-forward "|" pos t) (setq cnt (1+ cnt))))
    cnt))

(defun ein:markdown-table-get-cell (&optional n)
  "Return the content of the cell in column N of current row.
N defaults to column at point. This function assumes point is on
a table."
  (and n (ein:markdown-table-goto-column n))
  (skip-chars-backward "^|\n") (backward-char 1)
  (if (looking-at "|[^|\r\n]*")
      (let* ((pos (match-beginning 0))
             (val (buffer-substring (1+ pos) (match-end 0))))
        (goto-char (min (line-end-position) (+ 2 pos)))
        ;; Trim whitespaces
        (setq val (replace-regexp-in-string "\\`[ \t]+" "" val)
              val (replace-regexp-in-string "[ \t]+\\'" "" val)))
    (forward-char 1) ""))

(defun ein:markdown-table-goto-dline (n)
  "Go to the Nth data line in the table at point.
Return t when the line exists, nil otherwise. This function
assumes point is on a table."
  (goto-char (ein:markdown-table-begin))
  (let ((end (ein:markdown-table-end)) (cnt 0))
    (while (and (re-search-forward
                 ein:markdown-table-dline-regexp end t)
                (< (setq cnt (1+ cnt)) n)))
    (= cnt n)))

(defun ein:markdown-table-goto-column (n &optional on-delim)
  "Go to the Nth column in the table line at point.
With optional argument ON-DELIM, stop with point before the left
delimiter of the cell. If there are less than N cells, just go
beyond the last delimiter. This function assumes point is on a
table."
  (beginning-of-line 1)
  (when (> n 0)
    (while (and (> (setq n (1- n)) -1)
                (search-forward "|" (line-end-position) t)))
    (if on-delim
        (backward-char 1)
      (when (looking-at " ") (forward-char 1)))))

(defmacro ein:markdown-table-save-cell (&rest body)
  "Save cell at point, execute BODY and restore cell.
This function assumes point is on a table."
  (declare (debug (body)))
  (ein:markdown--with-gensyms (line column)
    `(let ((,line (copy-marker (line-beginning-position)))
           (,column (ein:markdown-table-get-column)))
       (unwind-protect
           (progn ,@body)
         (goto-char ,line)
         (ein:markdown-table-goto-column ,column)
         (set-marker ,line nil)))))

(defun ein:markdown-table-blank-line (s)
  "Convert a table line S into a line with blank cells."
  (if (string-match "^[ \t]*|-" s)
      (setq s (mapconcat
               (lambda (x) (if (member x '(?| ?+)) "|" " "))
               s ""))
    (while (string-match "|\\([ \t]*?[^ \t\r\n|][^\r\n|]*\\)|" s)
      (setq s (replace-match
               (concat "|" (make-string (length (match-string 1 s)) ?\ ) "|")
               t t s)))
    s))

(defun ein:markdown-table-colfmt (fmtspec)
  "Process column alignment specifier FMTSPEC for tables."
  (when (stringp fmtspec)
    (mapcar (lambda (x)
              (cond ((string-match-p "^:.*:$" x) 'c)
                    ((string-match-p "^:"     x) 'l)
                    ((string-match-p ":$"     x) 'r)
                    (t 'd)))
            (ein:markdown--split-string fmtspec "\\s-*|\\s-*"))))

(defun ein:markdown-table-align ()
  "Align table at point.
This function assumes point is on a table."
  (interactive)
  (let ((begin (ein:markdown-table-begin))
        (end (copy-marker (ein:markdown-table-end))))
    (ein:markdown-table-save-cell
     (goto-char begin)
     (let* (fmtspec
            ;; Store table indent
            (indent (progn (looking-at "[ \t]*") (match-string 0)))
            ;; Split table in lines and save column format specifier
            (lines (mapcar (lambda (l)
                             (if (string-match-p "\\`[ \t]*|[-:]" l)
                                 (progn (setq fmtspec (or fmtspec l)) nil) l))
                           (ein:markdown--split-string (buffer-substring begin end) "\n")))
            ;; Split lines in cells
            (cells (mapcar (lambda (l) (ein:markdown--split-string l "\\s-*|\\s-*"))
                           (remq nil lines)))
            ;; Calculate maximum number of cells in a line
            (maxcells (if cells
                          (apply #'max (mapcar #'length cells))
                        (user-error "Empty table")))
            ;; Empty cells to fill short lines
            (emptycells (make-list maxcells "")) maxwidths)
       ;; Calculate maximum width for each column
       (dotimes (i maxcells)
         (let ((column (mapcar (lambda (x) (or (nth i x) "")) cells)))
           (push (apply #'max 1 (mapcar #'ein:markdown--string-width column))
                 maxwidths)))
       (setq maxwidths (nreverse maxwidths))
       ;; Process column format specifier
       (setq fmtspec (ein:markdown-table-colfmt fmtspec))
       ;; Compute formats needed for output of table lines
       (let ((hfmt (concat indent "|"))
             (rfmt (concat indent "|"))
             hfmt1 rfmt1 fmt)
         (dolist (width maxwidths (setq hfmt (concat (substring hfmt 0 -1) "|")))
           (setq fmt (pop fmtspec))
           (cond ((equal fmt 'l) (setq hfmt1 ":%s-|" rfmt1 " %%-%ds |"))
                 ((equal fmt 'r) (setq hfmt1 "-%s:|" rfmt1  " %%%ds |"))
                 ((equal fmt 'c) (setq hfmt1 ":%s:|" rfmt1 " %%-%ds |"))
                 (t              (setq hfmt1 "-%s-|" rfmt1 " %%-%ds |")))
           (setq rfmt (concat rfmt (format rfmt1 width)))
           (setq hfmt (concat hfmt (format hfmt1 (make-string width ?-)))))
         ;; Replace modified lines only
         (dolist (line lines)
           (let ((line (if line
                           (apply #'format rfmt (append (pop cells) emptycells))
                         hfmt))
                 (previous (buffer-substring (point) (line-end-position))))
             (if (equal previous line)
                 (forward-line)
               (insert line "\n")
               (delete-region (point) (line-beginning-position 2))))))
       (set-marker end nil)))))

(defun ein:markdown-table-insert-row (&optional arg)
  "Insert a new row above the row at point into the table.
With optional argument ARG, insert below the current row."
  (interactive "P")
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (let* ((line (buffer-substring
                (line-beginning-position) (line-end-position)))
         (new (ein:markdown-table-blank-line line)))
    (beginning-of-line (if arg 2 1))
    (unless (bolp) (insert "\n"))
    (insert-before-markers new "\n")
    (beginning-of-line 0)
    (re-search-forward "| ?" (line-end-position) t)))

(defun ein:markdown-table-delete-row ()
  "Delete row or horizontal line at point from the table."
  (interactive)
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (let ((col (current-column)))
    (kill-region (line-beginning-position)
                 (min (1+ (line-end-position)) (point-max)))
    (unless (ein:markdown-table-at-point-p) (beginning-of-line 0))
    (move-to-column col)))

(defun ein:markdown-table-move-row (&optional up)
  "Move table line at point down.
With optional argument UP, move it up."
  (interactive "P")
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (let* ((col (current-column)) (pos (point))
         (tonew (if up 0 2)) txt)
    (beginning-of-line tonew)
    (unless (ein:markdown-table-at-point-p)
      (goto-char pos) (user-error "Cannot move row further"))
    (goto-char pos) (beginning-of-line 1) (setq pos (point))
    (setq txt (buffer-substring (point) (1+ (line-end-position))))
    (delete-region (point) (1+ (line-end-position)))
    (beginning-of-line tonew)
    (insert txt) (beginning-of-line 0)
    (move-to-column col)))

(defun ein:markdown-table-move-row-up ()
  "Move table row at point up."
  (interactive)
  (ein:markdown-table-move-row 'up))

(defun ein:markdown-table-move-row-down ()
  "Move table row at point down."
  (interactive)
  (ein:markdown-table-move-row nil))

(defun ein:markdown-table-insert-column ()
  "Insert a new table column."
  (interactive)
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (let* ((col (max 1 (ein:markdown-table-get-column)))
         (begin (ein:markdown-table-begin))
         (end (copy-marker (ein:markdown-table-end))))
    (ein:markdown-table-save-cell
     (goto-char begin)
     (while (< (point) end)
       (ein:markdown-table-goto-column col t)
       (if (ein:markdown-table-hline-at-point-p)
           (insert "|---")
         (insert "|   "))
       (forward-line)))
    (set-marker end nil)
    (ein:markdown-table-align)))

(defun ein:markdown-table-delete-column ()
  "Delete column at point from table."
  (interactive)
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (let ((col (ein:markdown-table-get-column))
        (begin (ein:markdown-table-begin))
        (end (copy-marker (ein:markdown-table-end))))
    (ein:markdown-table-save-cell
     (goto-char begin)
     (while (< (point) end)
       (ein:markdown-table-goto-column col t)
       (and (looking-at "|[^|\n]+|")
            (replace-match "|"))
       (forward-line)))
    (set-marker end nil)
    (ein:markdown-table-goto-column (max 1 (1- col)))
    (ein:markdown-table-align)))

(defun ein:markdown-table-move-column (&optional left)
  "Move table column at point to the right.
With optional argument LEFT, move it to the left."
  (interactive "P")
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (let* ((col (ein:markdown-table-get-column))
         (col1 (if left (1- col) col))
         (colpos (if left (1- col) (1+ col)))
         (begin (ein:markdown-table-begin))
         (end (copy-marker (ein:markdown-table-end))))
    (when (and left (= col 1))
      (user-error "Cannot move column further left"))
    (when (and (not left) (looking-at "[^|\n]*|[^|\n]*$"))
      (user-error "Cannot move column further right"))
    (ein:markdown-table-save-cell
     (goto-char begin)
     (while (< (point) end)
       (ein:markdown-table-goto-column col1 t)
       (when (looking-at "|\\([^|\n]+\\)|\\([^|\n]+\\)|")
         (replace-match "|\\2|\\1|"))
       (forward-line)))
    (set-marker end nil)
    (ein:markdown-table-goto-column colpos)
    (ein:markdown-table-align)))

(defun ein:markdown-table-move-column-left ()
  "Move table column at point to the left."
  (interactive)
  (ein:markdown-table-move-column 'left))

(defun ein:markdown-table-move-column-right ()
  "Move table column at point to the right."
  (interactive)
  (ein:markdown-table-move-column nil))

(defun ein:markdown-table-next-row ()
  "Go to the next row (same column) in the table.
Create new table lines if required."
  (interactive)
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (if (or (looking-at "[ \t]*$")
          (save-excursion (skip-chars-backward " \t") (bolp)))
      (newline)
    (ein:markdown-table-align)
    (let ((col (ein:markdown-table-get-column)))
      (beginning-of-line 2)
      (if (or (not (ein:markdown-table-at-point-p))
              (ein:markdown-table-hline-at-point-p))
          (progn
            (beginning-of-line 0)
            (ein:markdown-table-insert-row 'below)))
      (ein:markdown-table-goto-column col)
      (skip-chars-backward "^|\n\r")
      (when (looking-at " ") (forward-char 1)))))

(defun ein:markdown-table-forward-cell ()
  "Go to the next cell in the table.
Create new table lines if required."
  (interactive)
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (ein:markdown-table-align)
  (let ((end (ein:markdown-table-end)))
    (when (ein:markdown-table-hline-at-point-p) (end-of-line 1))
    (condition-case nil
        (progn
          (re-search-forward "|" end)
          (if (looking-at "[ \t]*$")
              (re-search-forward "|" end))
          (if (and (looking-at "[-:]")
                   (re-search-forward "^[ \t]*|\\([^-:]\\)" end t))
              (goto-char (match-beginning 1)))
          (if (looking-at "[-:]")
              (progn
                (beginning-of-line 0)
                (ein:markdown-table-insert-row 'below))
            (when (looking-at " ") (forward-char 1))))
      (error (ein:markdown-table-insert-row 'below)))))

(defun ein:markdown-table-backward-cell ()
  "Go to the previous cell in the table."
  (interactive)
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (ein:markdown-table-align)
  (when (ein:markdown-table-hline-at-point-p) (end-of-line 1))
  (condition-case nil
      (progn
        (re-search-backward "|" (ein:markdown-table-begin))
        (re-search-backward "|" (ein:markdown-table-begin)))
    (error (user-error "Cannot move to previous table cell")))
  (while (looking-at "|\\([-:]\\|[ \t]*$\\)")
    (re-search-backward "|" (ein:markdown-table-begin)))
  (when (looking-at "| ?") (goto-char (match-end 0))))

(defun ein:markdown-table-transpose ()
  "Transpose table at point.
Horizontal separator lines will be eliminated."
  (interactive)
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  (let* ((table (buffer-substring-no-properties
                 (ein:markdown-table-begin) (ein:markdown-table-end)))
         ;; Convert table to a Lisp structure
         (table (delq nil
                      (mapcar
                       (lambda (x)
                         (unless (string-match-p
                                  ein:markdown-table-hline-regexp x)
                           (ein:markdown--split-string x "\\s-*|\\s-*")))
                       (ein:markdown--split-string table "[ \t]*\n[ \t]*"))))
         (dline_old (ein:markdown-table-get-dline))
         (col_old (ein:markdown-table-get-column))
         (contents (mapcar (lambda (_)
                             (let ((tp table))
                               (mapcar
                                (lambda (_)
                                  (prog1
                                      (pop (car tp))
                                    (setq tp (cdr tp))))
                                table)))
                           (car table))))
    (goto-char (ein:markdown-table-begin))
    (re-search-forward "|") (backward-char)
    (delete-region (point) (ein:markdown-table-end))
    (insert (mapconcat
             (lambda(x)
               (concat "| " (mapconcat 'identity x " | " ) "  |\n"))
             contents ""))
    (ein:markdown-table-goto-dline col_old)
    (ein:markdown-table-goto-column dline_old))
  (ein:markdown-table-align))

(defun ein:markdown-table-sort-lines (&optional sorting-type)
  "Sort table lines according to the column at point.

The position of point indicates the column to be used for
sorting, and the range of lines is the range between the nearest
horizontal separator lines, or the entire table of no such lines
exist. If point is before the first column, user will be prompted
for the sorting column. If there is an active region, the mark
specifies the first line and the sorting column, while point
should be in the last line to be included into the sorting.

The command then prompts for the sorting type which can be
alphabetically or numerically. Sorting in reverse order is also
possible.

If SORTING-TYPE is specified when this function is called from a
Lisp program, no prompting will take place. SORTING-TYPE must be
a character, any of (?a ?A ?n ?N) where the capital letters
indicate that sorting should be done in reverse order."
  (interactive)
  (unless (ein:markdown-table-at-point-p)
    (user-error "Not at a table"))
  ;; Set sorting type and column used for sorting
  (let ((column (let ((c (ein:markdown-table-get-column)))
                  (cond ((> c 0) c)
                        ((called-interactively-p 'any)
                         (read-number "Use column N for sorting: "))
                        (t 1))))
        (sorting-type
         (or sorting-type
             (read-char-exclusive
              "Sort type: [a]lpha [n]umeric (A/N means reversed): "))))
    (save-restriction
      ;; Narrow buffer to appropriate sorting area
      (if (region-active-p)
          (narrow-to-region
           (save-excursion
             (progn
               (goto-char (region-beginning)) (line-beginning-position)))
           (save-excursion
             (progn
               (goto-char (region-end)) (line-end-position))))
        (let ((start (ein:markdown-table-begin))
              (end (ein:markdown-table-end)))
          (narrow-to-region
           (save-excursion
             (if (re-search-backward
                  ein:markdown-table-hline-regexp start t)
                 (line-beginning-position 2)
               start))
           (if (save-excursion (re-search-forward
                                ein:markdown-table-hline-regexp end t))
               (match-beginning 0)
             end))))
      ;; Determine arguments for `sort-subr'
      (let* ((extract-key-from-cell
              (cl-case sorting-type
                ((?a ?A) #'ein:markdown--remove-invisible-markup) ;; #'identity)
                ((?n ?N) #'string-to-number)
                (t (user-error "Invalid sorting type: %c" sorting-type))))
             (predicate
              (cl-case sorting-type
                ((?n ?N) #'<)
                ((?a ?A) #'string<))))
        ;; Sort selected area
        (goto-char (point-min))
        (sort-subr (memq sorting-type '(?A ?N))
                   (lambda ()
                     (forward-line)
                     (while (and (not (eobp))
                                 (not (looking-at
                                       ein:markdown-table-dline-regexp)))
                       (forward-line)))
                   #'end-of-line
                   (lambda ()
                     (funcall extract-key-from-cell
                              (ein:markdown-table-get-cell column)))
                   nil
                   predicate)
        (goto-char (point-min))))))

(defun ein:markdown-table-convert-region (begin end &optional separator)
  "Convert region from BEGIN to END to table with SEPARATOR.

If every line contains at least one TAB character, the function
assumes that the material is tab separated (TSV). If every line
contains a comma, comma-separated values (CSV) are assumed. If
not, lines are split at whitespace into cells.

You can use a prefix argument to force a specific separator:
\\[universal-argument] once forces CSV, \\[universal-argument]
twice forces TAB, and \\[universal-argument] three times will
prompt for a regular expression to match the separator, and a
numeric argument N indicates that at least N consecutive
spaces, or alternatively a TAB should be used as the separator."

  (interactive "r\nP")
  (let* ((begin (min begin end)) (end (max begin end)) re)
    (goto-char begin) (beginning-of-line 1)
    (setq begin (point-marker))
    (goto-char end)
    (if (bolp) (backward-char 1) (end-of-line 1))
    (setq end (point-marker))
    (when (equal separator '(64))
      (setq separator (read-regexp "Regexp for cell separator: ")))
    (unless separator
      ;; Get the right cell separator
      (goto-char begin)
      (setq separator
            (cond
             ((not (re-search-forward "^[^\n\t]+$" end t)) '(16))
             ((not (re-search-forward "^[^\n,]+$" end t)) '(4))
             (t 1))))
    (goto-char begin)
    (if (equal separator '(4))
        ;; Parse CSV
        (while (< (point) end)
          (cond
           ((looking-at "^") (insert "| "))
           ((looking-at "[ \t]*$") (replace-match " |") (beginning-of-line 2))
           ((looking-at "[ \t]*\"\\([^\"\n]*\\)\"")
            (replace-match "\\1") (if (looking-at "\"") (insert "\"")))
           ((looking-at "[^,\n]+") (goto-char (match-end 0)))
           ((looking-at "[ \t]*,") (replace-match " | "))
           (t (beginning-of-line 2))))
      (setq re
            (cond
             ((equal separator '(4))  "^\\|\"?[ \t]*,[ \t]*\"?")
             ((equal separator '(16)) "^\\|\t")
             ((integerp separator)
              (if (< separator 1)
                  (user-error "Cell separator must contain one or more spaces")
                (format "^ *\\| *\t *\\| \\{%d,\\}" separator)))
             ((stringp separator) (format "^ *\\|%s" separator))
             (t (error "Invalid cell separator"))))
      (while (re-search-forward re end t) (replace-match "| " t t)))
    (goto-char begin)
    (ein:markdown-table-align)))

(defun ein:markdown-insert-table (&optional rows columns align)
  "Insert an empty pipe table.
Optional arguments ROWS, COLUMNS, and ALIGN specify number of
rows and columns and the column alignment."
  (interactive)
  (let* ((rows (or rows (string-to-number (read-string "Row size: "))))
         (columns (or columns (string-to-number (read-string "Column size: "))))
         (align (or align (read-string "Alignment ([l]eft, [r]ight, [c]enter, or RET for default): ")))
         (align (cond ((equal align "l") ":--")
                      ((equal align "r") "--:")
                      ((equal align "c") ":-:")
                      (t "---")))
         (pos (point))
         (indent (make-string (current-column) ?\ ))
         (line (concat
                (apply 'concat indent "|"
                       (make-list columns "   |")) "\n"))
         (hline (apply 'concat indent "|"
                       (make-list columns (concat align "|")))))
    (if (string-match
         "^[ \t]*$" (buffer-substring-no-properties
                     (line-beginning-position) (point)))
        (beginning-of-line 1)
      (newline))
    (dotimes (_ rows) (insert line))
    (goto-char pos)
    (if (> rows 1)
        (progn
          (end-of-line 1) (insert (concat "\n" hline)) (goto-char pos)))
    (ein:markdown-table-forward-cell)))


;;; ElDoc Support =============================================================

(defun ein:markdown-eldoc-function ()
  "Return a helpful string when appropriate based on context.
* Report URL when point is at a hidden URL.
* Report language name when point is a code block with hidden markup."
  (cond
   ;; Hidden URL or reference for inline link
   ((or (thing-at-point-looking-at ein:markdown-regex-link-inline)
        (thing-at-point-looking-at ein:markdown-regex-link-reference))
    (let* ((imagep (string-equal (match-string 1) "!"))
           (edit-keys (ein:markdown--substitute-command-keys
                       (if imagep
                           "\\[ein:markdown-insert-image]"
                         "\\[ein:markdown-insert-link]")))
           (edit-str (propertize edit-keys 'face 'font-lock-constant-face))
           (referencep (string-equal (match-string 5) "["))
           (object (if referencep "reference" "URL")))
      (format "%s (%s to edit): %s" object edit-str
              (if referencep
                  (concat
                   (propertize "[" 'face 'ein:markdown-markup-face)
                   (propertize (match-string-no-properties 6)
                               'face 'ein:markdown-reference-face)
                   (propertize "]" 'face 'ein:markdown-markup-face))
                (propertize (match-string-no-properties 6)
                            'face 'ein:markdown-url-face)))))
   ;; Hidden language name for fenced code blocks
   ((and (ein:markdown-code-block-at-point-p)
         (not (get-text-property (point) 'ein:markdown-pre)))
    (let ((lang (save-excursion (ein:markdown-code-block-lang))))
      (unless lang (setq lang "[unspecified]"))
      (format "Code block language: %s"
              (propertize lang 'face 'ein:markdown-language-keyword-face))))))


;;; Mode Definition  ==========================================================

(defun ein:markdown-show-version ()
  "Show the version number in the minibuffer."
  (interactive)
  (message "ein:markdown-mode, version %s" ein:markdown-mode-version))

(defun ein:markdown-mode-info ()
  "Open the `ein:markdown-mode' homepage."
  (interactive)
  (browse-url "https://jblevins.org/projects/ein:markdown-mode/"))

;;;###autoload
(define-derived-mode ein:markdown-mode text-mode "ein:markdown"
  "Major mode for editing ein:markdown files."
  ;; Natural ein:markdown tab width
  (setq tab-width 4)
  ;; Comments
  (setq-local comment-start "<!-- ")
  (setq-local comment-end " -->")
  (setq-local comment-start-skip "<!--[ \t]*")
  (setq-local comment-column 0)
  (setq-local comment-auto-fill-only-comments nil)
  (setq-local comment-use-syntax t)
  (setq-local syntax-propertize-function #'ein:markdown-syntax-propertize)
  ;; Font lock.
  (setq font-lock-defaults
        '(ein:markdown-mode-font-lock-keywords
          nil nil nil nil
          (font-lock-multiline . t)
          (font-lock-syntactic-face-function . ein:markdown-syntactic-face)
          (font-lock-extra-managed-props
           . (composition display invisible keymap help-echo mouse-face))))
  ;; Math mode
  (when ein:markdown-enable-math (ein:markdown-toggle-math t))
  ;; Add a buffer-local hook to reload after file-local variables are read
  (add-hook 'hack-local-variables-hook #'ein:markdown-handle-local-variables nil t)
  ;; For imenu support
  (setq imenu-create-index-function
        (if ein:markdown-nested-imenu-heading-index
            #'ein:markdown-imenu-create-nested-index
          #'ein:markdown-imenu-create-flat-index))
  ;; Defun movement
  (setq-local beginning-of-defun-function #'ein:markdown-beginning-of-defun)
  (setq-local end-of-defun-function #'ein:markdown-end-of-defun)
  ;; Paragraph filling
  (setq-local fill-paragraph-function #'ein:markdown-fill-paragraph)
  (setq-local paragraph-start
              ;; Should match start of lines that start or separate paragraphs
              (mapconcat #'identity
                         '(
                           "\f" ; starts with a literal line-feed
                           "[ \t\f]*$" ; space-only line
                           "\\(?:[ \t]*>\\)+[ \t\f]*$"; empty line in blockquote
                           "[ \t]*[*+-][ \t]+" ; unordered list item
                           "[ \t]*\\(?:[0-9]+\\|#\\)\\.[ \t]+" ; ordered list item
                           "[ \t]*\\[\\S-*\\]:[ \t]+" ; link ref def
                           "[ \t]*:[ \t]+" ; definition
                           "^|" ; table or Pandoc line block
                           )
                         "\\|"))
  (setq-local paragraph-separate
              ;; Should match lines that separate paragraphs without being
              ;; part of any paragraph:
              (mapconcat #'identity
                         '("[ \t\f]*$" ; space-only line
                           "\\(?:[ \t]*>\\)+[ \t\f]*$"; empty line in blockquote
                           ;; The following is not ideal, but the Fill customization
                           ;; options really only handle paragraph-starting prefixes,
                           ;; not paragraph-ending suffixes:
                           ".*  $" ; line ending in two spaces
                           "^#+"
                           "[ \t]*\\[\\^\\S-*\\]:[ \t]*$") ; just the start of a footnote def
                         "\\|"))
  (setq-local adaptive-fill-first-line-regexp "\\`[ \t]*[A-Z]?>[ \t]*?\\'")
  (setq-local adaptive-fill-regexp "\\s-*")
  (setq-local adaptive-fill-function #'ein:markdown-adaptive-fill-function)
  (setq-local fill-forward-paragraph-function #'ein:markdown-fill-forward-paragraph)
  ;; Outline mode
  (setq-local outline-regexp ein:markdown-regex-header)
  (setq-local outline-level #'ein:markdown-outline-level)
  ;; Cause use of ellipses for invisible text.
  (add-to-invisibility-spec '(outline . t))
  ;; ElDoc support
  (if (eval-when-compile (fboundp 'add-function))
      (add-function :before-until (local 'eldoc-documentation-function)
                    #'ein:markdown-eldoc-function)
    (setq-local eldoc-documentation-function #'ein:markdown-eldoc-function))
  ;; Inhibiting line-breaking:
  ;; Separating out each condition into a separate function so that users can
  ;; override if desired (with remove-hook)
  (add-hook 'fill-nobreak-predicate
            #'ein:markdown-line-is-reference-definition-p nil t)
  (add-hook 'fill-nobreak-predicate
            #'ein:markdown-pipe-at-bol-p nil t)

  ;; Indentation
  (setq-local indent-line-function ein:markdown-indent-function)

  ;; Flyspell
  (setq-local flyspell-generic-check-word-predicate
              #'ein:markdown-flyspell-check-word-p)

  ;; Electric quoting
  (add-hook 'electric-quote-inhibit-functions
            #'ein:markdown--inhibit-electric-quote nil :local)

  ;; Backwards compatibility with ein:markdown-css-path
  (when (boundp 'ein:markdown-css-path)
    (warn "ein:markdown-css-path is deprecated, see ein:markdown-css-paths.")
    (add-to-list 'ein:markdown-css-paths ein:markdown-css-path)))

(ein:markdown-update-header-faces)
(provide 'ein-markdown-mode)

;; Local Variables:
;; indent-tabs-mode: nil
;; coding: utf-8
;; End:
;;; ein:markdown-mode.el ends here
