diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,13 @@
+<!--
+ - SPDX-FileCopyrightText: 2020 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -->
+
+Unreleased
+==========
+
+0.1.0.0
+=======
+
+Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,128 @@
+<!--
+ - SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -->
+
+# Xrefcheck
+
+[![Build status](https://badge.buildkite.com/75461331a6058b334383cdfca1071dc1f908b70cf069d857b7.svg?branch=master)](https://buildkite.com/serokell/xrefcheck)
+
+Xrefcheck is a tool for verifying local and external references in repository documentation that is quick, easy to setup, and suitable to be added to CI.
+
+<img src="https://user-images.githubusercontent.com/5394217/70820564-06b06e00-1dea-11ea-9680-27f661ca2a58.png" alt="Output sample" width="600"/>
+
+### Motivation
+
+As the project evolves, links in documentation have a tendency to get broken. This is usually because of:
+1. File movements;
+2. Markdown header renames;
+3. Outer sites ceasing their existence.
+
+This tool will help you to keep references in order.
+
+### Aims
+
+Comparing to alternative solutions, this tool tries to achieve the following points:
+
+* Quickness - local references are verified instantly even for moderately-sized repositories.
+* Easy setup - no extra actions required, just run the tool in the repository root.
+Both relative and absolute local links are supported out of the box.
+* Conservative verifier allows using this tool in CI, no false positives (e.g. on sites which require authentication) should be reported.
+
+### A comparison with other solutions
+
+* [linky](https://github.com/mattias-p/linky) - a well-configurable verifier written in Rust, scans one file at a time and works good in pair with system utilities like `find`.
+  This tool requires some configuring before it can be applied to a repository or added to CI.
+* [awesome_bot](https://github.com/dkhamsing/awesome_bot) - a solution written in Ruby that can be easily included in CI or integrated into GitHub.
+  Its features include duplicated URLs detection, specifying allowed HTTP error codes and reporting generation.
+  At the moment of writting, it scans only external references and checking anchors is not possible.
+* [remark-validate-links](https://github.com/remarkjs/remark-validate-links) and [remark-lint-no-dead-urls](https://github.com/davidtheclark/remark-lint-no-dead-urls) - highly configurable Javascript solution for checking local and remote links resp.
+  It is able to check multiple repositores at once if they are gathered in one folder.
+  Being written on JavaScript, it is fairly slow on large repositories.
+* [markdown-link-check](https://github.com/tcort/markdown-link-check) - another checker written in JavaScript, scans one file at a time.
+  Supports `mailto:` link resolution.
+* [url-checker](https://github.com/paramt/url-checker) - GitHub action which checks links in specified files.
+* [broken-link-checker](https://github.com/stevenvachon/broken-link-checker) - advanced checker for `HTML` files.
+
+## Build instructions [↑](#xrefcheck)
+
+Run `stack install` to build everything and install the executable.
+
+### CI and nix [↑](#xrefcheck)
+
+To build only the executables, run `nix-build`. You can use this line on your CI to use xrefcheck:
+```
+nix run -f https://github.com/serokell/xrefcheck/archive/master.tar.gz -c xrefcheck
+```
+
+Our CI uses `nix-build xrefcheck.nix` to build the whole project, including tests and Haddock.
+It is based on the [`haskell.nix`](https://input-output-hk.github.io/haskell.nix/) project.
+You can do that too if you wish.
+
+<details>
+  <summary>Details</summary>
+
+There is a [bug](https://github.com/input-output-hk/haskell.nix/issues/335) which causes us to put some redundancy into Nix files:
+1. [`nix/sources.json`](nix/sources.json) lists all such dependencies that we obtain using `git`.
+It specifies concrete git revisions and SHA256 checksums.
+2. [`xrefcheck.nix`](xrefcheck.nix) lists all such dependencies as well, but without revisions.
+
+As a consequence, you may have to update these files when you update [`stack.yaml`](stack.yaml).
+You can use [`niv update`](https://github.com/nmattia/niv#update) to update [`nix/sources.json`](nix/sources.json).
+
+</details>
+
+## Usage [↑](#xrefcheck)
+
+To find all broken links in a repository, run from within its folder:
+
+```sh
+xrefcheck
+```
+
+To also display all found links and anchors:
+
+```sh
+xrefcheck -v
+```
+
+For description of other options:
+
+```sh
+xrefcheck --help
+```
+
+## Configuring
+
+Configuration template (with all options explained) can be dumped with:
+
+```sh
+xrefcheck dump-config
+```
+
+Currently supported options include:
+* Timeout for checking external references;
+* List of ignored folders.
+
+## For further work [↑](#xrefcheck)
+
+- [ ] Support for non-Unix systems.
+- [ ] Support link detection in different languages, not only Markdown.
+  - [ ] Haskell Haddock is first in turn.
+
+## Issue tracker [↑](#xrefcheck)
+
+We use GitHub issues as our issue tracker.
+You can login using your GitHub account to leave a comment or create a new issue.
+
+## For Contributors [↑](#xrefcheck)
+
+Please see [CONTRIBUTING.md](/.github/CONTRIBUTING.md) for more information.
+
+## About Serokell [↑](#xrefcheck)
+
+Xrefcheck is maintained and funded with ❤️ by [Serokell](https://serokell.io/).
+The names and logo for Serokell are trademark of Serokell OÜ.
+
+We love open source software! See [our other projects](https://serokell.io/community?utm_source=github) or [hire us](https://serokell.io/hire-us?utm_source=github) to design, develop and grow your idea!
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,7 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+import Distribution.Simple
+main = defaultMain
diff --git a/exec/Main.hs b/exec/Main.hs
new file mode 100644
--- /dev/null
+++ b/exec/Main.hs
@@ -0,0 +1,77 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+module Main where
+
+import qualified Data.ByteString as BS
+import Data.Yaml (decodeFileEither, prettyPrintParseException)
+import Fmt (blockListF', build, fmt, fmtLn, indentF)
+import System.Directory (doesFileExist)
+
+import Xrefcheck.CLI
+import Xrefcheck.Config
+import Xrefcheck.Progress
+import Xrefcheck.Scan
+import Xrefcheck.Scanners
+import Xrefcheck.Verify
+
+formats :: FormatsSupport
+formats = specificFormatsSupport
+    [ markdownSupport
+    ]
+
+defaultAction :: Options -> IO ()
+defaultAction Options{..} = do
+    let root = oRoot
+
+    config <- case oConfigPath of
+      Nothing -> do
+        mConfigPath <- findFirstExistingFile defaultConfigPaths
+        case mConfigPath of
+          Nothing -> do
+            hPutStrLn @Text stderr "Configuration file not found, using default config\n"
+            pure defConfig
+          Just configPath ->
+            readConfig configPath
+      Just configPath -> do
+        readConfig configPath
+
+    repoInfo <- allowRewrite oShowProgressBar $ \rw ->
+        gatherRepoInfo rw formats (cTraversal config) root
+
+    when oVerbose $
+        fmtLn $ "Repository data:\n\n" <> indentF 2 (build repoInfo)
+
+    verifyRes <- allowRewrite oShowProgressBar $ \rw ->
+        verifyRepo rw (cVerification config) oMode root repoInfo
+    case verifyErrors verifyRes of
+        Nothing ->
+            fmtLn "All repository links are valid."
+        Just (toList -> errs) -> do
+            fmt $ "Invalid references found:\n\n" <>
+                  indentF 2 (blockListF' "➥ " build errs)
+            fmtLn $ "Invalid references dumped, " <> build (length errs) <> " in total."
+            exitFailure
+  where
+    findFirstExistingFile :: [FilePath] -> IO (Maybe FilePath)
+    findFirstExistingFile = \case
+      [] -> pure Nothing
+      (file : files) -> do
+        exists <- doesFileExist file
+        if exists then pure (Just file) else findFirstExistingFile files
+
+    readConfig :: FilePath -> IO Config
+    readConfig path =
+      decodeFileEither path
+      >>= either (error . toText . prettyPrintParseException) pure
+
+main :: IO ()
+main = do
+    command <- getCommand
+    case command of
+      DefaultCommand options ->
+        defaultAction options
+      DumpConfig path ->
+        BS.writeFile path defConfigText
diff --git a/src-files/def-config.yaml b/src-files/def-config.yaml
new file mode 100644
--- /dev/null
+++ b/src-files/def-config.yaml
@@ -0,0 +1,51 @@
+# SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
+#
+# SPDX-License-Identifier: Unlicense
+
+# Parameters of repository traversal.
+traversal:
+  # Folders which we pretend do not exist
+  # (so they are neither analyzed nor can be referenced).
+  ignored:
+    # Git files
+    - .git
+
+    # Stack files
+    - .stack-work
+
+# Verification parameters.
+verification:
+  # On 'anchor not found' error, how much similar anchors should be displayed as
+  # hint. Number should be between 0 and 1, larger value means stricter filter.
+  anchorSimilarityThreshold: 0.5
+
+  # When checking external references, how long to wait on request before
+  # declaring "Response timeout".
+  externalRefCheckTimeout: 10s
+
+  # File prefixes, references in which should not be analyzed.
+  notScanned:
+    # GitHub-specific files
+    - .github/pull_request_template.md
+    - .github/issue_template.md
+    - .github/PULL_REQUEST_TEMPLATE
+    - .github/ISSUE_TEMPLATE
+
+    # GitLab-specific files
+    - .gitlab/merge_request_templates/
+    - .gitlab/issue_templates/
+
+  # Glob patterns describing the files which do not physically exist in the
+  # repository but should be treated as existing nevertheless.
+  virtualFiles:
+    # GitHub pages
+    - ../../../issues
+    - ../../../issues/*
+    - ../../../pulls
+    - ../../../pulls/*
+
+    # GitLab pages
+    - ../../issues
+    - ../../issues/*
+    - ../../merge_requests
+    - ../../merge_requests/*
diff --git a/src/Xrefcheck/CLI.hs b/src/Xrefcheck/CLI.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/CLI.hs
@@ -0,0 +1,122 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# LANGUAGE ApplicativeDo #-}
+
+module Xrefcheck.CLI
+    ( VerifyMode (..)
+    , shouldCheckLocal
+    , shouldCheckExternal
+    , Command (..)
+    , Options (..)
+    , defaultConfigPaths
+    , getCommand
+    ) where
+
+import Data.Version (showVersion)
+import Options.Applicative (Parser, ReadM, command, eitherReader, execParser, fullDesc, help,
+                            helper, hsubparser, info, infoOption, long, metavar, option, progDesc,
+                            short, strOption, switch, value)
+import Paths_xrefcheck (version)
+
+import Xrefcheck.Core
+
+modeReadM :: ReadM VerifyMode
+modeReadM = eitherReader $ \s ->
+    case find ((== s) . fst) modes of
+        Just (_, mode) -> Right mode
+        Nothing -> Left . mconcat $ intersperse "\n"
+            [ "Unknown mode " <> show s <> "."
+            , "Allowed values: " <> mconcat (intersperse ", " $ map (show . fst) modes)
+            ]
+  where
+    modes =
+        [ ("local-only", LocalOnlyMode)
+        , ("external-only", ExternalOnlyMode)
+        , ("full", FullMode)
+        ]
+
+data Command
+  = DefaultCommand Options
+  | DumpConfig FilePath
+
+data Options = Options
+    { oConfigPath      :: Maybe FilePath
+    , oRoot            :: FilePath
+    , oMode            :: VerifyMode
+    , oVerbose         :: Bool
+    , oShowProgressBar :: Bool
+    }
+
+-- | Where to try to seek configuration if specific path is not set.
+defaultConfigPaths :: [FilePath]
+defaultConfigPaths = ["./xrefcheck.yaml", "./.xrefcheck.yaml"]
+
+optionsParser :: Parser Options
+optionsParser = do
+    oConfigPath <- optional . strOption $
+        short 'c' <>
+        long "config" <>
+        metavar "FILEPATH" <>
+        help ("Path to configuration file. \
+             \If not specified, tries to read config from one of " <>
+             (mconcat . intersperse ", " $ map show defaultConfigPaths) <> ". \
+             \If none of these files exist, default configuration is used."
+             )
+    oRoot <- strOption $
+        short 'r' <>
+        long "root" <>
+        metavar "DIRECTORY" <>
+        help "Path to repository root." <>
+        value "."
+    oMode <- option modeReadM $
+        short 'm' <>
+        long "mode" <>
+        metavar "KEYWORD" <>
+        value FullMode <>
+        help "Which parts of verification to invoke. \
+             \You can enable only verification of repository-local references, \
+             \only verification of external references or both. \
+             \Default mode: full."
+    oVerbose <- switch $
+        short 'v' <>
+        long "verbose" <>
+        help "Report repository scan and verification details."
+    oShowProgressBar <- fmap not . switch $
+        long "no-progress" <>
+        help "Do not display progress bar during verification."
+    return Options{..}
+
+dumpConfigOptions :: Parser FilePath
+dumpConfigOptions = hsubparser $
+  command "dump-config" $
+    info parser $
+    progDesc "Dump default configuration into a file."
+  where
+    parser = strOption $
+      short 'o' <>
+      long "output" <>
+      metavar "FILEPATH" <>
+      value ".xrefcheck.yaml" <>
+      help "Name of created config file."
+
+totalParser :: Parser Command
+totalParser = asum
+  [ DefaultCommand <$> optionsParser
+  , DumpConfig <$> dumpConfigOptions
+  ]
+
+versionOption :: Parser (a -> a)
+versionOption = infoOption ("xrefcheck-" <> showVersion version) $
+    long "version" <>
+    help "Show version."
+
+getCommand :: IO Command
+getCommand = do
+    execParser $
+        info (helper <*> versionOption <*> totalParser) $
+        fullDesc <>
+        progDesc "Cross-references verifier for markdown documentation in \
+                 \Git repositories."
diff --git a/src/Xrefcheck/Config.hs b/src/Xrefcheck/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/Config.hs
@@ -0,0 +1,70 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Xrefcheck.Config where
+
+import Data.Aeson.Options (defaultOptions)
+import Data.Aeson.TH (deriveFromJSON)
+import Data.Yaml (FromJSON (..), decodeEither', prettyPrintParseException, withText)
+import Instances.TH.Lift ()
+import qualified Language.Haskell.TH.Syntax as TH
+import System.FilePath.Posix ((</>))
+import TH.RelativePaths (qReadFileBS)
+import Time (KnownRatName, Second, Time, unitsP)
+
+import Xrefcheck.System (CanonicalizedGlobPattern)
+
+-- | Overall config.
+data Config = Config
+    { cTraversal    :: TraversalConfig
+    , cVerification :: VerifyConfig
+    }
+
+-- | Config of repositry traversal.
+data TraversalConfig = TraversalConfig
+    { tcIgnored   :: [FilePath]
+      -- ^ Folders, files in which we completely ignore.
+    }
+
+-- | Config of verification.
+data VerifyConfig = VerifyConfig
+    { vcAnchorSimilarityThreshold :: Double
+    , vcExternalRefCheckTimeout   :: Time Second
+    , vcVirtualFiles              :: [CanonicalizedGlobPattern]
+      -- ^ Files which we pretend do exist.
+    , vcNotScanned                :: [FilePath]
+      -- ^ Folders, references in files of which we should not analyze.
+    }
+
+-----------------------------------------------------------
+-- Default config
+-----------------------------------------------------------
+
+-- | Default config in textual representation.
+--
+-- Sometimes you cannot just use 'defConfig' because clarifying comments
+-- would be lost.
+defConfigText :: ByteString
+defConfigText =
+  $(TH.lift =<< qReadFileBS ("src-files" </> "def-config.yaml"))
+
+defConfig :: HasCallStack => Config
+defConfig =
+  either (error . toText . prettyPrintParseException) id $
+  decodeEither' defConfigText
+
+-----------------------------------------------------------
+-- Yaml instances
+-----------------------------------------------------------
+
+deriveFromJSON defaultOptions ''Config
+deriveFromJSON defaultOptions ''TraversalConfig
+deriveFromJSON defaultOptions ''VerifyConfig
+
+instance KnownRatName unit => FromJSON (Time unit) where
+    parseJSON = withText "time" $
+        maybe (fail "Unknown time") pure . unitsP . toString
diff --git a/src/Xrefcheck/Core.hs b/src/Xrefcheck/Core.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/Core.hs
@@ -0,0 +1,280 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# LANGUAGE PatternSynonyms #-}
+
+-- | Various primitives.
+
+module Xrefcheck.Core where
+
+import Control.DeepSeq (NFData)
+import Control.Lens (makeLenses, (%=))
+import Data.Char (isAlphaNum)
+import qualified Data.Char as C
+import Data.Default (Default (..))
+import qualified Data.List as L
+import qualified Data.Map as M
+import qualified Data.Text as T
+import Fmt (Buildable (..), blockListF, blockListF', nameF, (+|), (|+))
+import System.Console.Pretty (Color (..), Style (..), color, style)
+import System.FilePath.Posix (isPathSeparator, pathSeparator)
+import Text.Numeral.Roman (toRoman)
+
+import Xrefcheck.Progress
+import Xrefcheck.Util
+
+-----------------------------------------------------------
+-- Types
+-----------------------------------------------------------
+
+-- | Description of element position in source file.
+-- We keep this in text because scanners for different formats use different
+-- representation of this thing, and it actually appears in reports only.
+newtype Position = Position (Maybe Text)
+    deriving (Show, Generic)
+
+instance Buildable Position where
+    build (Position pos) = case pos of
+        Nothing -> ""
+        Just p  -> style Faint $ "at src:" <> build p
+
+-- | Full info about a reference.
+data Reference = Reference
+    { rName   :: Text
+      -- ^ Text displayed as reference.
+    , rLink   :: Text
+      -- ^ File or site reference points to.
+    , rAnchor :: Maybe Text
+      -- ^ Section or custom anchor tag.
+    , rPos    :: Position
+    } deriving (Show, Generic)
+
+-- | Context of anchor.
+data AnchorType
+    = HeaderAnchor Int
+      -- ^ Every section header is usually an anchor
+    | HandAnchor
+      -- ^ They can be set up manually
+    | BiblioAnchor
+      -- ^ Id of entry in bibliography
+    deriving (Show, Eq, Generic)
+
+-- | A referable anchor.
+data Anchor = Anchor
+    { aType :: AnchorType
+    , aName :: Text
+    , aPos  :: Position
+    } deriving (Show, Generic)
+
+-- | All information regarding a single file we care about.
+data FileInfo = FileInfo
+    { _fiReferences :: [Reference]
+    , _fiAnchors    :: [Anchor]
+    } deriving (Show, Generic)
+makeLenses ''FileInfo
+
+instance Default FileInfo where
+    def = FileInfo [] []
+
+newtype RepoInfo = RepoInfo (Map FilePath FileInfo)
+    deriving (Show)
+
+finaliseFileInfo :: FileInfo -> FileInfo
+finaliseFileInfo = execState $ do
+    fiReferences %= reverse
+    fiAnchors %= reverse
+
+-----------------------------------------------------------
+-- Instances
+-----------------------------------------------------------
+
+instance NFData Position
+instance NFData Reference
+instance NFData AnchorType
+instance NFData Anchor
+instance NFData FileInfo
+
+instance Buildable Reference where
+    build Reference{..} =
+        nameF ("reference " +| paren (build loc) |+ " " +| rPos |+ "") $
+            blockListF
+            [ "text: " <> show rName
+            , "link: " <> build rLink
+            , "anchor: " <> build (rAnchor ?: style Faint "-")
+            ]
+      where
+        loc = locationType rLink
+
+instance Buildable AnchorType where
+    build = style Faint . \case
+        HeaderAnchor l -> color Green ("header " <> toRoman l)
+        HandAnchor -> color Yellow "hand made"
+        BiblioAnchor -> color Cyan "biblio"
+
+instance Buildable Anchor where
+    build (Anchor t a p) = a |+ " (" +| t |+ ") " +| p |+ ""
+
+instance Buildable FileInfo where
+    build FileInfo{..} =
+        blockListF
+        [ nameF "references" $ blockListF _fiReferences
+        , nameF "anchors" $ blockListF _fiAnchors
+        ]
+
+instance Buildable RepoInfo where
+    build (RepoInfo m) =
+        blockListF' "⮚" buildFileReport (M.toList m)
+      where
+        buildFileReport (name, info) = mconcat
+            [ color Cyan $ fromString name <> ":\n"
+            , build info
+            , "\n"
+            ]
+
+-----------------------------------------------------------
+-- Analysing
+-----------------------------------------------------------
+
+pattern PathSep :: Char
+pattern PathSep <- (isPathSeparator -> True)
+
+-- | Type of reference.
+data LocationType
+    = LocalLoc
+      -- ^ Reference on this file
+    | RelativeLoc
+      -- ^ Reference to a file relative to given one
+    | AbsoluteLoc
+      -- ^ Reference to a file relative to the root
+    | ExternalLoc
+      -- ^ Reference to a file at outer site
+    | OtherLoc
+      -- ^ Entry not to be processed (e.g. "mailto:e-mail")
+    deriving (Show)
+
+instance Buildable LocationType where
+    build = \case
+        LocalLoc -> color Green "local"
+        RelativeLoc -> color Yellow "relative"
+        AbsoluteLoc -> color Blue "absolute"
+        ExternalLoc -> color Red "external"
+        OtherLoc -> ""
+
+-- | Whether this is a link to external resource.
+isExternal :: LocationType -> Bool
+isExternal = \case
+    ExternalLoc -> True
+    _ -> False
+
+-- | Whether this is a link to repo-local resource.
+isLocal :: LocationType -> Bool
+isLocal = \case
+    LocalLoc -> True
+    RelativeLoc -> True
+    AbsoluteLoc -> True
+    ExternalLoc -> False
+    OtherLoc -> False
+
+-- | Get type of reference.
+locationType :: Text -> LocationType
+locationType location = case toString location of
+    []                      -> LocalLoc
+    PathSep : _             -> AbsoluteLoc
+    '.' : PathSep : _       -> RelativeLoc
+    '.' : '.' : PathSep : _ -> RelativeLoc
+    _ | hasUrlProtocol      -> ExternalLoc
+      | hasProtocol         -> OtherLoc
+      | otherwise           -> RelativeLoc
+  where
+    hasUrlProtocol = "://" `T.isInfixOf` T.take 10 location
+    hasProtocol = ":" `T.isInfixOf` T.take 10 location
+
+-- | Which parts of verification do we perform.
+data VerifyMode
+    = LocalOnlyMode
+    | ExternalOnlyMode
+    | FullMode
+
+shouldCheckLocal :: VerifyMode -> Bool
+shouldCheckLocal = \case
+    LocalOnlyMode -> True
+    ExternalOnlyMode -> False
+    FullMode -> True
+
+shouldCheckExternal :: VerifyMode -> Bool
+shouldCheckExternal = \case
+    LocalOnlyMode -> False
+    ExternalOnlyMode -> True
+    FullMode -> True
+
+-- | Convert section header name to an anchor refering it.
+-- Conversion rules: https://docs.gitlab.com/ee/user/markdown.html#header-ids-and-links
+headerToAnchor :: Text -> Text
+headerToAnchor t = t
+    & T.toLower
+    & T.replace "+" tmp
+    & T.replace " " tmp
+    & joinSyms tmp
+    & T.replace (tmp <> "-") "-"
+    & T.replace ("-" <> tmp) "-"
+    & T.replace tmp "-"
+    & T.filter (\c -> isAlphaNum c || c == '_' || c == '-')
+  where
+    joinSyms sym = T.intercalate sym . filter (not . null) . T.splitOn sym
+    tmp = "\0"
+
+-- | When there are several anchors with the same name, github automatically attaches
+-- "-<number>" suffixes to duplications to make them referable unambiguously.
+-- For instance, if there are two headers called "description", they would gain
+-- "description" and "description-1" anchors correspondingly.
+--
+-- This function strips this suffix and returns the original anchor in case when
+-- suffix is present.
+stripAnchorDupNo :: Text -> Maybe Text
+stripAnchorDupNo t = do
+    let strippedNo = T.dropWhileEnd C.isNumber t
+    guard (length strippedNo < length t)
+    T.stripSuffix "-" strippedNo
+
+-- | Strip './' prefix from local references.
+canonizeLocalRef :: Text -> Text
+canonizeLocalRef ref =
+    case T.stripPrefix localPrefix ref of
+      Nothing -> ref
+      Just r  -> canonizeLocalRef r
+  where
+    localPrefix = toText ['.', pathSeparator]
+
+-----------------------------------------------------------
+-- Visualisation
+-----------------------------------------------------------
+
+data VerifyProgress = VerifyProgress
+    { vrLocal    :: !(Progress Int)
+    , vrExternal :: !(Progress Int)
+    } deriving (Show)
+
+initVerifyProgress :: [Reference] -> VerifyProgress
+initVerifyProgress references =
+    VerifyProgress
+    { vrLocal = initProgress (length localRefs)
+    , vrExternal = initProgress (length extRefs)
+    }
+  where
+    (extRefs, localRefs) =
+        L.partition isExternal $
+        map (locationType . rLink) references
+
+showAnalyseProgress :: VerifyMode -> VerifyProgress -> Text
+showAnalyseProgress mode VerifyProgress{..} = mconcat . mconcat $
+    [ [ "Verifying " ]
+    , [ showProgress "local" 10 White vrLocal <> " "
+      | shouldCheckLocal mode ]
+    , [ showProgress "external" 15 Yellow vrExternal
+      | shouldCheckExternal mode ]
+    ]
+
+reprintAnalyseProgress :: Rewrite -> VerifyMode -> VerifyProgress -> IO ()
+reprintAnalyseProgress rw mode p = putTextRewrite rw (showAnalyseProgress mode p)
diff --git a/src/Xrefcheck/Progress.hs b/src/Xrefcheck/Progress.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/Progress.hs
@@ -0,0 +1,119 @@
+{- SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+-- | Printing progress bars.
+module Xrefcheck.Progress
+    ( -- * Progress
+      Progress (..)
+    , initProgress
+    , incProgress
+    , incProgressErrors
+    , showProgress
+
+      -- * Printing
+    , Rewrite
+    , allowRewrite
+    , putTextRewrite
+    ) where
+
+import Data.Ratio ((%))
+import System.Console.Pretty (Color (..), Style (..), color, style)
+import Time (ms, threadDelay)
+
+-----------------------------------------------------------
+-- Progress
+-----------------------------------------------------------
+
+-- | Processing progress of any thing.
+data Progress a = Progress
+    { pCurrent :: a
+      -- ^ How much has been completed.
+    , pTotal   :: a
+      -- ^ Overall amount of work.
+    , pErrors  :: !a
+      -- ^ How many of the completed work finished with an error.
+    } deriving (Show)
+
+-- | Initialise null progress.
+initProgress :: Num a => a -> Progress a
+initProgress a = Progress{ pTotal = a, pCurrent = 0, pErrors = 0 }
+
+-- | Increase progress amount.
+incProgress :: (Num a, Show a) => Progress a -> Progress a
+incProgress Progress{..} = Progress{ pCurrent = pCurrent + 1, .. }
+
+-- | Increase errors amount.
+incProgressErrors :: (Num a, Show a) => Progress a -> Progress a
+incProgressErrors Progress{..} = Progress{ pErrors = pErrors + 1, .. }
+
+-- | Visualise progress bar.
+showProgress :: Text -> Int -> Color -> Progress Int -> Text
+showProgress name width col Progress{..} = mconcat
+    [ color col (name <> ": [")
+    , toText bar
+    , color col "]"
+    , status
+    ]
+  where
+    done = floor $ (pCurrent % pTotal) * fromIntegral width
+    errs = ceiling $ (pErrors % pTotal) * fromIntegral width
+    done' = max 0 $ done - errs
+    remained' = width - errs - done'
+    bar | pTotal == 0 = replicate width '-'
+        | otherwise = mconcat
+            [ color Red $ replicate errs '■'
+            , color col $ replicate done' '■'
+            , color col $ replicate remained' ' '
+            , " "
+            ]
+    status
+        | pTotal == 0 = ""
+        | pErrors == 0 = style Faint $ color White "✓"
+        | otherwise = color Red "!"
+
+-----------------------------------------------------------
+-- Rewritable output
+-----------------------------------------------------------
+
+-- | Rewrites state.
+data RewriteCtx = RewriteCtx
+    { rMaxPrintedSize :: IORef Int
+    }
+
+-- | Passing this object allows returning caret and replace text in line.
+-- Only functions which has this thing can do that because being
+-- interleaved with 'putTextLn' printing caret symbol produced garbage.
+data Rewrite
+  = Rewrite RewriteCtx
+    -- ^ Default value.
+  | RewriteDisabled
+    -- ^ Do not print anything which will be rewritten.
+    -- Useful when terminal does not interpret caret returns as expected.
+
+-- | Provide context for rewrite operations.
+allowRewrite :: (MonadIO m, MonadMask m) => Bool -> (Rewrite -> m a) -> m a
+allowRewrite enabled action =
+    bracket prepare erase action
+  where
+    prepare
+      | enabled = do
+          rMaxPrintedSize <- newIORef 0
+          return $ Rewrite RewriteCtx{..}
+      | otherwise =
+          pure RewriteDisabled
+    erase (Rewrite RewriteCtx{..}) = liftIO $ do
+        maxPrintedSize <- readIORef rMaxPrintedSize
+        hPutStr stderr $ '\r' : replicate maxPrintedSize ' ' ++ "\r"
+        -- prevent our output to interleave with further outputs
+        threadDelay (ms 100)
+    erase RewriteDisabled = pass
+
+-- | Return caret and print the given text.
+putTextRewrite :: MonadIO m => Rewrite -> Text -> m ()
+putTextRewrite (Rewrite RewriteCtx{..}) msg = do
+    liftIO $ hPutStr stderr ('\r' : toString msg)
+    atomicModifyIORef' rMaxPrintedSize $ \maxPrinted ->
+        (max maxPrinted (length msg), ())
+putTextRewrite RewriteDisabled _ = pass
diff --git a/src/Xrefcheck/Scan.hs b/src/Xrefcheck/Scan.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/Scan.hs
@@ -0,0 +1,80 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+-- | Generalised repo scanner and analyser.
+
+module Xrefcheck.Scan
+    ( Extension
+    , ScanAction
+    , FormatsSupport
+    , RepoInfo (..)
+
+    , gatherRepoInfo
+    , specificFormatsSupport
+    ) where
+
+import qualified Data.Foldable as F
+import qualified Data.Map as M
+import GHC.Err (errorWithoutStackTrace)
+import qualified System.Directory.Tree as Tree
+import System.FilePath.Posix (takeDirectory, takeExtension, (</>))
+
+import Xrefcheck.Config
+import Xrefcheck.Core
+import Xrefcheck.Progress
+import Xrefcheck.Util ()
+
+-- | File extension, dot included.
+type Extension = String
+
+-- | Way to parse a file.
+type ScanAction = FilePath -> IO FileInfo
+
+-- | All supported ways to parse a file.
+type FormatsSupport = Extension -> Maybe ScanAction
+
+specificFormatsSupport :: [([Extension], ScanAction)] -> FormatsSupport
+specificFormatsSupport formats = \ext -> M.lookup ext formatsMap
+  where
+    formatsMap = M.fromList
+        [ (extension, parser)
+        | (extensions, parser) <- formats
+        , extension <- extensions
+        ]
+
+gatherRepoInfo
+    :: MonadIO m
+    => Rewrite -> FormatsSupport -> TraversalConfig -> FilePath -> m RepoInfo
+gatherRepoInfo rw formatsSupport config root = do
+    putTextRewrite rw "Scanning repository..."
+    _ Tree.:/ repoTree <- liftIO $ Tree.readDirectoryWithL processFile rootNE
+    let fileInfos = filter (\(path, _) -> not $ isIgnored path) $
+                    dropSndMaybes . F.toList $
+                    Tree.zipPaths . (dirOfRoot Tree.:/) $
+                    filterExcludedDirs root repoTree
+    return $ RepoInfo (M.fromList fileInfos)
+  where
+    rootNE = if null root then "." else root
+    dirOfRoot = if root == "" || root == "." then "" else takeDirectory root
+    processFile file = do
+        let ext = takeExtension file
+        let mscanner = formatsSupport ext
+        forM mscanner $ \scanFile ->
+            scanFile file
+    dropSndMaybes l = [(a, b) | (a, Just b) <- l]
+
+    ignored = map (root </>) (tcIgnored config)
+    isIgnored path = any (`isPrefixOf` path) ignored
+    filterExcludedDirs cur = \case
+        Tree.Dir name subfiles ->
+            let subfiles' =
+                  if isIgnored cur
+                  then []
+                  else map visitRec subfiles
+                visitRec sub = filterExcludedDirs (cur </> Tree.name sub) sub
+            in Tree.Dir name subfiles'
+        file@Tree.File{} -> file
+        Tree.Failed _name err ->
+            errorWithoutStackTrace $ "Repository traversal failed: " <> show err
diff --git a/src/Xrefcheck/Scanners.hs b/src/Xrefcheck/Scanners.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/Scanners.hs
@@ -0,0 +1,6 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# OPTIONS_GHC -F -pgmF autoexporter #-}
diff --git a/src/Xrefcheck/Scanners/Markdown.hs b/src/Xrefcheck/Scanners/Markdown.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/Scanners/Markdown.hs
@@ -0,0 +1,102 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+-- | Markdown documents markdownScanner.
+
+module Xrefcheck.Scanners.Markdown
+    ( markdownScanner
+    , markdownSupport
+    ) where
+
+import CMarkGFM (Node (..), NodeType (..), PosInfo (..), commonmarkToNode)
+import Control.Lens ((%=))
+import qualified Data.ByteString.Lazy as BSL
+import Data.Default (Default (..))
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as LT
+import Fmt (Buildable (..), blockListF, nameF, (+|), (|+))
+import GHC.Conc (par)
+
+import Xrefcheck.Core
+import Xrefcheck.Scan
+
+instance Buildable Node where
+    build (Node _mpos ty subs) = nameF (show ty) $ blockListF subs
+
+toPosition :: Maybe PosInfo -> Position
+toPosition = Position . \case
+    Nothing -> Nothing
+    Just PosInfo{..}
+        | startLine == endLine -> Just $
+            startLine |+ ":" +| startColumn |+ "-" +| endColumn |+ ""
+        | otherwise -> Just $
+            " " +|
+            startLine |+ ":" +| startColumn |+ " - " +|
+            endLine |+ ":" +| endColumn |+ ""
+
+nodeFlatten :: Node -> [NodeType]
+nodeFlatten (Node _pos ty subs) = ty : concatMap nodeFlatten subs
+
+nodeExtractText :: Node -> Text
+nodeExtractText = mconcat . map extractText . nodeFlatten
+  where
+    extractText = \case
+        TEXT t -> t
+        CODE t -> t
+        _ -> ""
+
+nodeExtractInfo :: Node -> ExceptT Text Identity FileInfo
+nodeExtractInfo docNode = fmap finaliseFileInfo $ execStateT (loop docNode) def
+  where
+    loop node@(Node pos ty subs) = case ty of
+        DOCUMENT ->
+            mapM_ loop subs
+        PARAGRAPH ->
+            mapM_ loop subs
+        HEADING lvl ->
+            let text = nodeExtractText node
+                aType = HeaderAnchor lvl
+                aName = headerToAnchor text
+                aPos = toPosition pos
+            in fiAnchors %= (Anchor{..} :)
+        LIST _ ->
+            mapM_ loop subs
+        ITEM ->
+            mapM_ loop subs
+        HTML_INLINE htmlText -> do
+            let mName = T.stripSuffix "\">" =<< T.stripPrefix "<a name=\"" htmlText
+            whenJust mName $ \aName -> do
+                let aType = HandAnchor
+                    aPos = toPosition pos
+                fiAnchors %= (Anchor{..} :)
+        LINK url _ -> do
+            let rName = nodeExtractText node
+                rPos = toPosition pos
+                link = if null url then rName else url
+            let (rLink, rAnchor) = case T.splitOn "#" link of
+                    [t]    -> (t, Nothing)
+                    t : ts -> (t, Just $ T.intercalate "#" ts)
+                    []     -> error "impossible"
+            fiReferences %= (Reference{..} :)
+        _ -> pass
+
+parseFileInfo :: FilePath -> LT.Text -> FileInfo
+parseFileInfo path input =
+    let outcome = runIdentity . runExceptT $
+                  nodeExtractInfo $ commonmarkToNode [] [] $ toStrict input
+    in case outcome of
+        Left err  -> error $ "Failed to parse file " <> show path <>
+                             ": " <> show err
+        Right res -> res
+
+markdownScanner :: ScanAction
+markdownScanner path = liftIO $ do
+    res <- parseFileInfo path . decodeUtf8 <$> BSL.readFile path
+    force res `par` return res
+
+markdownSupport :: ([Extension], ScanAction)
+markdownSupport = ([".md"], markdownScanner)
diff --git a/src/Xrefcheck/System.hs b/src/Xrefcheck/System.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/System.hs
@@ -0,0 +1,29 @@
+{- SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+module Xrefcheck.System
+    ( readingSystem
+    , CanonicalizedGlobPattern (..)
+    ) where
+
+import Data.Aeson (FromJSON (..), withText)
+import GHC.IO.Unsafe (unsafePerformIO)
+import System.Directory (canonicalizePath)
+import qualified System.FilePath.Glob as Glob
+
+-- | We can quite safely treat surrounding filesystem as frozen,
+-- so IO reading operations can be turned into pure values.
+readingSystem :: IO a -> a
+readingSystem = unsafePerformIO
+
+-- | Glob pattern with 'canonicalizePath' applied O_o.
+newtype CanonicalizedGlobPattern = CanonicalizedGlobPattern Glob.Pattern
+
+instance FromJSON CanonicalizedGlobPattern where
+    parseJSON = withText "Repo-rooted glob pattern" $ \path -> do
+        let !cpath = readingSystem $ canonicalizePath (toString path)
+        cpat <- Glob.tryCompileWith Glob.compDefault cpath
+                & either fail pure
+        return $ CanonicalizedGlobPattern cpat
diff --git a/src/Xrefcheck/Util.hs b/src/Xrefcheck/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/Util.hs
@@ -0,0 +1,26 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Xrefcheck.Util
+    ( nameF'
+    , paren
+    ) where
+
+import Fmt (Builder, build, fmt, nameF)
+import System.Console.Pretty (Pretty (..), Style (Faint))
+
+instance Pretty Builder where
+    colorize s c = build @Text . colorize s c . fmt
+    style s = build @Text . style s . fmt
+
+nameF' :: Builder -> Builder -> Builder
+nameF' a b = nameF (style Faint a) b
+
+paren :: Builder -> Builder
+paren a
+    | a == "" = ""
+    | otherwise = "(" <> a <> ")"
diff --git a/src/Xrefcheck/Verify.hs b/src/Xrefcheck/Verify.hs
new file mode 100644
--- /dev/null
+++ b/src/Xrefcheck/Verify.hs
@@ -0,0 +1,290 @@
+{- SPDX-FileCopyrightText: 2018-2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# LANGUAGE DeriveFunctor         #-}
+{-# LANGUAGE PartialTypeSignatures #-}
+{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
+
+module Xrefcheck.Verify
+    ( -- * General verification
+      VerifyResult (..)
+    , verifyOk
+    , verifyErrors
+    , verifying
+
+    , WithReferenceLoc (..)
+
+      -- * Cross-references validation
+    , VerifyError (..)
+    , verifyRepo
+    , checkExternalResource
+    ) where
+
+import Control.Concurrent.Async (forConcurrently, withAsync)
+import Control.Monad.Except (ExceptT, MonadError (..))
+import qualified Data.Map as M
+import qualified Data.Text as T
+import Data.Text.Metrics (damerauLevenshteinNorm)
+import Fmt (Buildable (..), blockListF', listF, (+|), (|+))
+import qualified GHC.Exts as Exts
+import Network.HTTP.Client (HttpException (..), HttpExceptionContent (..), responseStatus)
+import Network.HTTP.Req (GET (..), HEAD (..), HttpException (..), NoReqBody (..), defaultHttpConfig,
+                         ignoreResponse, parseUrl, req, runReq)
+import Network.HTTP.Types.Status (Status, statusCode, statusMessage)
+import System.Console.Pretty (Style (..), style)
+import System.Directory (canonicalizePath, doesDirectoryExist, doesFileExist)
+import qualified System.FilePath.Glob as Glob
+import System.FilePath.Posix (takeDirectory, (</>))
+import Time (RatioNat, Second, Time (..), ms, threadDelay, timeout)
+
+import Xrefcheck.Config
+import Xrefcheck.Core
+import Xrefcheck.Progress
+import Xrefcheck.System
+
+{-# ANN module ("HLint: ignore Use uncurry" :: Text) #-}
+{-# ANN module ("HLint: ignore Use 'runExceptT' from Universum" :: Text) #-}
+
+-----------------------------------------------------------
+-- General verification
+-----------------------------------------------------------
+
+newtype VerifyResult e = VerifyResult [e]
+    deriving (Show, Functor)
+
+deriving instance Semigroup (VerifyResult e)
+deriving instance Monoid (VerifyResult e)
+
+instance Buildable e => Buildable (VerifyResult e) where
+    build vr = case verifyErrors vr of
+        Nothing   -> "ok"
+        Just errs -> listF errs
+
+verifyOk :: VerifyResult e -> Bool
+verifyOk (VerifyResult errors) = null errors
+
+verifyErrors :: VerifyResult e -> Maybe (NonEmpty e)
+verifyErrors (VerifyResult errors) = nonEmpty errors
+
+verifying :: Monad m => ExceptT e m () -> m (VerifyResult e)
+verifying (ExceptT action) = fmap toVerifyRes action
+
+toVerifyRes :: Either e () -> VerifyResult e
+toVerifyRes = VerifyResult . either one (\() -> [])
+
+-----------------------------------------------------------
+-- Cross-references validation
+-----------------------------------------------------------
+
+data WithReferenceLoc a = WithReferenceLoc
+    { wrlFile      :: FilePath
+    , wrlReference :: Reference
+    , wrlItem      :: a
+    }
+
+instance Buildable a => Buildable (WithReferenceLoc a) where
+    build WithReferenceLoc{..} =
+        "In file " +| style Faint (style Bold wrlFile) |+ "\nbad " +| wrlReference |+ "\n"
+        +| wrlItem |+ "\n\n"
+
+data VerifyError
+    = FileDoesNotExist FilePath
+    | AnchorDoesNotExist Text [Anchor]
+    | AmbiguousAnchorRef FilePath Text (NonEmpty Anchor)
+    | ExternalResourceInvalidUri
+    | ExternalResourceUnavailable Status
+    | ExternalResourceSomeError Text
+    deriving (Show)
+
+instance Buildable VerifyError where
+    build = \case
+        FileDoesNotExist file ->
+            "⛀  File does not exist:\n   " +| file |+ "\n"
+        AnchorDoesNotExist anchor similar ->
+            "⛀  Anchor '" +| anchor |+ "' is not present" +|
+            anchorHints similar
+        AmbiguousAnchorRef file anchor fileAnchors ->
+            "⛀  Ambiguous reference to anchor '" +| anchor |+ "'\n   " +|
+            "In file " +| file |+ "\n   " +|
+            "Similar anchors are:\n" +|
+                blockListF' "    -" build fileAnchors |+ "" +|
+            "   Use of such anchors is discouraged because referenced object\n\
+            \   can change silently whereas the document containing it evolves.\n"
+        ExternalResourceInvalidUri ->
+            "⛂  Bad url (expected 'http' or 'https')\n"
+        ExternalResourceUnavailable status ->
+            "⛂  Resource unavailable (" +| statusCode status |+ " " +|
+            decodeUtf8 @Text (statusMessage status) |+ ")\n"
+        ExternalResourceSomeError err ->
+            "⛂  " +| build err |+ "\n\n"
+      where
+        anchorHints = \case
+            []  -> "\n"
+            [h] -> ",\n   did you mean " +| h |+ "?\n"
+            hs  -> ", did you mean:\n" +| blockListF' "    -" build hs
+
+verifyRepo
+    :: Rewrite
+    -> VerifyConfig
+    -> VerifyMode
+    -> FilePath
+    -> RepoInfo
+    -> IO (VerifyResult $ WithReferenceLoc VerifyError)
+verifyRepo rw config@VerifyConfig{..} mode root repoInfo'@(RepoInfo repoInfo) = do
+    let toScan = do
+          (file, fileInfo) <- M.toList repoInfo
+          guard . not $ any ((`isPrefixOf` file) . (root </>)) vcNotScanned
+          ref <- _fiReferences fileInfo
+          return (file, ref)
+
+    progressRef <- newIORef $ initVerifyProgress (map snd toScan)
+
+    withAsync (printer progressRef) $ \_ ->
+        fmap fold . forConcurrently toScan $ \(file, ref) ->
+            verifyReference config mode progressRef repoInfo' root file ref
+  where
+    printer progressRef = forever $ do
+        readIORef progressRef >>= reprintAnalyseProgress rw mode
+        threadDelay (ms 100)
+
+shouldCheckLocType :: VerifyMode -> LocationType -> Bool
+shouldCheckLocType mode locType
+    | isExternal locType = shouldCheckExternal mode
+    | isLocal locType = shouldCheckLocal mode
+    | otherwise = False
+
+verifyReference
+    :: VerifyConfig
+    -> VerifyMode
+    -> IORef VerifyProgress
+    -> RepoInfo
+    -> FilePath
+    -> FilePath
+    -> Reference
+    -> IO (VerifyResult $ WithReferenceLoc VerifyError)
+verifyReference config@VerifyConfig{..} mode progressRef (RepoInfo repoInfo)
+                root fileWithReference ref@Reference{..} = do
+
+    let locType = locationType rLink
+
+    if shouldCheckLocType mode locType
+    then do
+        res <- case locType of
+            LocalLoc    -> checkRef rAnchor fileWithReference
+            RelativeLoc -> checkRef rAnchor
+                          (takeDirectory fileWithReference
+                            </> toString (canonizeLocalRef rLink))
+            AbsoluteLoc -> checkRef rAnchor (root <> toString rLink)
+            ExternalLoc -> checkExternalResource config rLink
+            OtherLoc    -> verifying pass
+
+        let moveProgress =
+                incProgress .
+                (if verifyOk res then id else incProgressErrors)
+
+        atomicModifyIORef' progressRef $ \VerifyProgress{..} ->
+            ( if isExternal locType
+              then VerifyProgress{ vrExternal = moveProgress vrExternal, .. }
+              else VerifyProgress{ vrLocal = moveProgress vrLocal, .. }
+            , ()
+            )
+        return $ fmap (WithReferenceLoc fileWithReference ref) res
+    else return mempty
+  where
+    checkRef mAnchor referredFile = verifying $ do
+        checkReferredFileExists referredFile
+        case M.lookup referredFile repoInfo of
+            Nothing -> pass  -- no support for such file, can do nothing
+            Just referredFileInfo ->
+                whenJust mAnchor $ checkAnchor referredFile (_fiAnchors referredFileInfo)
+
+    checkReferredFileExists file = do
+        let fileExists = readingSystem $ doesFileExist file
+        let dirExists = readingSystem $ doesDirectoryExist file
+
+        let cfile = readingSystem $ canonicalizePath file
+        let isVirtual = or
+                [ Glob.match pat cfile
+                | CanonicalizedGlobPattern pat <- vcVirtualFiles ]
+
+        unless (fileExists || dirExists || isVirtual) $
+            throwError (FileDoesNotExist file)
+
+    checkAnchor file fileAnchors anchor = do
+        checkAnchorReferenceAmbiguity file fileAnchors anchor
+        checkDeduplicatedAnchorReference file fileAnchors anchor
+        checkAnchorExists fileAnchors anchor
+
+    -- Detect a case when original file contains two identical anchors, github
+    -- has added a suffix to the duplicate, and now the original is referrenced -
+    -- such links are pretty fragile and we discourage their use despite
+    -- they are in fact unambiguous.
+    checkAnchorReferenceAmbiguity file fileAnchors anchor = do
+        let similarAnchors = filter ((== anchor) . aName) fileAnchors
+        when (length similarAnchors > 1) $
+            throwError $ AmbiguousAnchorRef file anchor (Exts.fromList similarAnchors)
+
+    -- Similar to the previous one, but for the case when we reference the
+    -- renamed duplicate.
+    checkDeduplicatedAnchorReference file fileAnchors anchor =
+        whenJust (stripAnchorDupNo anchor) $ \origAnchor ->
+            checkAnchorReferenceAmbiguity file fileAnchors origAnchor
+
+    checkAnchorExists givenAnchors anchor =
+        case find ((== anchor) . aName) givenAnchors of
+            Just _ -> pass
+            Nothing ->
+                let isSimilar = (>= vcAnchorSimilarityThreshold)
+                    similarAnchors =
+                        filter (isSimilar . realToFrac . damerauLevenshteinNorm anchor . aName)
+                        givenAnchors
+                in throwError $ AnchorDoesNotExist anchor similarAnchors
+
+checkExternalResource :: VerifyConfig
+                      -> Text
+                      -> IO (VerifyResult VerifyError)
+checkExternalResource VerifyConfig{..} link
+    | doesReferLocalhost = return mempty
+    | otherwise = fmap toVerifyRes $ do
+        makeRequest HEAD 0.3 >>= \case
+            Right () -> return $ Right ()
+            Left   _ -> makeRequest GET 0.7
+  where
+    doesReferLocalhost = any (`T.isInfixOf` link) ["://localhost", "://127.0.0.1"]
+
+    makeRequest :: _ => method -> RatioNat -> IO (Either VerifyError ())
+    makeRequest method timeoutFrac = runExceptT $ do
+        parsedUrl <- parseUrl (encodeUtf8 link)
+                   & maybe (throwError ExternalResourceInvalidUri) pure
+        let reqLink = case parsedUrl of
+                Left (url, option) ->
+                    runReq defaultHttpConfig $
+                    req method url NoReqBody ignoreResponse option
+                Right (url, option) ->
+                    runReq defaultHttpConfig $
+                    req method url NoReqBody ignoreResponse option
+
+        let maxTime = Time @Second $ unTime vcExternalRefCheckTimeout * timeoutFrac
+
+        mres <- liftIO (timeout maxTime $ void reqLink)
+                `catch` (either throwError (\() -> return (Just ())) . interpretErrors)
+        maybe (throwError $ ExternalResourceSomeError "Response timeout") pure mres
+
+    isAllowedErrorCode = or . sequence
+        -- We have to stay conservative - if some URL can be accessed under
+        -- some circumstances, we should do our best to report it as fine.
+        [ (403 ==)  -- unauthorized access
+        , (405 ==)  -- method mismatch
+        ]
+
+    interpretErrors = \case
+        JsonHttpException _ -> error "External link JSON parse exception"
+        VanillaHttpException err -> case err of
+            InvalidUrlException{} -> error "External link URL invalid exception"
+            HttpExceptionRequest _ exc -> case exc of
+                StatusCodeException resp _
+                    | isAllowedErrorCode (statusCode $ responseStatus resp) -> Right ()
+                    | otherwise -> Left $ ExternalResourceUnavailable (responseStatus resp)
+                other -> Left . ExternalResourceSomeError $ show other
diff --git a/tests/Main.hs b/tests/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/Main.hs
@@ -0,0 +1,10 @@
+{- SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+import Spec (spec)
+import Test.Hspec (hspec)
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,6 @@
+{- SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
diff --git a/tests/Test/Xrefcheck/AnchorsSpec.hs b/tests/Test/Xrefcheck/AnchorsSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Test/Xrefcheck/AnchorsSpec.hs
@@ -0,0 +1,50 @@
+{- SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+module Test.Xrefcheck.AnchorsSpec where
+
+import Test.Hspec (Spec, describe, it)
+import Test.QuickCheck ((===))
+
+import Xrefcheck.Core (headerToAnchor)
+
+spec :: Spec
+spec = do
+    describe "Header-to-anchor conversion" $
+        mapM_ (\(a, b) -> it (show a <> " == " <> show b) $ headerToAnchor a === b) $
+        [ ( "Some header"
+          , "some-header"
+          )
+        , ( "Do +5 times"
+          , "do-5-times"
+          )
+        , ( "a # b"
+          , "a--b"
+          )
+        , ( "a ## b"
+          , "a--b"
+          )
+        , ( "a * b"
+          , "a--b"
+          )
+        , ( "a / b"
+          , "a--b"
+          )
+        , ( "a \\ b"
+          , "a--b"
+          )
+        , ( "Some *italic* text"
+          , "some-italic-text"
+          )
+        , ( "Some-text-with--many----hyphens"
+          , "some-text-with--many----hyphens"
+          )
+        , ( "Some-+++++--mess++-mda"
+          , "some---mess-mda"
+          )
+        , ( ":white_check_mark: Checklist for your Pull Request"
+          , "white_check_mark-checklist-for-your-pull-request"
+          )
+        ]
diff --git a/tests/Test/Xrefcheck/ConfigSpec.hs b/tests/Test/Xrefcheck/ConfigSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Test/Xrefcheck/ConfigSpec.hs
@@ -0,0 +1,16 @@
+{- SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+module Test.Xrefcheck.ConfigSpec where
+
+import Test.Hspec (Spec, it)
+import Test.QuickCheck (ioProperty, once)
+
+import Xrefcheck.Config
+
+spec :: Spec
+spec =
+  it "Default config is valid" $
+    once . ioProperty $ evaluateWHNF_ @_ @Config defConfig
diff --git a/tests/Test/Xrefcheck/LocalSpec.hs b/tests/Test/Xrefcheck/LocalSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Test/Xrefcheck/LocalSpec.hs
@@ -0,0 +1,23 @@
+{- SPDX-FileCopyrightText: 2019 Serokell <https://serokell.io>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+module Test.Xrefcheck.LocalSpec where
+
+import Test.Hspec (Spec, describe, it)
+import Test.QuickCheck ((===))
+
+import Xrefcheck.Core (canonizeLocalRef)
+
+spec :: Spec
+spec = do
+    describe "Local refs canonizing" $ do
+      it "Strips ./" $
+        canonizeLocalRef "./AnchorsSpec.hs" === "AnchorsSpec.hs"
+
+      it "Strips ././" $
+        canonizeLocalRef "././AnchorsSpec.hs" === "AnchorsSpec.hs"
+
+      it "Leaves plain other intact" $
+        canonizeLocalRef "../AnchorsSpec.hs" === "../AnchorsSpec.hs"
diff --git a/xrefcheck.cabal b/xrefcheck.cabal
new file mode 100644
--- /dev/null
+++ b/xrefcheck.cabal
@@ -0,0 +1,176 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 7e709817763e8a6ce4c21d58175267e5326dd39a1902a4685e2379a2c1442a84
+
+name:           xrefcheck
+version:        0.1.0.0
+description:    Please see the README on GitHub at <https://github.com/serokell/xrefcheck#readme>
+homepage:       https://github.com/serokell/xrefcheck#readme
+bug-reports:    https://github.com/serokell/xrefcheck/issues
+author:         Kostya Ivanov, Serokell
+maintainer:     Serokell <hi@serokell.io>
+copyright:      2018-2019 Serokell <https://serokell.io>
+license:        MPL-2.0
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    CHANGES.md
+    src-files/def-config.yaml
+
+source-repository head
+  type: git
+  location: https://github.com/serokell/xrefcheck
+
+library
+  exposed-modules:
+      Xrefcheck.CLI
+      Xrefcheck.Config
+      Xrefcheck.Core
+      Xrefcheck.Progress
+      Xrefcheck.Scan
+      Xrefcheck.Scanners
+      Xrefcheck.Scanners.Markdown
+      Xrefcheck.System
+      Xrefcheck.Util
+      Xrefcheck.Verify
+  other-modules:
+      Paths_xrefcheck
+  hs-source-dirs:
+      src
+  default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric FlexibleContexts FlexibleInstances FunctionalDependencies GeneralizedNewtypeDeriving LambdaCase MonadFailDesugaring MultiParamTypeClasses MultiWayIf NamedFieldPuns OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeFamilies UndecidableInstances ViewPatterns TypeApplications TypeOperators
+  ghc-options: -Wall -Wincomplete-record-updates
+  build-tool-depends:
+      autoexporter:autoexporter
+  build-depends:
+      Glob
+    , aeson
+    , aeson-options
+    , async
+    , base-noprelude
+    , bytestring
+    , cmark-gfm
+    , containers
+    , data-default
+    , deepseq
+    , directory
+    , directory-tree
+    , filepath
+    , fmt
+    , http-client
+    , http-types
+    , lens
+    , loot-prelude
+    , mtl
+    , o-clock
+    , optparse-applicative
+    , pretty-terminal
+    , req
+    , roman-numerals
+    , template-haskell
+    , text
+    , text-metrics
+    , th-lift-instances
+    , th-utilities
+    , yaml
+  default-language: Haskell2010
+
+executable xrefcheck
+  main-is: Main.hs
+  other-modules:
+      Paths_xrefcheck
+  hs-source-dirs:
+      exec
+  default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric FlexibleContexts FlexibleInstances FunctionalDependencies GeneralizedNewtypeDeriving LambdaCase MonadFailDesugaring MultiParamTypeClasses MultiWayIf NamedFieldPuns OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeFamilies UndecidableInstances ViewPatterns TypeApplications TypeOperators
+  ghc-options: -Wall -Wincomplete-record-updates -threaded -rtsopts -with-rtsopts=-N -O2
+  build-tool-depends:
+      autoexporter:autoexporter
+  build-depends:
+      Glob
+    , aeson
+    , aeson-options
+    , async
+    , base-noprelude
+    , bytestring
+    , cmark-gfm
+    , containers
+    , data-default
+    , deepseq
+    , directory
+    , directory-tree
+    , filepath
+    , fmt
+    , http-client
+    , http-types
+    , lens
+    , loot-prelude
+    , mtl
+    , o-clock
+    , optparse-applicative
+    , pretty-terminal
+    , req
+    , roman-numerals
+    , template-haskell
+    , text
+    , text-metrics
+    , th-lift-instances
+    , th-utilities
+    , xrefcheck
+    , yaml
+  default-language: Haskell2010
+
+test-suite xrefcheck-tests
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  other-modules:
+      Spec
+      Test.Xrefcheck.AnchorsSpec
+      Test.Xrefcheck.ConfigSpec
+      Test.Xrefcheck.LocalSpec
+      Paths_xrefcheck
+  hs-source-dirs:
+      tests
+  default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric FlexibleContexts FlexibleInstances FunctionalDependencies GeneralizedNewtypeDeriving LambdaCase MonadFailDesugaring MultiParamTypeClasses MultiWayIf NamedFieldPuns OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeFamilies UndecidableInstances ViewPatterns TypeApplications TypeOperators
+  ghc-options: -Wall -Wincomplete-record-updates
+  build-tool-depends:
+      autoexporter:autoexporter
+    , hspec-discover:hspec-discover
+  build-depends:
+      Glob
+    , QuickCheck
+    , aeson
+    , aeson-options
+    , async
+    , base-noprelude
+    , bytestring
+    , cmark-gfm
+    , containers
+    , data-default
+    , deepseq
+    , directory
+    , directory-tree
+    , filepath
+    , fmt
+    , hspec
+    , http-client
+    , http-types
+    , lens
+    , loot-prelude
+    , mtl
+    , o-clock
+    , optparse-applicative
+    , pretty-terminal
+    , req
+    , roman-numerals
+    , template-haskell
+    , text
+    , text-metrics
+    , th-lift-instances
+    , th-utilities
+    , xrefcheck
+    , yaml
+  default-language: Haskell2010
