diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Sridhar Ratnakumar
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# commonmark-wikilink
+
+An improved version of [the official wikilink extension](https://hackage.haskell.org/package/commonmark-extensions-0.2.3.3/docs/Commonmark-Extensions-Wikilinks.html) that works with Markdown wikilinks in the wild (such as Obsidian).
+
+This library is used in [Emanote](https://emanote.srid.ca/).
diff --git a/commonmark-wikilink.cabal b/commonmark-wikilink.cabal
new file mode 100644
--- /dev/null
+++ b/commonmark-wikilink.cabal
@@ -0,0 +1,86 @@
+cabal-version:      2.4
+name:               commonmark-wikilink
+version:            0.1.0.0
+license:            MIT
+copyright:          2022 Sridhar Ratnakumar
+maintainer:         srid@srid.ca
+author:             Sridhar Ratnakumar
+category:           Web
+synopsis:           Obsidian-friendly commonmark wikilink parser
+description:        Obsidian-friendly commonmark wikilink parser
+bug-reports:        https://github.com/srid/commonmark-wikilink
+extra-source-files:
+  LICENSE
+  README.md
+
+common shared
+  ghc-options:
+    -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns
+    -Wmissing-deriving-strategies -Wunused-foralls -Wunused-foralls
+    -fprint-explicit-foralls -fprint-explicit-kinds
+
+  mixins:
+    base hiding (Prelude),
+    relude (Relude as Prelude, Relude.Container.One),
+    relude
+
+  default-extensions:
+    NoStarIsType
+    BangPatterns
+    ConstraintKinds
+    DataKinds
+    DeriveDataTypeable
+    DeriveFoldable
+    DeriveFunctor
+    DeriveGeneric
+    DeriveLift
+    DeriveTraversable
+    DerivingStrategies
+    DerivingVia
+    EmptyCase
+    EmptyDataDecls
+    EmptyDataDeriving
+    ExistentialQuantification
+    ExplicitForAll
+    FlexibleContexts
+    FlexibleInstances
+    GADTSyntax
+    GeneralisedNewtypeDeriving
+    ImportQualifiedPost
+    KindSignatures
+    LambdaCase
+    MultiParamTypeClasses
+    MultiWayIf
+    NumericUnderscores
+    OverloadedStrings
+    PolyKinds
+    PostfixOperators
+    RankNTypes
+    ScopedTypeVariables
+    StandaloneDeriving
+    StandaloneKindSignatures
+    TupleSections
+    TypeApplications
+    TypeFamilies
+    TypeOperators
+    ViewPatterns
+
+  build-depends:
+    , aeson
+    , base               >=4.13.0.0 && <4.18.0.0.0
+    , commonmark
+    , commonmark-pandoc
+    , megaparsec
+    , network-uri
+    , pandoc-types
+    , parsec
+    , relude             >=1.0
+    , uri-encode
+    , url-slug
+
+  hs-source-dirs:     src
+  default-language:   Haskell2010
+
+library
+  import:          shared
+  exposed-modules: Commonmark.Extensions.WikiLink
diff --git a/src/Commonmark/Extensions/WikiLink.hs b/src/Commonmark/Extensions/WikiLink.hs
new file mode 100644
--- /dev/null
+++ b/src/Commonmark/Extensions/WikiLink.hs
@@ -0,0 +1,286 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Commonmark.Extensions.WikiLink (
+  -- * Types
+  WikiLink,
+  WikiLinkType (..),
+
+  -- * Parsing wikilinks
+  mkWikiLinkFromSlugs,
+  mkWikiLinkFromInline,
+  delineateLink,
+
+  -- * Wikilink candidates
+  allowedWikiLinks,
+
+  -- * Converting wikilinks
+  wikilinkInline,
+  wikiLinkInlineRendered,
+
+  -- * Commonmark parser spec
+  wikilinkSpec,
+
+  -- * Anchors in URLs
+  Anchor,
+  anchorSuffix,
+
+  -- * Pandoc helper
+  plainify,
+) where
+
+import Commonmark qualified as CM
+import Commonmark.Pandoc qualified as CP
+import Commonmark.TokParsers qualified as CT
+import Control.Monad (liftM2)
+import Data.Aeson (ToJSON (toJSON))
+import Data.Data (Data)
+import Data.List.NonEmpty qualified as NE
+import Data.Map.Strict qualified as Map
+import Data.Text qualified as T
+import Network.URI.Encode qualified as UE
+import Network.URI.Slug (Slug)
+import Network.URI.Slug qualified as Slug
+import Text.Megaparsec qualified as M
+import Text.Pandoc.Builder qualified as B
+import Text.Pandoc.Walk qualified as W
+import Text.Parsec qualified as P
+import Text.Read (Read (readsPrec))
+import Text.Show qualified (Show (show))
+
+{- | Represents the "Foo" in [[Foo]]
+
+ As wiki links may contain multiple path components, it can represent
+ [[Foo/Bar]], hence we use nonempty slug list.
+-}
+newtype WikiLink = WikiLink {unWikiLink :: NonEmpty Slug}
+  deriving stock (Eq, Ord, Typeable, Data)
+
+instance ToJSON WikiLink where
+  toJSON = toJSON . wikilinkUrl
+
+instance Show WikiLink where
+  show wl =
+    toString $ "[[" <> wikilinkUrl wl <> "]]"
+
+-- -----------------
+-- Making wiki links
+-- -----------------
+
+mkWikiLinkFromSlugs :: NonEmpty Slug -> WikiLink
+mkWikiLinkFromSlugs = WikiLink
+
+mkWikiLinkFromUrl :: (Monad m, Alternative m) => Text -> m WikiLink
+mkWikiLinkFromUrl s = do
+  slugs <- maybe empty pure $ nonEmpty $ Slug.decodeSlug <$> T.splitOn "/" s
+  pure $ WikiLink slugs
+
+mkWikiLinkFromInline :: B.Inline -> Maybe (WikiLink, [B.Inline])
+mkWikiLinkFromInline inl = do
+  B.Link (_id, _class, otherAttrs) is (url, tit) <- pure inl
+  (Left (_, wl), _manchor) <- delineateLink (otherAttrs <> one ("title", tit)) url
+  pure (wl, is)
+
+-- | An URL anchor without the '#'
+newtype Anchor = Anchor Text
+  deriving newtype (Eq, Show, Ord)
+
+mkAnchor :: String -> Maybe Anchor
+mkAnchor ('#' : name) = pure $ Anchor $ toText name
+mkAnchor _ = Nothing
+
+anchorSuffix :: Maybe Anchor -> Text
+anchorSuffix = maybe "" (\(Anchor a) -> "#" <> a)
+
+dropUrlAnchor :: Text -> (Text, Maybe Anchor)
+dropUrlAnchor = second (mkAnchor . toString) . T.breakOn "#"
+
+{- | Given a Pandoc Link node, apparaise what kind of link it is.
+
+ * Nothing, if the link is an absolute URL
+ * Just (Left wl), if a wiki-link
+ * Just (Right fp), if a relative path (not a wiki-link)
+-}
+delineateLink :: [(Text, Text)] -> Text -> Maybe (Either (WikiLinkType, WikiLink) FilePath, Maybe Anchor)
+delineateLink (Map.fromList -> attrs) url = do
+  -- Must be relative
+  guard $ not $ "://" `T.isInfixOf` url
+  wikiLink <|> internalLink
+  where
+    wikiLink = do
+      wlType :: WikiLinkType <- readMaybe . toString <=< Map.lookup htmlAttr $ attrs
+      -- Ignore anchors until https://github.com/srid/emanote/discussions/105
+      let (s, manc) = dropUrlAnchor url
+      wl <- mkWikiLinkFromUrl s
+      pure (Left (wlType, wl), manc)
+    internalLink = do
+      -- Avoid links like "mailto:", "magnet:", etc.
+      -- An easy way to parse them is to look for colon character.
+      --
+      -- This does mean that "Foo: Bar.md" cannot be linked to this way, however
+      -- the user can do it using wiki-links.
+      guard $ not $ ":" `T.isInfixOf` url
+      let (s, manc) = dropUrlAnchor url
+      guard $ not $ T.null s -- Same page anchors
+      pure (Right $ UE.decode (toString s), manc)
+
+-- ---------------------
+-- Converting wiki links
+-- ---------------------
+
+-- | [[Foo/Bar]] -> "Foo/Bar"
+wikilinkUrl :: WikiLink -> Text
+wikilinkUrl =
+  T.intercalate "/" . fmap Slug.unSlug . toList . unWikiLink
+
+wikilinkInline :: WikiLinkType -> WikiLink -> B.Inlines -> B.Inlines
+wikilinkInline typ wl = B.linkWith attrs (wikilinkUrl wl) ""
+  where
+    attrs = ("", [], [(htmlAttr, show typ)])
+
+wikiLinkInlineRendered :: B.Inline -> Maybe Text
+wikiLinkInlineRendered x = do
+  (wl, inl) <- mkWikiLinkFromInline x
+  pure $ case nonEmpty inl of
+    Nothing -> show wl
+    Just _ ->
+      let inlStr = plainify inl
+       in if inlStr == wikilinkUrl wl
+            then show wl
+            else "[[" <> wikilinkUrl wl <> "|" <> plainify inl <> "]]"
+
+{- | Return the various ways to link to a route (ignoring ext)
+
+ Foo/Bar/Qux.md -> [[Qux]], [[Bar/Qux]], [[Foo/Bar/Qux]]
+
+ All possible combinations of Wikilink type use is automatically included.
+-}
+allowedWikiLinks :: HasCallStack => NonEmpty Slug -> NonEmpty (WikiLinkType, WikiLink)
+allowedWikiLinks slugs =
+  let wls = WikiLink <$> tailsNE slugs
+      typs :: NonEmpty WikiLinkType = NE.fromList universe
+   in liftM2 (,) typs wls
+  where
+    tailsNE =
+      NE.fromList . mapMaybe nonEmpty . tails . toList
+
+-------------------------
+-- Parser
+--------------------------
+
+{- | A # prefix or suffix allows semantically distinct wikilinks
+
+ Typically called branching link or a tag link, when used with #.
+-}
+data WikiLinkType
+  = -- | [[Foo]]
+    WikiLinkNormal
+  | -- | [[Foo]]#
+    WikiLinkBranch
+  | -- | #[[Foo]]
+    WikiLinkTag
+  | -- | ![[Foo]]
+    WikiLinkEmbed
+  deriving stock (Eq, Show, Ord, Typeable, Data, Enum, Bounded, Generic)
+  deriving anyclass (ToJSON)
+
+instance Read WikiLinkType where
+  readsPrec _ s
+    | s == show WikiLinkNormal = [(WikiLinkNormal, "")]
+    | s == show WikiLinkBranch = [(WikiLinkBranch, "")]
+    | s == show WikiLinkTag = [(WikiLinkTag, "")]
+    | s == show WikiLinkEmbed = [(WikiLinkEmbed, "")]
+    | otherwise = []
+
+-- | The HTML 'data attribute' storing the wiki-link type.
+htmlAttr :: Text
+htmlAttr = "data-wikilink-type"
+
+class HasWikiLink il where
+  wikilink :: WikiLinkType -> WikiLink -> Maybe il -> il
+
+instance HasWikiLink (CP.Cm b B.Inlines) where
+  wikilink typ wl il =
+    CP.Cm $ wikilinkInline typ wl $ maybe mempty CP.unCm il
+
+{- | Like `Commonmark.Extensions.Wikilinks.wikilinkSpec` but Zettelkasten-friendly.
+
+ Compared with the official extension, this has two differences:
+
+ - Supports flipped inner text, eg: `[[Foo | some inner text]]`
+ - Supports neuron folgezettel, i.e.: #[[Foo]] or [[Foo]]#
+-}
+wikilinkSpec ::
+  (Monad m, CM.IsInline il, HasWikiLink il) =>
+  CM.SyntaxSpec m il bl
+wikilinkSpec =
+  mempty
+    { CM.syntaxInlineParsers =
+        [ P.try $
+            P.choice
+              [ P.try (CT.symbol '#' *> pWikilink WikiLinkTag)
+              , P.try (CT.symbol '!' *> pWikilink WikiLinkEmbed)
+              , P.try (pWikilink WikiLinkBranch <* CT.symbol '#')
+              , P.try (pWikilink WikiLinkNormal)
+              ]
+        ]
+    }
+  where
+    pWikilink typ = do
+      replicateM_ 2 $ CT.symbol '['
+      P.notFollowedBy (CT.symbol '[')
+      url <-
+        CM.untokenize <$> many (satisfyNoneOf [isPipe, isAnchor, isClose])
+      wl <- mkWikiLinkFromUrl url
+      -- We ignore the anchor until https://github.com/srid/emanote/discussions/105
+      _anchor <-
+        M.optional $
+          CM.untokenize
+            <$> ( CT.symbol '#'
+                    *> many (satisfyNoneOf [isPipe, isClose])
+                )
+      title <-
+        M.optional $
+          -- TODO: Should parse as inline so link text can be formatted?
+          CM.untokenize
+            <$> ( CT.symbol '|'
+                    *> many (satisfyNoneOf [isClose])
+                )
+      replicateM_ 2 $ CT.symbol ']'
+      return $ wikilink typ wl (fmap CM.str title)
+    satisfyNoneOf toks =
+      CT.satisfyTok $ \t -> not $ or $ toks <&> \tok -> tok t
+    isAnchor =
+      isSymbol '#'
+    isPipe =
+      isSymbol '|'
+    isClose =
+      isSymbol ']'
+    isSymbol c =
+      CT.hasType (CM.Symbol c)
+
+{- | Convert Pandoc AST inlines to raw text.
+
+ TODO: extend on top of plainify from heist-extra
+-}
+plainify :: [B.Inline] -> Text
+plainify = W.query $ \case
+  B.Str x -> x
+  B.Code _attr x -> x
+  B.Space -> " "
+  B.SoftBreak -> " "
+  B.LineBreak -> " "
+  -- TODO: if fmt is html, we should strip the html tags
+  B.RawInline _fmt s -> s
+  -- Ignore "wrapper" inlines like span.
+  B.Span _ _ -> ""
+  -- TODO: How to wrap math stuff here?
+  B.Math _mathTyp s -> s
+  -- Wiki-links must be displayed using its show instance (which returns its
+  -- human-readable representation)
+  (mkWikiLinkFromInline -> Just (wl, customText)) ->
+    if null customText
+      then show wl -- We will display raw wikilink; ideally, though, we want to display the title.
+      else plainify customText
+  -- Ignore the rest of AST nodes, as they are recursively defined in terms of
+  -- `Inline` which `W.query` will traverse again.
+  _ -> ""
