aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2018-06-08 14:49:09 +0200
committerLoïc Hoguin <[email protected]>2018-06-08 14:49:09 +0200
commit524777054be30c848c1883ffd15b245c29f73004 (patch)
tree6c3df0022ee6d228341bb6ce2c525011076c123d
parent48cbfe8b60f3e555acd2d623db10e4eb56234179 (diff)
downloadasciideck-524777054be30c848c1883ffd15b245c29f73004.tar.gz
asciideck-524777054be30c848c1883ffd15b245c29f73004.tar.bz2
asciideck-524777054be30c848c1883ffd15b245c29f73004.zip
Rewrite the project
The new code is much more readable and easier to extend. I took inspiration from Haskell's Parsec project which seems to only write the happy-path and applied the idea to Erlang's exceptions. When the parser tries to parse, say, a list, and crashes, it tries with a table next, and so on until something matches. Normal paragraphs always match so there can be no parsing failures. The parser now has a number of passes: first the block parser, then lists and tables passes to build a proper tree out of them and finally an inline pass to apply inline formatting. The resulting AST can then be modified at will and passed on to translator modules which output a different format. The man page translator was also rewritten and has been tested against both Cowboy and Gun. Numerous issues were fixed as a result of this rewrite.
-rw-r--r--LICENSE13
-rw-r--r--Makefile21
-rw-r--r--ebin/asciideck.app6
-rw-r--r--erlang.mk1490
-rw-r--r--src/asciideck.erl13
-rw-r--r--src/asciideck_attributes_parser.erl120
-rw-r--r--src/asciideck_attributes_pass.erl112
-rw-r--r--src/asciideck_block_parser.erl1116
-rw-r--r--src/asciideck_inline_pass.erl308
-rw-r--r--src/asciideck_line_reader.erl94
-rw-r--r--src/asciideck_lists_pass.erl155
-rw-r--r--src/asciideck_parser.erl388
-rw-r--r--src/asciideck_tables_pass.erl191
-rw-r--r--src/asciideck_to_manpage.erl236
-rw-r--r--test/man_SUITE.erl3
-rw-r--r--test/parser_SUITE.erl286
16 files changed, 3402 insertions, 1150 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b9f1042
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2016-2018, Loïc Hoguin <[email protected]>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
index bba4e2f..06c2e6f 100644
--- a/Makefile
+++ b/Makefile
@@ -2,17 +2,22 @@
PROJECT = asciideck
PROJECT_DESCRIPTION = Asciidoc for Erlang.
-PROJECT_VERSION = 0.1.0
-
-# Options.
-
-CI_OTP ?= OTP-18.0.3 OTP-18.1.5 OTP-18.2.4.1 OTP-18.3.4.4 OTP-19.0.7 OTP-19.1.5
-CI_HIPE ?= $(lastword $(CI_OTP))
-CI_ERLLVM ?= $(CI_HIPE)
+PROJECT_VERSION = 0.2.0
# Dependencies.
-TEST_DEPS = ct_helper
+TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}'
+TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) ct_helper
dep_ct_helper = git https://github.com/ninenines/ct_helper master
+# CI configuration.
+
+dep_ci.erlang.mk = git https://github.com/ninenines/ci.erlang.mk master
+DEP_EARLY_PLUGINS = ci.erlang.mk
+
+AUTO_CI_OTP ?= OTP-19+
+AUTO_CI_HIPE ?= OTP-LATEST
+# AUTO_CI_ERLLVM ?= OTP-LATEST
+AUTO_CI_WINDOWS ?= OTP-19+
+
include erlang.mk
diff --git a/ebin/asciideck.app b/ebin/asciideck.app
index 56ab5f3..8f631cd 100644
--- a/ebin/asciideck.app
+++ b/ebin/asciideck.app
@@ -1,7 +1,7 @@
-{application, asciideck, [
+{application, 'asciideck', [
{description, "Asciidoc for Erlang."},
- {vsn, "0.1.0"},
- {modules, ['asciideck','asciideck_parser','asciideck_to_manpage']},
+ {vsn, "0.2.0"},
+ {modules, ['asciideck','asciideck_attributes_parser','asciideck_attributes_pass','asciideck_block_parser','asciideck_inline_pass','asciideck_line_reader','asciideck_lists_pass','asciideck_tables_pass','asciideck_to_manpage']},
{registered, []},
{applications, [kernel,stdlib]},
{env, []}
diff --git a/erlang.mk b/erlang.mk
index 38bea99..86be4cb 100644
--- a/erlang.mk
+++ b/erlang.mk
@@ -15,8 +15,10 @@
.PHONY: all app apps deps search rel relup docs install-docs check tests clean distclean help erlang-mk
ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST)))
+export ERLANG_MK_FILENAME
-ERLANG_MK_VERSION = 2016.11.03-4-g9e9b7d2
+ERLANG_MK_VERSION = 208a116
+ERLANG_MK_WITHOUT =
# Make 3.81 and 3.82 are deprecated.
@@ -152,9 +154,13 @@ define comma_list
$(subst $(space),$(comma),$(strip $(1)))
endef
+define escape_dquotes
+$(subst ",\",$1)
+endef
+
# Adding erlang.mk to make Erlang scripts who call init:get_plain_arguments() happy.
define erlang
-$(ERL) $(2) -pz $(ERLANG_MK_TMP)/rebar/ebin -eval "$(subst $(newline),,$(subst ",\",$(1)))" -- erlang.mk
+$(ERL) $2 -pz $(ERLANG_MK_TMP)/rebar/ebin -eval "$(subst $(newline),,$(call escape_dquotes,$1))" -- erlang.mk
endef
ifeq ($(PLATFORM),msys2)
@@ -183,19 +189,109 @@ ERLANG_MK_COMMIT ?=
ERLANG_MK_BUILD_CONFIG ?= build.config
ERLANG_MK_BUILD_DIR ?= .erlang.mk.build
+erlang-mk: WITHOUT ?= $(ERLANG_MK_WITHOUT)
erlang-mk:
- git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR)
ifdef ERLANG_MK_COMMIT
+ git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR)
cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT)
+else
+ git clone --depth 1 $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR)
endif
if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi
- $(MAKE) -C $(ERLANG_MK_BUILD_DIR)
+ $(MAKE) -C $(ERLANG_MK_BUILD_DIR) WITHOUT='$(strip $(WITHOUT))'
cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk
rm -rf $(ERLANG_MK_BUILD_DIR)
# The erlang.mk package index is bundled in the default erlang.mk build.
# Search for the string "copyright" to skip to the rest of the code.
+# Copyright (c) 2015-2017, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: distclean-kerl
+
+KERL_INSTALL_DIR ?= $(HOME)/erlang
+
+ifeq ($(strip $(KERL)),)
+KERL := $(ERLANG_MK_TMP)/kerl/kerl
+endif
+
+export KERL
+
+KERL_GIT ?= https://github.com/kerl/kerl
+KERL_COMMIT ?= master
+
+KERL_MAKEFLAGS ?=
+
+OTP_GIT ?= https://github.com/erlang/otp
+
+define kerl_otp_target
+ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(1)),)
+$(KERL_INSTALL_DIR)/$(1): $(KERL)
+ MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $(1) $(1)
+ $(KERL) install $(1) $(KERL_INSTALL_DIR)/$(1)
+endif
+endef
+
+define kerl_hipe_target
+ifeq ($(wildcard $(KERL_INSTALL_DIR)/$1-native),)
+$(KERL_INSTALL_DIR)/$1-native: $(KERL)
+ KERL_CONFIGURE_OPTIONS=--enable-native-libs \
+ MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $1 $1-native
+ $(KERL) install $1-native $(KERL_INSTALL_DIR)/$1-native
+endif
+endef
+
+$(KERL):
+ $(verbose) mkdir -p $(ERLANG_MK_TMP)
+ $(gen_verbose) git clone --depth 1 $(KERL_GIT) $(ERLANG_MK_TMP)/kerl
+ $(verbose) cd $(ERLANG_MK_TMP)/kerl && git checkout $(KERL_COMMIT)
+ $(verbose) chmod +x $(KERL)
+
+distclean:: distclean-kerl
+
+distclean-kerl:
+ $(gen_verbose) rm -rf $(KERL)
+
+# Allow users to select which version of Erlang/OTP to use for a project.
+
+ifneq ($(strip $(LATEST_ERLANG_OTP)),)
+ERLANG_OTP := $(notdir $(lastword $(sort \
+ $(filter-out $(KERL_INSTALL_DIR)/OTP_% %-rc1 %-rc2 %-rc3,\
+ $(wildcard $(KERL_INSTALL_DIR)/*[^-native])))))
+endif
+
+ERLANG_OTP ?=
+ERLANG_HIPE ?=
+
+# Use kerl to enforce a specific Erlang/OTP version for a project.
+ifneq ($(strip $(ERLANG_OTP)),)
+export PATH := $(KERL_INSTALL_DIR)/$(ERLANG_OTP)/bin:$(PATH)
+SHELL := env PATH=$(PATH) $(SHELL)
+$(eval $(call kerl_otp_target,$(ERLANG_OTP)))
+
+# Build Erlang/OTP only if it doesn't already exist.
+ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_OTP))$(BUILD_ERLANG_OTP),)
+$(info Building Erlang/OTP $(ERLANG_OTP)... Please wait...)
+$(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_OTP) ERLANG_OTP=$(ERLANG_OTP) BUILD_ERLANG_OTP=1 >&2)
+endif
+
+else
+# Same for a HiPE enabled VM.
+ifneq ($(strip $(ERLANG_HIPE)),)
+export PATH := $(KERL_INSTALL_DIR)/$(ERLANG_HIPE)-native/bin:$(PATH)
+SHELL := env PATH=$(PATH) $(SHELL)
+$(eval $(call kerl_hipe_target,$(ERLANG_HIPE)))
+
+# Build Erlang/OTP only if it doesn't already exist.
+ifeq ($(wildcard $(KERL_INSTALL_DIR)/$(ERLANG_HIPE))$(BUILD_ERLANG_OTP),)
+$(info Building HiPE-enabled Erlang/OTP $(ERLANG_OTP)... Please wait...)
+$(shell $(MAKE) $(KERL_INSTALL_DIR)/$(ERLANG_HIPE) ERLANG_HIPE=$(ERLANG_HIPE) BUILD_ERLANG_OTP=1 >&2)
+endif
+
+endif
+endif
+
PACKAGES += aberth
pkg_aberth_name = aberth
pkg_aberth_description = Generic BERT-RPC server in Erlang
@@ -404,14 +500,6 @@ pkg_bootstrap_fetch = git
pkg_bootstrap_repo = https://github.com/schlagert/bootstrap
pkg_bootstrap_commit = master
-PACKAGES += boss_db
-pkg_boss_db_name = boss_db
-pkg_boss_db_description = BossDB: a sharded, caching, pooling, evented ORM for Erlang
-pkg_boss_db_homepage = https://github.com/ErlyORM/boss_db
-pkg_boss_db_fetch = git
-pkg_boss_db_repo = https://github.com/ErlyORM/boss_db
-pkg_boss_db_commit = master
-
PACKAGES += boss
pkg_boss_name = boss
pkg_boss_description = Erlang web MVC, now featuring Comet
@@ -420,6 +508,14 @@ pkg_boss_fetch = git
pkg_boss_repo = https://github.com/ChicagoBoss/ChicagoBoss
pkg_boss_commit = master
+PACKAGES += boss_db
+pkg_boss_db_name = boss_db
+pkg_boss_db_description = BossDB: a sharded, caching, pooling, evented ORM for Erlang
+pkg_boss_db_homepage = https://github.com/ErlyORM/boss_db
+pkg_boss_db_fetch = git
+pkg_boss_db_repo = https://github.com/ErlyORM/boss_db
+pkg_boss_db_commit = master
+
PACKAGES += brod
pkg_brod_name = brod
pkg_brod_description = Kafka client in Erlang
@@ -524,13 +620,13 @@ pkg_chumak_fetch = git
pkg_chumak_repo = https://github.com/chovencorp/chumak
pkg_chumak_commit = master
-PACKAGES += classifier
-pkg_classifier_name = classifier
-pkg_classifier_description = An Erlang Bayesian Filter and Text Classifier
-pkg_classifier_homepage = https://github.com/inaka/classifier
-pkg_classifier_fetch = git
-pkg_classifier_repo = https://github.com/inaka/classifier
-pkg_classifier_commit = master
+PACKAGES += cl
+pkg_cl_name = cl
+pkg_cl_description = OpenCL binding for Erlang
+pkg_cl_homepage = https://github.com/tonyrog/cl
+pkg_cl_fetch = git
+pkg_cl_repo = https://github.com/tonyrog/cl
+pkg_cl_commit = master
PACKAGES += clique
pkg_clique_name = clique
@@ -540,14 +636,6 @@ pkg_clique_fetch = git
pkg_clique_repo = https://github.com/basho/clique
pkg_clique_commit = develop
-PACKAGES += cl
-pkg_cl_name = cl
-pkg_cl_description = OpenCL binding for Erlang
-pkg_cl_homepage = https://github.com/tonyrog/cl
-pkg_cl_fetch = git
-pkg_cl_repo = https://github.com/tonyrog/cl
-pkg_cl_commit = master
-
PACKAGES += cloudi_core
pkg_cloudi_core_name = cloudi_core
pkg_cloudi_core_description = CloudI internal service runtime
@@ -564,13 +652,13 @@ pkg_cloudi_service_api_requests_fetch = git
pkg_cloudi_service_api_requests_repo = https://github.com/CloudI/cloudi_service_api_requests
pkg_cloudi_service_api_requests_commit = master
-PACKAGES += cloudi_service_db_cassandra_cql
-pkg_cloudi_service_db_cassandra_cql_name = cloudi_service_db_cassandra_cql
-pkg_cloudi_service_db_cassandra_cql_description = Cassandra CQL CloudI Service
-pkg_cloudi_service_db_cassandra_cql_homepage = http://cloudi.org/
-pkg_cloudi_service_db_cassandra_cql_fetch = git
-pkg_cloudi_service_db_cassandra_cql_repo = https://github.com/CloudI/cloudi_service_db_cassandra_cql
-pkg_cloudi_service_db_cassandra_cql_commit = master
+PACKAGES += cloudi_service_db
+pkg_cloudi_service_db_name = cloudi_service_db
+pkg_cloudi_service_db_description = CloudI Database (in-memory/testing/generic)
+pkg_cloudi_service_db_homepage = http://cloudi.org/
+pkg_cloudi_service_db_fetch = git
+pkg_cloudi_service_db_repo = https://github.com/CloudI/cloudi_service_db
+pkg_cloudi_service_db_commit = master
PACKAGES += cloudi_service_db_cassandra
pkg_cloudi_service_db_cassandra_name = cloudi_service_db_cassandra
@@ -580,6 +668,14 @@ pkg_cloudi_service_db_cassandra_fetch = git
pkg_cloudi_service_db_cassandra_repo = https://github.com/CloudI/cloudi_service_db_cassandra
pkg_cloudi_service_db_cassandra_commit = master
+PACKAGES += cloudi_service_db_cassandra_cql
+pkg_cloudi_service_db_cassandra_cql_name = cloudi_service_db_cassandra_cql
+pkg_cloudi_service_db_cassandra_cql_description = Cassandra CQL CloudI Service
+pkg_cloudi_service_db_cassandra_cql_homepage = http://cloudi.org/
+pkg_cloudi_service_db_cassandra_cql_fetch = git
+pkg_cloudi_service_db_cassandra_cql_repo = https://github.com/CloudI/cloudi_service_db_cassandra_cql
+pkg_cloudi_service_db_cassandra_cql_commit = master
+
PACKAGES += cloudi_service_db_couchdb
pkg_cloudi_service_db_couchdb_name = cloudi_service_db_couchdb
pkg_cloudi_service_db_couchdb_description = CouchDB CloudI Service
@@ -604,14 +700,6 @@ pkg_cloudi_service_db_memcached_fetch = git
pkg_cloudi_service_db_memcached_repo = https://github.com/CloudI/cloudi_service_db_memcached
pkg_cloudi_service_db_memcached_commit = master
-PACKAGES += cloudi_service_db
-pkg_cloudi_service_db_name = cloudi_service_db
-pkg_cloudi_service_db_description = CloudI Database (in-memory/testing/generic)
-pkg_cloudi_service_db_homepage = http://cloudi.org/
-pkg_cloudi_service_db_fetch = git
-pkg_cloudi_service_db_repo = https://github.com/CloudI/cloudi_service_db
-pkg_cloudi_service_db_commit = master
-
PACKAGES += cloudi_service_db_mysql
pkg_cloudi_service_db_mysql_name = cloudi_service_db_mysql
pkg_cloudi_service_db_mysql_description = MySQL CloudI Service
@@ -940,14 +1028,6 @@ pkg_dnssd_fetch = git
pkg_dnssd_repo = https://github.com/benoitc/dnssd_erlang
pkg_dnssd_commit = master
-PACKAGES += dtl
-pkg_dtl_name = dtl
-pkg_dtl_description = Django Template Language: A full-featured port of the Django template engine to Erlang.
-pkg_dtl_homepage = https://github.com/oinksoft/dtl
-pkg_dtl_fetch = git
-pkg_dtl_repo = https://github.com/oinksoft/dtl
-pkg_dtl_commit = master
-
PACKAGES += dynamic_compile
pkg_dynamic_compile_name = dynamic_compile
pkg_dynamic_compile_description = compile and load erlang modules from string input
@@ -1036,14 +1116,6 @@ pkg_edown_fetch = git
pkg_edown_repo = https://github.com/uwiger/edown
pkg_edown_commit = master
-PACKAGES += eep_app
-pkg_eep_app_name = eep_app
-pkg_eep_app_description = Embedded Event Processing
-pkg_eep_app_homepage = https://github.com/darach/eep-erl
-pkg_eep_app_fetch = git
-pkg_eep_app_repo = https://github.com/darach/eep-erl
-pkg_eep_app_commit = master
-
PACKAGES += eep
pkg_eep_name = eep
pkg_eep_description = Erlang Easy Profiling (eep) application provides a way to analyze application performance and call hierarchy
@@ -1052,6 +1124,14 @@ pkg_eep_fetch = git
pkg_eep_repo = https://github.com/virtan/eep
pkg_eep_commit = master
+PACKAGES += eep_app
+pkg_eep_app_name = eep_app
+pkg_eep_app_description = Embedded Event Processing
+pkg_eep_app_homepage = https://github.com/darach/eep-erl
+pkg_eep_app_fetch = git
+pkg_eep_app_repo = https://github.com/darach/eep-erl
+pkg_eep_app_commit = master
+
PACKAGES += efene
pkg_efene_name = efene
pkg_efene_description = Alternative syntax for the Erlang Programming Language focusing on simplicity, ease of use and programmer UX
@@ -1076,14 +1156,6 @@ pkg_ehsa_fetch = hg
pkg_ehsa_repo = https://bitbucket.org/a12n/ehsa
pkg_ehsa_commit = default
-PACKAGES += ejabberd
-pkg_ejabberd_name = ejabberd
-pkg_ejabberd_description = Robust, ubiquitous and massively scalable Jabber / XMPP Instant Messaging platform
-pkg_ejabberd_homepage = https://github.com/processone/ejabberd
-pkg_ejabberd_fetch = git
-pkg_ejabberd_repo = https://github.com/processone/ejabberd
-pkg_ejabberd_commit = master
-
PACKAGES += ej
pkg_ej_name = ej
pkg_ej_description = Helper module for working with Erlang terms representing JSON
@@ -1092,6 +1164,14 @@ pkg_ej_fetch = git
pkg_ej_repo = https://github.com/seth/ej
pkg_ej_commit = master
+PACKAGES += ejabberd
+pkg_ejabberd_name = ejabberd
+pkg_ejabberd_description = Robust, ubiquitous and massively scalable Jabber / XMPP Instant Messaging platform
+pkg_ejabberd_homepage = https://github.com/processone/ejabberd
+pkg_ejabberd_fetch = git
+pkg_ejabberd_repo = https://github.com/processone/ejabberd
+pkg_ejabberd_commit = master
+
PACKAGES += ejwt
pkg_ejwt_name = ejwt
pkg_ejwt_description = erlang library for JSON Web Token
@@ -1252,6 +1332,14 @@ pkg_eredis_pool_fetch = git
pkg_eredis_pool_repo = https://github.com/hiroeorz/eredis_pool
pkg_eredis_pool_commit = master
+PACKAGES += erl_streams
+pkg_erl_streams_name = erl_streams
+pkg_erl_streams_description = Streams in Erlang
+pkg_erl_streams_homepage = https://github.com/epappas/erl_streams
+pkg_erl_streams_fetch = git
+pkg_erl_streams_repo = https://github.com/epappas/erl_streams
+pkg_erl_streams_commit = master
+
PACKAGES += erlang_cep
pkg_erlang_cep_name = erlang_cep
pkg_erlang_cep_description = A basic CEP package written in erlang
@@ -1428,14 +1516,6 @@ pkg_erlport_fetch = git
pkg_erlport_repo = https://github.com/hdima/erlport
pkg_erlport_commit = master
-PACKAGES += erlsha2
-pkg_erlsha2_name = erlsha2
-pkg_erlsha2_description = SHA-224, SHA-256, SHA-384, SHA-512 implemented in Erlang NIFs.
-pkg_erlsha2_homepage = https://github.com/vinoski/erlsha2
-pkg_erlsha2_fetch = git
-pkg_erlsha2_repo = https://github.com/vinoski/erlsha2
-pkg_erlsha2_commit = master
-
PACKAGES += erlsh
pkg_erlsh_name = erlsh
pkg_erlsh_description = Erlang shell tools
@@ -1444,6 +1524,14 @@ pkg_erlsh_fetch = git
pkg_erlsh_repo = https://github.com/proger/erlsh
pkg_erlsh_commit = master
+PACKAGES += erlsha2
+pkg_erlsha2_name = erlsha2
+pkg_erlsha2_description = SHA-224, SHA-256, SHA-384, SHA-512 implemented in Erlang NIFs.
+pkg_erlsha2_homepage = https://github.com/vinoski/erlsha2
+pkg_erlsha2_fetch = git
+pkg_erlsha2_repo = https://github.com/vinoski/erlsha2
+pkg_erlsha2_commit = master
+
PACKAGES += erlsom
pkg_erlsom_name = erlsom
pkg_erlsom_description = XML parser for Erlang
@@ -1452,14 +1540,6 @@ pkg_erlsom_fetch = git
pkg_erlsom_repo = https://github.com/willemdj/erlsom
pkg_erlsom_commit = master
-PACKAGES += erl_streams
-pkg_erl_streams_name = erl_streams
-pkg_erl_streams_description = Streams in Erlang
-pkg_erl_streams_homepage = https://github.com/epappas/erl_streams
-pkg_erl_streams_fetch = git
-pkg_erl_streams_repo = https://github.com/epappas/erl_streams
-pkg_erl_streams_commit = master
-
PACKAGES += erlubi
pkg_erlubi_name = erlubi
pkg_erlubi_description = Ubigraph Erlang Client (and Process Visualizer)
@@ -1516,6 +1596,14 @@ pkg_erwa_fetch = git
pkg_erwa_repo = https://github.com/bwegh/erwa
pkg_erwa_commit = master
+PACKAGES += escalus
+pkg_escalus_name = escalus
+pkg_escalus_description = An XMPP client library in Erlang for conveniently testing XMPP servers
+pkg_escalus_homepage = https://github.com/esl/escalus
+pkg_escalus_fetch = git
+pkg_escalus_repo = https://github.com/esl/escalus
+pkg_escalus_commit = master
+
PACKAGES += espec
pkg_espec_name = espec
pkg_espec_description = ESpec: Behaviour driven development framework for Erlang
@@ -1540,14 +1628,6 @@ pkg_etap_fetch = git
pkg_etap_repo = https://github.com/ngerakines/etap
pkg_etap_commit = master
-PACKAGES += etest_http
-pkg_etest_http_name = etest_http
-pkg_etest_http_description = etest Assertions around HTTP (client-side)
-pkg_etest_http_homepage = https://github.com/wooga/etest_http
-pkg_etest_http_fetch = git
-pkg_etest_http_repo = https://github.com/wooga/etest_http
-pkg_etest_http_commit = master
-
PACKAGES += etest
pkg_etest_name = etest
pkg_etest_description = A lightweight, convention over configuration test framework for Erlang
@@ -1556,6 +1636,14 @@ pkg_etest_fetch = git
pkg_etest_repo = https://github.com/wooga/etest
pkg_etest_commit = master
+PACKAGES += etest_http
+pkg_etest_http_name = etest_http
+pkg_etest_http_description = etest Assertions around HTTP (client-side)
+pkg_etest_http_homepage = https://github.com/wooga/etest_http
+pkg_etest_http_fetch = git
+pkg_etest_http_repo = https://github.com/wooga/etest_http
+pkg_etest_http_commit = master
+
PACKAGES += etoml
pkg_etoml_name = etoml
pkg_etoml_description = TOML language erlang parser
@@ -1564,14 +1652,6 @@ pkg_etoml_fetch = git
pkg_etoml_repo = https://github.com/kalta/etoml
pkg_etoml_commit = master
-PACKAGES += eunit_formatters
-pkg_eunit_formatters_name = eunit_formatters
-pkg_eunit_formatters_description = Because eunit's output sucks. Let's make it better.
-pkg_eunit_formatters_homepage = https://github.com/seancribbs/eunit_formatters
-pkg_eunit_formatters_fetch = git
-pkg_eunit_formatters_repo = https://github.com/seancribbs/eunit_formatters
-pkg_eunit_formatters_commit = master
-
PACKAGES += eunit
pkg_eunit_name = eunit
pkg_eunit_description = The EUnit lightweight unit testing framework for Erlang - this is the canonical development repository.
@@ -1580,6 +1660,14 @@ pkg_eunit_fetch = git
pkg_eunit_repo = https://github.com/richcarl/eunit
pkg_eunit_commit = master
+PACKAGES += eunit_formatters
+pkg_eunit_formatters_name = eunit_formatters
+pkg_eunit_formatters_description = Because eunit's output sucks. Let's make it better.
+pkg_eunit_formatters_homepage = https://github.com/seancribbs/eunit_formatters
+pkg_eunit_formatters_fetch = git
+pkg_eunit_formatters_repo = https://github.com/seancribbs/eunit_formatters
+pkg_eunit_formatters_commit = master
+
PACKAGES += euthanasia
pkg_euthanasia_name = euthanasia
pkg_euthanasia_description = Merciful killer for your Erlang processes
@@ -1716,6 +1804,14 @@ pkg_fn_fetch = git
pkg_fn_repo = https://github.com/reiddraper/fn
pkg_fn_commit = master
+PACKAGES += folsom
+pkg_folsom_name = folsom
+pkg_folsom_description = Expose Erlang Events and Metrics
+pkg_folsom_homepage = https://github.com/boundary/folsom
+pkg_folsom_fetch = git
+pkg_folsom_repo = https://github.com/boundary/folsom
+pkg_folsom_commit = master
+
PACKAGES += folsom_cowboy
pkg_folsom_cowboy_name = folsom_cowboy
pkg_folsom_cowboy_description = A Cowboy based Folsom HTTP Wrapper.
@@ -1732,14 +1828,6 @@ pkg_folsomite_fetch = git
pkg_folsomite_repo = https://github.com/campanja/folsomite
pkg_folsomite_commit = master
-PACKAGES += folsom
-pkg_folsom_name = folsom
-pkg_folsom_description = Expose Erlang Events and Metrics
-pkg_folsom_homepage = https://github.com/boundary/folsom
-pkg_folsom_fetch = git
-pkg_folsom_repo = https://github.com/boundary/folsom
-pkg_folsom_commit = master
-
PACKAGES += fs
pkg_fs_name = fs
pkg_fs_description = Erlang FileSystem Listener
@@ -1908,14 +1996,6 @@ pkg_gold_fever_fetch = git
pkg_gold_fever_repo = https://github.com/inaka/gold_fever
pkg_gold_fever_commit = master
-PACKAGES += gossiperl
-pkg_gossiperl_name = gossiperl
-pkg_gossiperl_description = Gossip middleware in Erlang
-pkg_gossiperl_homepage = http://gossiperl.com/
-pkg_gossiperl_fetch = git
-pkg_gossiperl_repo = https://github.com/gossiperl/gossiperl
-pkg_gossiperl_commit = master
-
PACKAGES += gpb
pkg_gpb_name = gpb
pkg_gpb_description = A Google Protobuf implementation for Erlang
@@ -1940,6 +2020,22 @@ pkg_grapherl_fetch = git
pkg_grapherl_repo = https://github.com/eproxus/grapherl
pkg_grapherl_commit = master
+PACKAGES += grpc
+pkg_grpc_name = grpc
+pkg_grpc_description = gRPC server in Erlang
+pkg_grpc_homepage = https://github.com/Bluehouse-Technology/grpc
+pkg_grpc_fetch = git
+pkg_grpc_repo = https://github.com/Bluehouse-Technology/grpc
+pkg_grpc_commit = master
+
+PACKAGES += grpc_client
+pkg_grpc_client_name = grpc_client
+pkg_grpc_client_description = gRPC client in Erlang
+pkg_grpc_client_homepage = https://github.com/Bluehouse-Technology/grpc_client
+pkg_grpc_client_fetch = git
+pkg_grpc_client_repo = https://github.com/Bluehouse-Technology/grpc_client
+pkg_grpc_client_commit = master
+
PACKAGES += gun
pkg_gun_name = gun
pkg_gun_description = Asynchronous SPDY, HTTP and Websocket client written in Erlang.
@@ -2020,6 +2116,14 @@ pkg_ibrowse_fetch = git
pkg_ibrowse_repo = https://github.com/cmullaparthi/ibrowse
pkg_ibrowse_commit = master
+PACKAGES += idna
+pkg_idna_name = idna
+pkg_idna_description = Erlang IDNA lib
+pkg_idna_homepage = https://github.com/benoitc/erlang-idna
+pkg_idna_fetch = git
+pkg_idna_repo = https://github.com/benoitc/erlang-idna
+pkg_idna_commit = master
+
PACKAGES += ierlang
pkg_ierlang_name = ierlang
pkg_ierlang_description = An Erlang language kernel for IPython.
@@ -2036,14 +2140,6 @@ pkg_iota_fetch = git
pkg_iota_repo = https://github.com/jpgneves/iota
pkg_iota_commit = master
-PACKAGES += ircd
-pkg_ircd_name = ircd
-pkg_ircd_description = A pluggable IRC daemon application/library for Erlang.
-pkg_ircd_homepage = https://github.com/tonyg/erlang-ircd
-pkg_ircd_fetch = git
-pkg_ircd_repo = https://github.com/tonyg/erlang-ircd
-pkg_ircd_commit = master
-
PACKAGES += irc_lib
pkg_irc_lib_name = irc_lib
pkg_irc_lib_description = Erlang irc client library
@@ -2052,6 +2148,14 @@ pkg_irc_lib_fetch = git
pkg_irc_lib_repo = https://github.com/OtpChatBot/irc_lib
pkg_irc_lib_commit = master
+PACKAGES += ircd
+pkg_ircd_name = ircd
+pkg_ircd_description = A pluggable IRC daemon application/library for Erlang.
+pkg_ircd_homepage = https://github.com/tonyg/erlang-ircd
+pkg_ircd_fetch = git
+pkg_ircd_repo = https://github.com/tonyg/erlang-ircd
+pkg_ircd_commit = master
+
PACKAGES += iris
pkg_iris_name = iris
pkg_iris_description = Iris Erlang binding
@@ -2124,6 +2228,22 @@ pkg_joxa_fetch = git
pkg_joxa_repo = https://github.com/joxa/joxa
pkg_joxa_commit = master
+PACKAGES += json
+pkg_json_name = json
+pkg_json_description = a high level json library for erlang (17.0+)
+pkg_json_homepage = https://github.com/talentdeficit/json
+pkg_json_fetch = git
+pkg_json_repo = https://github.com/talentdeficit/json
+pkg_json_commit = master
+
+PACKAGES += json_rec
+pkg_json_rec_name = json_rec
+pkg_json_rec_description = JSON to erlang record
+pkg_json_rec_homepage = https://github.com/justinkirby/json_rec
+pkg_json_rec_fetch = git
+pkg_json_rec_repo = https://github.com/justinkirby/json_rec
+pkg_json_rec_commit = master
+
PACKAGES += jsone
pkg_jsone_name = jsone
pkg_jsone_description = An Erlang library for encoding, decoding JSON data.
@@ -2140,14 +2260,6 @@ pkg_jsonerl_fetch = git
pkg_jsonerl_repo = https://github.com/lambder/jsonerl
pkg_jsonerl_commit = master
-PACKAGES += json
-pkg_json_name = json
-pkg_json_description = a high level json library for erlang (17.0+)
-pkg_json_homepage = https://github.com/talentdeficit/json
-pkg_json_fetch = git
-pkg_json_repo = https://github.com/talentdeficit/json
-pkg_json_commit = master
-
PACKAGES += jsonpath
pkg_jsonpath_name = jsonpath
pkg_jsonpath_description = Fast Erlang JSON data retrieval and updates via javascript-like notation
@@ -2156,14 +2268,6 @@ pkg_jsonpath_fetch = git
pkg_jsonpath_repo = https://github.com/GeneStevens/jsonpath
pkg_jsonpath_commit = master
-PACKAGES += json_rec
-pkg_json_rec_name = json_rec
-pkg_json_rec_description = JSON to erlang record
-pkg_json_rec_homepage = https://github.com/justinkirby/json_rec
-pkg_json_rec_fetch = git
-pkg_json_rec_repo = https://github.com/justinkirby/json_rec
-pkg_json_rec_commit = master
-
PACKAGES += jsonx
pkg_jsonx_name = jsonx
pkg_jsonx_description = JSONX is an Erlang library for efficient decode and encode JSON, written in C.
@@ -2292,6 +2396,14 @@ pkg_kvs_fetch = git
pkg_kvs_repo = https://github.com/synrc/kvs
pkg_kvs_commit = master
+PACKAGES += lager
+pkg_lager_name = lager
+pkg_lager_description = A logging framework for Erlang/OTP.
+pkg_lager_homepage = https://github.com/erlang-lager/lager
+pkg_lager_fetch = git
+pkg_lager_repo = https://github.com/erlang-lager/lager
+pkg_lager_commit = master
+
PACKAGES += lager_amqp_backend
pkg_lager_amqp_backend_name = lager_amqp_backend
pkg_lager_amqp_backend_description = AMQP RabbitMQ Lager backend
@@ -2300,20 +2412,12 @@ pkg_lager_amqp_backend_fetch = git
pkg_lager_amqp_backend_repo = https://github.com/jbrisbin/lager_amqp_backend
pkg_lager_amqp_backend_commit = master
-PACKAGES += lager
-pkg_lager_name = lager
-pkg_lager_description = A logging framework for Erlang/OTP.
-pkg_lager_homepage = https://github.com/basho/lager
-pkg_lager_fetch = git
-pkg_lager_repo = https://github.com/basho/lager
-pkg_lager_commit = master
-
PACKAGES += lager_syslog
pkg_lager_syslog_name = lager_syslog
pkg_lager_syslog_description = Syslog backend for lager
-pkg_lager_syslog_homepage = https://github.com/basho/lager_syslog
+pkg_lager_syslog_homepage = https://github.com/erlang-lager/lager_syslog
pkg_lager_syslog_fetch = git
-pkg_lager_syslog_repo = https://github.com/basho/lager_syslog
+pkg_lager_syslog_repo = https://github.com/erlang-lager/lager_syslog
pkg_lager_syslog_commit = master
PACKAGES += lambdapad
@@ -2484,6 +2588,14 @@ pkg_mavg_fetch = git
pkg_mavg_repo = https://github.com/EchoTeam/mavg
pkg_mavg_commit = master
+PACKAGES += mc_erl
+pkg_mc_erl_name = mc_erl
+pkg_mc_erl_description = mc-erl is a server for Minecraft 1.4.7 written in Erlang.
+pkg_mc_erl_homepage = https://github.com/clonejo/mc-erl
+pkg_mc_erl_fetch = git
+pkg_mc_erl_repo = https://github.com/clonejo/mc-erl
+pkg_mc_erl_commit = master
+
PACKAGES += mcd
pkg_mcd_name = mcd
pkg_mcd_description = Fast memcached protocol client in pure Erlang
@@ -2500,14 +2612,6 @@ pkg_mcerlang_fetch = git
pkg_mcerlang_repo = https://github.com/fredlund/McErlang
pkg_mcerlang_commit = master
-PACKAGES += mc_erl
-pkg_mc_erl_name = mc_erl
-pkg_mc_erl_description = mc-erl is a server for Minecraft 1.4.7 written in Erlang.
-pkg_mc_erl_homepage = https://github.com/clonejo/mc-erl
-pkg_mc_erl_fetch = git
-pkg_mc_erl_repo = https://github.com/clonejo/mc-erl
-pkg_mc_erl_commit = master
-
PACKAGES += meck
pkg_meck_name = meck
pkg_meck_description = A mocking library for Erlang
@@ -2772,6 +2876,14 @@ pkg_nprocreg_fetch = git
pkg_nprocreg_repo = https://github.com/nitrogen/nprocreg
pkg_nprocreg_commit = master
+PACKAGES += oauth
+pkg_oauth_name = oauth
+pkg_oauth_description = An Erlang OAuth 1.0 implementation
+pkg_oauth_homepage = https://github.com/tim/erlang-oauth
+pkg_oauth_fetch = git
+pkg_oauth_repo = https://github.com/tim/erlang-oauth
+pkg_oauth_commit = master
+
PACKAGES += oauth2
pkg_oauth2_name = oauth2
pkg_oauth2_description = Erlang Oauth2 implementation
@@ -2780,13 +2892,13 @@ pkg_oauth2_fetch = git
pkg_oauth2_repo = https://github.com/kivra/oauth2
pkg_oauth2_commit = master
-PACKAGES += oauth
-pkg_oauth_name = oauth
-pkg_oauth_description = An Erlang OAuth 1.0 implementation
-pkg_oauth_homepage = https://github.com/tim/erlang-oauth
-pkg_oauth_fetch = git
-pkg_oauth_repo = https://github.com/tim/erlang-oauth
-pkg_oauth_commit = master
+PACKAGES += observer_cli
+pkg_observer_cli_name = observer_cli
+pkg_observer_cli_description = Visualize Erlang/Elixir Nodes On The Command Line
+pkg_observer_cli_homepage = http://zhongwencool.github.io/observer_cli
+pkg_observer_cli_fetch = git
+pkg_observer_cli_repo = https://github.com/zhongwencool/observer_cli
+pkg_observer_cli_commit = master
PACKAGES += octopus
pkg_octopus_name = octopus
@@ -2836,6 +2948,14 @@ pkg_openpoker_fetch = git
pkg_openpoker_repo = https://github.com/hpyhacking/openpoker
pkg_openpoker_commit = master
+PACKAGES += otpbp
+pkg_otpbp_name = otpbp
+pkg_otpbp_description = Parse transformer for use new OTP functions in old Erlang/OTP releases (R15, R16, 17, 18, 19)
+pkg_otpbp_homepage = https://github.com/Ledest/otpbp
+pkg_otpbp_fetch = git
+pkg_otpbp_repo = https://github.com/Ledest/otpbp
+pkg_otpbp_commit = master
+
PACKAGES += pal
pkg_pal_name = pal
pkg_pal_description = Pragmatic Authentication Library
@@ -2972,14 +3092,6 @@ pkg_procket_fetch = git
pkg_procket_repo = https://github.com/msantos/procket
pkg_procket_commit = master
-PACKAGES += proper
-pkg_proper_name = proper
-pkg_proper_description = PropEr: a QuickCheck-inspired property-based testing tool for Erlang.
-pkg_proper_homepage = http://proper.softlab.ntua.gr
-pkg_proper_fetch = git
-pkg_proper_repo = https://github.com/manopapad/proper
-pkg_proper_commit = master
-
PACKAGES += prop
pkg_prop_name = prop
pkg_prop_description = An Erlang code scaffolding and generator system.
@@ -2988,6 +3100,14 @@ pkg_prop_fetch = git
pkg_prop_repo = https://github.com/nuex/prop
pkg_prop_commit = master
+PACKAGES += proper
+pkg_proper_name = proper
+pkg_proper_description = PropEr: a QuickCheck-inspired property-based testing tool for Erlang.
+pkg_proper_homepage = http://proper.softlab.ntua.gr
+pkg_proper_fetch = git
+pkg_proper_repo = https://github.com/manopapad/proper
+pkg_proper_commit = master
+
PACKAGES += props
pkg_props_name = props
pkg_props_description = Property structure library
@@ -3060,14 +3180,6 @@ pkg_quickrand_fetch = git
pkg_quickrand_repo = https://github.com/okeuday/quickrand
pkg_quickrand_commit = master
-PACKAGES += rabbit_exchange_type_riak
-pkg_rabbit_exchange_type_riak_name = rabbit_exchange_type_riak
-pkg_rabbit_exchange_type_riak_description = Custom RabbitMQ exchange type for sticking messages in Riak
-pkg_rabbit_exchange_type_riak_homepage = https://github.com/jbrisbin/riak-exchange
-pkg_rabbit_exchange_type_riak_fetch = git
-pkg_rabbit_exchange_type_riak_repo = https://github.com/jbrisbin/riak-exchange
-pkg_rabbit_exchange_type_riak_commit = master
-
PACKAGES += rabbit
pkg_rabbit_name = rabbit
pkg_rabbit_description = RabbitMQ Server
@@ -3076,6 +3188,14 @@ pkg_rabbit_fetch = git
pkg_rabbit_repo = https://github.com/rabbitmq/rabbitmq-server.git
pkg_rabbit_commit = master
+PACKAGES += rabbit_exchange_type_riak
+pkg_rabbit_exchange_type_riak_name = rabbit_exchange_type_riak
+pkg_rabbit_exchange_type_riak_description = Custom RabbitMQ exchange type for sticking messages in Riak
+pkg_rabbit_exchange_type_riak_homepage = https://github.com/jbrisbin/riak-exchange
+pkg_rabbit_exchange_type_riak_fetch = git
+pkg_rabbit_exchange_type_riak_repo = https://github.com/jbrisbin/riak-exchange
+pkg_rabbit_exchange_type_riak_commit = master
+
PACKAGES += rack
pkg_rack_name = rack
pkg_rack_description = Rack handler for erlang
@@ -3220,14 +3340,6 @@ pkg_rfc4627_jsonrpc_fetch = git
pkg_rfc4627_jsonrpc_repo = https://github.com/tonyg/erlang-rfc4627
pkg_rfc4627_jsonrpc_commit = master
-PACKAGES += riakc
-pkg_riakc_name = riakc
-pkg_riakc_description = Erlang clients for Riak.
-pkg_riakc_homepage = https://github.com/basho/riak-erlang-client
-pkg_riakc_fetch = git
-pkg_riakc_repo = https://github.com/basho/riak-erlang-client
-pkg_riakc_commit = master
-
PACKAGES += riak_control
pkg_riak_control_name = riak_control
pkg_riak_control_description = Webmachine-based administration interface for Riak.
@@ -3260,14 +3372,6 @@ pkg_riak_ensemble_fetch = git
pkg_riak_ensemble_repo = https://github.com/basho/riak_ensemble
pkg_riak_ensemble_commit = master
-PACKAGES += riakhttpc
-pkg_riakhttpc_name = riakhttpc
-pkg_riakhttpc_description = Riak Erlang client using the HTTP interface
-pkg_riakhttpc_homepage = https://github.com/basho/riak-erlang-http-client
-pkg_riakhttpc_fetch = git
-pkg_riakhttpc_repo = https://github.com/basho/riak-erlang-http-client
-pkg_riakhttpc_commit = master
-
PACKAGES += riak_kv
pkg_riak_kv_name = riak_kv
pkg_riak_kv_description = Riak Key/Value Store
@@ -3276,14 +3380,6 @@ pkg_riak_kv_fetch = git
pkg_riak_kv_repo = https://github.com/basho/riak_kv
pkg_riak_kv_commit = master
-PACKAGES += riaknostic
-pkg_riaknostic_name = riaknostic
-pkg_riaknostic_description = A diagnostic tool for Riak installations, to find common errors asap
-pkg_riaknostic_homepage = https://github.com/basho/riaknostic
-pkg_riaknostic_fetch = git
-pkg_riaknostic_repo = https://github.com/basho/riaknostic
-pkg_riaknostic_commit = master
-
PACKAGES += riak_pg
pkg_riak_pg_name = riak_pg
pkg_riak_pg_description = Distributed process groups with riak_core.
@@ -3300,14 +3396,6 @@ pkg_riak_pipe_fetch = git
pkg_riak_pipe_repo = https://github.com/basho/riak_pipe
pkg_riak_pipe_commit = master
-PACKAGES += riakpool
-pkg_riakpool_name = riakpool
-pkg_riakpool_description = erlang riak client pool
-pkg_riakpool_homepage = https://github.com/dweldon/riakpool
-pkg_riakpool_fetch = git
-pkg_riakpool_repo = https://github.com/dweldon/riakpool
-pkg_riakpool_commit = master
-
PACKAGES += riak_sysmon
pkg_riak_sysmon_name = riak_sysmon
pkg_riak_sysmon_description = Simple OTP app for managing Erlang VM system_monitor event messages
@@ -3324,6 +3412,38 @@ pkg_riak_test_fetch = git
pkg_riak_test_repo = https://github.com/basho/riak_test
pkg_riak_test_commit = master
+PACKAGES += riakc
+pkg_riakc_name = riakc
+pkg_riakc_description = Erlang clients for Riak.
+pkg_riakc_homepage = https://github.com/basho/riak-erlang-client
+pkg_riakc_fetch = git
+pkg_riakc_repo = https://github.com/basho/riak-erlang-client
+pkg_riakc_commit = master
+
+PACKAGES += riakhttpc
+pkg_riakhttpc_name = riakhttpc
+pkg_riakhttpc_description = Riak Erlang client using the HTTP interface
+pkg_riakhttpc_homepage = https://github.com/basho/riak-erlang-http-client
+pkg_riakhttpc_fetch = git
+pkg_riakhttpc_repo = https://github.com/basho/riak-erlang-http-client
+pkg_riakhttpc_commit = master
+
+PACKAGES += riaknostic
+pkg_riaknostic_name = riaknostic
+pkg_riaknostic_description = A diagnostic tool for Riak installations, to find common errors asap
+pkg_riaknostic_homepage = https://github.com/basho/riaknostic
+pkg_riaknostic_fetch = git
+pkg_riaknostic_repo = https://github.com/basho/riaknostic
+pkg_riaknostic_commit = master
+
+PACKAGES += riakpool
+pkg_riakpool_name = riakpool
+pkg_riakpool_description = erlang riak client pool
+pkg_riakpool_homepage = https://github.com/dweldon/riakpool
+pkg_riakpool_fetch = git
+pkg_riakpool_repo = https://github.com/dweldon/riakpool
+pkg_riakpool_commit = master
+
PACKAGES += rivus_cep
pkg_rivus_cep_name = rivus_cep
pkg_rivus_cep_description = Complex event processing in Erlang
@@ -3604,6 +3724,14 @@ pkg_stripe_fetch = git
pkg_stripe_repo = https://github.com/mattsta/stripe-erlang
pkg_stripe_commit = v1
+PACKAGES += subproc
+pkg_subproc_name = subproc
+pkg_subproc_description = unix subprocess manager with {active,once|false} modes
+pkg_subproc_homepage = http://dozzie.jarowit.net/trac/wiki/subproc
+pkg_subproc_fetch = git
+pkg_subproc_repo = https://github.com/dozzie/subproc
+pkg_subproc_commit = v0.1.0
+
PACKAGES += supervisor3
pkg_supervisor3_name = supervisor3
pkg_supervisor3_description = OTP supervisor with additional strategies
@@ -3644,14 +3772,6 @@ pkg_switchboard_fetch = git
pkg_switchboard_repo = https://github.com/thusfresh/switchboard
pkg_switchboard_commit = master
-PACKAGES += sync
-pkg_sync_name = sync
-pkg_sync_description = On-the-fly recompiling and reloading in Erlang.
-pkg_sync_homepage = https://github.com/rustyio/sync
-pkg_sync_fetch = git
-pkg_sync_repo = https://github.com/rustyio/sync
-pkg_sync_commit = master
-
PACKAGES += syn
pkg_syn_name = syn
pkg_syn_description = A global Process Registry and Process Group manager for Erlang.
@@ -3660,6 +3780,14 @@ pkg_syn_fetch = git
pkg_syn_repo = https://github.com/ostinelli/syn
pkg_syn_commit = master
+PACKAGES += sync
+pkg_sync_name = sync
+pkg_sync_description = On-the-fly recompiling and reloading in Erlang.
+pkg_sync_homepage = https://github.com/rustyio/sync
+pkg_sync_fetch = git
+pkg_sync_repo = https://github.com/rustyio/sync
+pkg_sync_commit = master
+
PACKAGES += syntaxerl
pkg_syntaxerl_name = syntaxerl
pkg_syntaxerl_description = Syntax checker for Erlang
@@ -3732,6 +3860,14 @@ pkg_tirerl_fetch = git
pkg_tirerl_repo = https://github.com/inaka/tirerl
pkg_tirerl_commit = master
+PACKAGES += toml
+pkg_toml_name = toml
+pkg_toml_description = TOML (0.4.0) config parser
+pkg_toml_homepage = http://dozzie.jarowit.net/trac/wiki/TOML
+pkg_toml_fetch = git
+pkg_toml_repo = https://github.com/dozzie/toml
+pkg_toml_commit = v0.2.0
+
PACKAGES += traffic_tools
pkg_traffic_tools_name = traffic_tools
pkg_traffic_tools_description = Simple traffic limiting library
@@ -3775,9 +3911,9 @@ pkg_trie_commit = master
PACKAGES += triq
pkg_triq_name = triq
pkg_triq_description = Trifork QuickCheck
-pkg_triq_homepage = https://github.com/krestenkrab/triq
+pkg_triq_homepage = https://triq.gitlab.io
pkg_triq_fetch = git
-pkg_triq_repo = https://github.com/krestenkrab/triq
+pkg_triq_repo = https://gitlab.com/triq/triq.git
pkg_triq_commit = master
PACKAGES += tunctl
@@ -4012,14 +4148,6 @@ pkg_yaws_fetch = git
pkg_yaws_repo = https://github.com/klacke/yaws
pkg_yaws_commit = master
-PACKAGES += zabbix_sender
-pkg_zabbix_sender_name = zabbix_sender
-pkg_zabbix_sender_description = Zabbix trapper for sending data to Zabbix in pure Erlang
-pkg_zabbix_sender_homepage = https://github.com/stalkermn/zabbix_sender
-pkg_zabbix_sender_fetch = git
-pkg_zabbix_sender_repo = https://github.com/stalkermn/zabbix_sender.git
-pkg_zabbix_sender_commit = master
-
PACKAGES += zab_engine
pkg_zab_engine_name = zab_engine
pkg_zab_engine_description = zab propotocol implement by erlang
@@ -4028,6 +4156,14 @@ pkg_zab_engine_fetch = git
pkg_zab_engine_repo = https://github.com/xinmingyao/zab_engine
pkg_zab_engine_commit = master
+PACKAGES += zabbix_sender
+pkg_zabbix_sender_name = zabbix_sender
+pkg_zabbix_sender_description = Zabbix trapper for sending data to Zabbix in pure Erlang
+pkg_zabbix_sender_homepage = https://github.com/stalkermn/zabbix_sender
+pkg_zabbix_sender_fetch = git
+pkg_zabbix_sender_repo = https://github.com/stalkermn/zabbix_sender.git
+pkg_zabbix_sender_commit = master
+
PACKAGES += zeta
pkg_zeta_name = zeta
pkg_zeta_description = HTTP access log parser in Erlang
@@ -4098,7 +4234,7 @@ endif
# Copyright (c) 2013-2016, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
-.PHONY: distclean-deps
+.PHONY: distclean-deps clean-tmp-deps.log
# Configuration.
@@ -4118,11 +4254,32 @@ export DEPS_DIR
REBAR_DEPS_DIR = $(DEPS_DIR)
export REBAR_DEPS_DIR
+# External "early" plugins (see core/plugins.mk for regular plugins).
+# They both use the core_dep_plugin macro.
+
+define core_dep_plugin
+ifeq ($(2),$(PROJECT))
+-include $$(patsubst $(PROJECT)/%,%,$(1))
+else
+-include $(DEPS_DIR)/$(1)
+
+$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ;
+endif
+endef
+
+DEP_EARLY_PLUGINS ?=
+
+$(foreach p,$(DEP_EARLY_PLUGINS),\
+ $(eval $(if $(findstring /,$p),\
+ $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\
+ $(call core_dep_plugin,$p/early-plugins.mk,$p))))
+
dep_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1)))
dep_repo = $(patsubst git://github.com/%,https://github.com/%, \
$(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo)))
dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit)))
+LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$(a)),$(APPS_DIR)/$(a)))
ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d)))
ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call dep_name,$(dep))))
@@ -4139,16 +4296,13 @@ export NO_AUTOPATCH
# Verbosity.
-dep_verbose_0 = @echo " DEP " $(1);
+dep_verbose_0 = @echo " DEP $1 ($(call dep_commit,$1))";
dep_verbose_2 = set -x;
dep_verbose = $(dep_verbose_$(V))
# Core targets.
-ifdef IS_APP
-apps::
-else
-apps:: $(ALL_APPS_DIRS)
+apps:: $(ALL_APPS_DIRS) clean-tmp-deps.log
ifeq ($(IS_APP)$(IS_DEP),)
$(verbose) rm -f $(ERLANG_MK_TMP)/apps.log
endif
@@ -4156,45 +4310,47 @@ endif
# Create ebin directory for all apps to make sure Erlang recognizes them
# as proper OTP applications when using -include_lib. This is a temporary
# fix, a proper fix would be to compile apps/* in the right order.
- $(verbose) for dep in $(ALL_APPS_DIRS) ; do \
- mkdir -p $$dep/ebin || exit $$?; \
+ifndef IS_APP
+ $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \
+ mkdir -p $$dep/ebin; \
done
- $(verbose) for dep in $(ALL_APPS_DIRS) ; do \
+endif
+# at the toplevel: if LOCAL_DEPS is defined with at least one local app, only
+# compile that list of apps. otherwise, compile everything.
+# within an app: compile all LOCAL_DEPS that are (uncompiled) local apps
+ $(verbose) set -e; for dep in $(if $(LOCAL_DEPS_DIRS)$(IS_APP),$(LOCAL_DEPS_DIRS),$(ALL_APPS_DIRS)) ; do \
if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/apps.log; then \
:; \
else \
echo $$dep >> $(ERLANG_MK_TMP)/apps.log; \
- $(MAKE) -C $$dep IS_APP=1 || exit $$?; \
+ $(MAKE) -C $$dep IS_APP=1; \
fi \
done
+
+clean-tmp-deps.log:
+ifeq ($(IS_APP)$(IS_DEP),)
+ $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log
endif
ifneq ($(SKIP_DEPS),)
deps::
else
-ifeq ($(ALL_DEPS_DIRS),)
-deps:: apps
-else
-deps:: $(ALL_DEPS_DIRS) apps
-ifeq ($(IS_APP)$(IS_DEP),)
- $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log
-endif
+deps:: $(ALL_DEPS_DIRS) apps clean-tmp-deps.log
$(verbose) mkdir -p $(ERLANG_MK_TMP)
- $(verbose) for dep in $(ALL_DEPS_DIRS) ; do \
+ $(verbose) set -e; for dep in $(ALL_DEPS_DIRS) ; do \
if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \
:; \
else \
echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \
if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \
- $(MAKE) -C $$dep IS_DEP=1 || exit $$?; \
+ $(MAKE) -C $$dep IS_DEP=1; \
else \
- echo "Error: No Makefile to build dependency $$dep."; \
+ echo "Error: No Makefile to build dependency $$dep." >&2; \
exit 2; \
fi \
fi \
done
endif
-endif
# Deps related targets.
@@ -4203,17 +4359,18 @@ endif
# in practice only Makefile is needed so far.
define dep_autopatch
if [ -f $(DEPS_DIR)/$(1)/erlang.mk ]; then \
+ rm -rf $(DEPS_DIR)/$1/ebin/; \
$(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \
$(call dep_autopatch_erlang_mk,$(1)); \
elif [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \
- if [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \
+ if [ -f $(DEPS_DIR)/$1/rebar.lock ]; then \
+ $(call dep_autopatch2,$1); \
+ elif [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \
$(call dep_autopatch2,$(1)); \
- elif [ 0 != `grep -ci rebar $(DEPS_DIR)/$(1)/Makefile` ]; then \
+ elif [ 0 != `grep -ci "^[^#].*rebar" $(DEPS_DIR)/$(1)/Makefile` ]; then \
$(call dep_autopatch2,$(1)); \
- elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i rebar '{}' \;`" ]; then \
+ elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;`" ]; then \
$(call dep_autopatch2,$(1)); \
- else \
- $(call erlang,$(call dep_autopatch_app.erl,$(1))); \
fi \
else \
if [ ! -d $(DEPS_DIR)/$(1)/src/ ]; then \
@@ -4225,11 +4382,14 @@ define dep_autopatch
endef
define dep_autopatch2
+ ! test -f $(DEPS_DIR)/$1/ebin/$1.app || \
+ mv -n $(DEPS_DIR)/$1/ebin/$1.app $(DEPS_DIR)/$1/src/$1.app.src; \
+ rm -f $(DEPS_DIR)/$1/ebin/$1.app; \
if [ -f $(DEPS_DIR)/$1/src/$1.app.src.script ]; then \
$(call erlang,$(call dep_autopatch_appsrc_script.erl,$(1))); \
fi; \
$(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \
- if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script ]; then \
+ if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script -o -f $(DEPS_DIR)/$1/rebar.lock ]; then \
$(call dep_autopatch_fetch_rebar); \
$(call dep_autopatch_rebar,$(1)); \
else \
@@ -4241,11 +4401,15 @@ define dep_autopatch_noop
printf "noop:\n" > $(DEPS_DIR)/$(1)/Makefile
endef
-# Overwrite erlang.mk with the current file by default.
+# Replace "include erlang.mk" with a line that will load the parent Erlang.mk
+# if given. Do it for all 3 possible Makefile file names.
ifeq ($(NO_AUTOPATCH_ERLANG_MK),)
define dep_autopatch_erlang_mk
- echo "include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk" \
- > $(DEPS_DIR)/$1/erlang.mk
+ for f in Makefile makefile GNUmakefile; do \
+ if [ -f $(DEPS_DIR)/$1/$$f ]; then \
+ sed -i.bak s/'include *erlang.mk'/'include $$(if $$(ERLANG_MK_FILENAME),$$(ERLANG_MK_FILENAME),erlang.mk)'/ $(DEPS_DIR)/$1/$$f; \
+ fi \
+ done
endef
else
define dep_autopatch_erlang_mk
@@ -4310,6 +4474,10 @@ define dep_autopatch_rebar.erl
Write("C_SRC_TYPE = rebar\n"),
Write("DRV_CFLAGS = -fPIC\nexport DRV_CFLAGS\n"),
Write(["ERLANG_ARCH = ", rebar_utils:wordsize(), "\nexport ERLANG_ARCH\n"]),
+ ToList = fun
+ (V) when is_atom(V) -> atom_to_list(V);
+ (V) when is_list(V) -> "'\\"" ++ V ++ "\\"'"
+ end,
fun() ->
Write("ERLC_OPTS = +debug_info\nexport ERLC_OPTS\n"),
case lists:keyfind(erl_opts, 1, Conf) of
@@ -4317,26 +4485,50 @@ define dep_autopatch_rebar.erl
{_, ErlOpts} ->
lists:foreach(fun
({d, D}) ->
- Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n");
+ Write("ERLC_OPTS += -D" ++ ToList(D) ++ "=1\n");
+ ({d, DKey, DVal}) ->
+ Write("ERLC_OPTS += -D" ++ ToList(DKey) ++ "=" ++ ToList(DVal) ++ "\n");
({i, I}) ->
Write(["ERLC_OPTS += -I ", I, "\n"]);
({platform_define, Regex, D}) ->
case rebar_utils:is_arch(Regex) of
- true -> Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n");
+ true -> Write("ERLC_OPTS += -D" ++ ToList(D) ++ "=1\n");
false -> ok
end;
({parse_transform, PT}) ->
- Write("ERLC_OPTS += +'{parse_transform, " ++ atom_to_list(PT) ++ "}'\n");
+ Write("ERLC_OPTS += +'{parse_transform, " ++ ToList(PT) ++ "}'\n");
(_) -> ok
end, ErlOpts)
end,
Write("\n")
end(),
+ GetHexVsn = fun(N) ->
+ case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.lock)") of
+ {ok, Lock} ->
+ io:format("~p~n", [Lock]),
+ case lists:keyfind("1.1.0", 1, Lock) of
+ {_, LockPkgs} ->
+ io:format("~p~n", [LockPkgs]),
+ case lists:keyfind(atom_to_binary(N, latin1), 1, LockPkgs) of
+ {_, {pkg, _, Vsn}, _} ->
+ io:format("~p~n", [Vsn]),
+ {N, {hex, binary_to_list(Vsn)}};
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end;
+ _ ->
+ false
+ end
+ end,
fun() ->
File = case lists:keyfind(deps, 1, Conf) of
false -> [];
{_, Deps} ->
[begin case case Dep of
+ N when is_atom(N) -> GetHexVsn(N);
{N, S} when is_atom(N), is_list(S) -> {N, {hex, S}};
{N, S} when is_tuple(S) -> {N, S};
{N, _, S} -> {N, S};
@@ -4373,7 +4565,8 @@ define dep_autopatch_rebar.erl
Write("\npre-deps::\n"),
Write("\npre-app::\n"),
PatchHook = fun(Cmd) ->
- case Cmd of
+ Cmd2 = re:replace(Cmd, "^([g]?make)(.*)( -C.*)", "\\\\1\\\\3\\\\2", [{return, list}]),
+ case Cmd2 of
"make -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1);
"gmake -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1);
"make " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1);
@@ -4488,7 +4681,7 @@ define dep_autopatch_rebar.erl
end,
[PortSpec(S) || S <- PortSpecs]
end,
- Write("\ninclude $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk"),
+ Write("\ninclude $$\(if $$\(ERLANG_MK_FILENAME),$$\(ERLANG_MK_FILENAME),erlang.mk)"),
RunPlugin = fun(Plugin, Step) ->
case erlang:function_exported(Plugin, Step, 2) of
false -> ok;
@@ -4536,27 +4729,17 @@ define dep_autopatch_rebar.erl
halt()
endef
-define dep_autopatch_app.erl
- UpdateModules = fun(App) ->
- case filelib:is_regular(App) of
- false -> ok;
- true ->
- {ok, [{application, '$(1)', L0}]} = file:consult(App),
- Mods = filelib:fold_files("$(call core_native_path,$(DEPS_DIR)/$1/src)", "\\\\.erl$$", true,
- fun (F, Acc) -> [list_to_atom(filename:rootname(filename:basename(F)))|Acc] end, []),
- L = lists:keystore(modules, 1, L0, {modules, Mods}),
- ok = file:write_file(App, io_lib:format("~p.~n", [{application, '$(1)', L}]))
- end
- end,
- UpdateModules("$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"),
- halt()
-endef
-
define dep_autopatch_appsrc_script.erl
AppSrc = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)",
AppSrcScript = AppSrc ++ ".script",
- Bindings = erl_eval:new_bindings(),
- {ok, Conf} = file:script(AppSrcScript, Bindings),
+ {ok, Conf0} = file:consult(AppSrc),
+ Bindings0 = erl_eval:new_bindings(),
+ Bindings1 = erl_eval:add_binding('CONFIG', Conf0, Bindings0),
+ Bindings = erl_eval:add_binding('SCRIPT', AppSrcScript, Bindings1),
+ Conf = case file:script(AppSrcScript, Bindings) of
+ {ok, [C]} -> C;
+ {ok, C} -> C
+ end,
ok = file:write_file(AppSrc, io_lib:format("~p.~n", [Conf])),
halt()
endef
@@ -4569,7 +4752,11 @@ define dep_autopatch_appsrc.erl
true ->
{ok, [{application, $(1), L0}]} = file:consult(AppSrcIn),
L1 = lists:keystore(modules, 1, L0, {modules, []}),
- L2 = case lists:keyfind(vsn, 1, L1) of {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"}); _ -> L1 end,
+ L2 = case lists:keyfind(vsn, 1, L1) of
+ {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"});
+ {_, {cmd, _}} -> lists:keyreplace(vsn, 1, L1, {vsn, "cmd"});
+ _ -> L1
+ end,
L3 = case lists:keyfind(registered, 1, L2) of false -> [{registered, []}|L2]; _ -> L2 end,
ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $(1), L3}])),
case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end
@@ -4599,11 +4786,15 @@ define dep_fetch_cp
cp -R $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1));
endef
+define dep_fetch_ln
+ ln -s $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1));
+endef
+
# Hex only has a package version. No need to look in the Erlang.mk packages.
define dep_fetch_hex
mkdir -p $(ERLANG_MK_TMP)/hex $(DEPS_DIR)/$1; \
$(call core_http_get,$(ERLANG_MK_TMP)/hex/$1.tar,\
- https://s3.amazonaws.com/s3.hex.pm/tarballs/$1-$(strip $(word 2,$(dep_$1))).tar); \
+ https://repo.hex.pm/tarballs/$1-$(strip $(word 2,$(dep_$1))).tar); \
tar -xOf $(ERLANG_MK_TMP)/hex/$1.tar contents.tar.gz | tar -C $(DEPS_DIR)/$1 -xzf -;
endef
@@ -4634,7 +4825,7 @@ $(DEPS_DIR)/$(call dep_name,$1):
$(eval DEP_NAME := $(call dep_name,$1))
$(eval DEP_STR := $(if $(filter-out $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))"))
$(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \
- echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)."; \
+ echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)." >&2; \
exit 17; \
fi
$(verbose) mkdir -p $(DEPS_DIR)
@@ -4676,15 +4867,15 @@ ifndef IS_APP
clean:: clean-apps
clean-apps:
- $(verbose) for dep in $(ALL_APPS_DIRS) ; do \
- $(MAKE) -C $$dep clean IS_APP=1 || exit $$?; \
+ $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \
+ $(MAKE) -C $$dep clean IS_APP=1; \
done
distclean:: distclean-apps
distclean-apps:
- $(verbose) for dep in $(ALL_APPS_DIRS) ; do \
- $(MAKE) -C $$dep distclean IS_APP=1 || exit $$?; \
+ $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \
+ $(MAKE) -C $$dep distclean IS_APP=1; \
done
endif
@@ -4704,84 +4895,6 @@ ERLANG_MK_RECURSIVE_REL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-rel-deps-list.log
ERLANG_MK_RECURSIVE_TEST_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-test-deps-list.log
ERLANG_MK_RECURSIVE_SHELL_DEPS_LIST = $(ERLANG_MK_TMP)/recursive-shell-deps-list.log
-# External plugins.
-
-DEP_PLUGINS ?=
-
-define core_dep_plugin
--include $(DEPS_DIR)/$(1)
-
-$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ;
-endef
-
-$(foreach p,$(DEP_PLUGINS),\
- $(eval $(if $(findstring /,$p),\
- $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\
- $(call core_dep_plugin,$p/plugins.mk,$p))))
-
-# Copyright (c) 2013-2016, Loïc Hoguin <[email protected]>
-# This file is part of erlang.mk and subject to the terms of the ISC License.
-
-# Configuration.
-
-DTL_FULL_PATH ?=
-DTL_PATH ?= templates/
-DTL_SUFFIX ?= _dtl
-DTL_OPTS ?=
-
-# Verbosity.
-
-dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F));
-dtl_verbose = $(dtl_verbose_$(V))
-
-# Core targets.
-
-DTL_PATH := $(abspath $(DTL_PATH))
-DTL_FILES := $(sort $(call core_find,$(DTL_PATH),*.dtl))
-
-ifneq ($(DTL_FILES),)
-
-DTL_NAMES = $(addsuffix $(DTL_SUFFIX),$(DTL_FILES:$(DTL_PATH)/%.dtl=%))
-DTL_MODULES = $(if $(DTL_FULL_PATH),$(subst /,_,$(DTL_NAMES)),$(notdir $(DTL_NAMES)))
-BEAM_FILES += $(addsuffix .beam,$(addprefix ebin/,$(DTL_MODULES)))
-
-ifneq ($(words $(DTL_FILES)),0)
-# Rebuild templates when the Makefile changes.
-$(ERLANG_MK_TMP)/last-makefile-change-erlydtl: $(MAKEFILE_LIST)
- @mkdir -p $(ERLANG_MK_TMP)
- @if test -f $@; then \
- touch $(DTL_FILES); \
- fi
- @touch $@
-
-ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change-erlydtl
-endif
-
-define erlydtl_compile.erl
- [begin
- Module0 = case "$(strip $(DTL_FULL_PATH))" of
- "" ->
- filename:basename(F, ".dtl");
- _ ->
- "$(DTL_PATH)/" ++ F2 = filename:rootname(F, ".dtl"),
- re:replace(F2, "/", "_", [{return, list}, global])
- end,
- Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"),
- case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors]) of
- ok -> ok;
- {ok, _} -> ok
- end
- end || F <- string:tokens("$(1)", " ")],
- halt().
-endef
-
-ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/
- $(if $(strip $?),\
- $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$(call core_native_path,$?)),\
- -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/))
-
-endif
-
# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -4801,10 +4914,9 @@ endef
define compile_proto.erl
[begin
- Dir = filename:dirname(filename:dirname(F)),
protobuffs_compile:generate_source(F,
- [{output_include_dir, Dir ++ "/include"},
- {output_src_dir, Dir ++ "/ebin"}])
+ [{output_include_dir, "./include"},
+ {output_src_dir, "./ebin"}])
end || F <- string:tokens("$(1)", " ")],
halt().
endef
@@ -4828,6 +4940,8 @@ COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST)))
ERLC_EXCLUDE ?=
ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE)))
+ERLC_ASN1_OPTS ?=
+
ERLC_MIB_OPTS ?=
COMPILE_MIB_FIRST ?=
COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST)))
@@ -4877,25 +4991,25 @@ endif
ifeq ($(wildcard src/$(PROJECT_MOD).erl),)
define app_file
-{application, $(PROJECT), [
+{application, '$(PROJECT)', [
{description, "$(PROJECT_DESCRIPTION)"},
{vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP),
{id$(comma)$(space)"$(1)"}$(comma))
{modules, [$(call comma_list,$(2))]},
{registered, []},
- {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]},
+ {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(foreach dep,$(DEPS),$(call dep_name,$(dep))))]},
{env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),)
]}.
endef
else
define app_file
-{application, $(PROJECT), [
+{application, '$(PROJECT)', [
{description, "$(PROJECT_DESCRIPTION)"},
{vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP),
{id$(comma)$(space)"$(1)"}$(comma))
{modules, [$(call comma_list,$(2))]},
{registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]},
- {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]},
+ {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(foreach dep,$(DEPS),$(call dep_name,$(dep))))]},
{mod, {$(PROJECT_MOD), []}},
{env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),)
]}.
@@ -4920,7 +5034,7 @@ ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES))))
define compile_asn1
$(verbose) mkdir -p include/
- $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(1)
+ $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(ERLC_ASN1_OPTS) $(1)
$(verbose) mv asn1/*.erl src/
$(verbose) mv asn1/*.hrl include/
$(verbose) mv asn1/*.asn1db include/
@@ -4960,6 +5074,14 @@ define makedep.erl
E = ets:new(makedep, [bag]),
G = digraph:new([acyclic]),
ErlFiles = lists:usort(string:tokens("$(ERL_FILES)", " ")),
+ DepsDir = "$(call core_native_path,$(DEPS_DIR))",
+ AppsDir = "$(call core_native_path,$(APPS_DIR))",
+ DepsDirsSrc = "$(if $(wildcard $(DEPS_DIR)/*/src), $(call core_native_path,$(wildcard $(DEPS_DIR)/*/src)))",
+ DepsDirsInc = "$(if $(wildcard $(DEPS_DIR)/*/include), $(call core_native_path,$(wildcard $(DEPS_DIR)/*/include)))",
+ AppsDirsSrc = "$(if $(wildcard $(APPS_DIR)/*/src), $(call core_native_path,$(wildcard $(APPS_DIR)/*/src)))",
+ AppsDirsInc = "$(if $(wildcard $(APPS_DIR)/*/include), $(call core_native_path,$(wildcard $(APPS_DIR)/*/include)))",
+ DepsDirs = lists:usort(string:tokens(DepsDirsSrc++DepsDirsInc, " ")),
+ AppsDirs = lists:usort(string:tokens(AppsDirsSrc++AppsDirsInc, " ")),
Modules = [{list_to_atom(filename:basename(F, ".erl")), F} || F <- ErlFiles],
Add = fun (Mod, Dep) ->
case lists:keyfind(Dep, 1, Modules) of
@@ -4974,61 +5096,99 @@ define makedep.erl
end,
AddHd = fun (F, Mod, DepFile) ->
case file:open(DepFile, [read]) of
- {error, enoent} -> ok;
+ {error, enoent} ->
+ ok;
{ok, Fd} ->
- F(F, Fd, Mod),
{_, ModFile} = lists:keyfind(Mod, 1, Modules),
- ets:insert(E, {ModFile, DepFile})
+ case ets:match(E, {ModFile, DepFile}) of
+ [] ->
+ ets:insert(E, {ModFile, DepFile}),
+ F(F, Fd, Mod,0);
+ _ -> ok
+ end
end
end,
+ SearchHrl = fun
+ F(_Hrl, []) -> {error,enoent};
+ F(Hrl, [Dir|Dirs]) ->
+ HrlF = filename:join([Dir,Hrl]),
+ case filelib:is_file(HrlF) of
+ true ->
+ {ok, HrlF};
+ false -> F(Hrl,Dirs)
+ end
+ end,
Attr = fun
- (F, Mod, behavior, Dep) -> Add(Mod, Dep);
- (F, Mod, behaviour, Dep) -> Add(Mod, Dep);
- (F, Mod, compile, {parse_transform, Dep}) -> Add(Mod, Dep);
- (F, Mod, compile, Opts) when is_list(Opts) ->
+ (_F, Mod, behavior, Dep) ->
+ Add(Mod, Dep);
+ (_F, Mod, behaviour, Dep) ->
+ Add(Mod, Dep);
+ (_F, Mod, compile, {parse_transform, Dep}) ->
+ Add(Mod, Dep);
+ (_F, Mod, compile, Opts) when is_list(Opts) ->
case proplists:get_value(parse_transform, Opts) of
undefined -> ok;
Dep -> Add(Mod, Dep)
end;
(F, Mod, include, Hrl) ->
- case filelib:is_file("include/" ++ Hrl) of
- true -> AddHd(F, Mod, "include/" ++ Hrl);
- false ->
- case filelib:is_file("src/" ++ Hrl) of
- true -> AddHd(F, Mod, "src/" ++ Hrl);
- false -> false
- end
+ case SearchHrl(Hrl, ["src", "include",AppsDir,DepsDir]++AppsDirs++DepsDirs) of
+ {ok, FoundHrl} -> AddHd(F, Mod, FoundHrl);
+ {error, _} -> false
+ end;
+ (F, Mod, include_lib, Hrl) ->
+ case SearchHrl(Hrl, ["src", "include",AppsDir,DepsDir]++AppsDirs++DepsDirs) of
+ {ok, FoundHrl} -> AddHd(F, Mod, FoundHrl);
+ {error, _} -> false
end;
- (F, Mod, include_lib, "$1/include/" ++ Hrl) -> AddHd(F, Mod, "include/" ++ Hrl);
- (F, Mod, include_lib, Hrl) -> AddHd(F, Mod, "include/" ++ Hrl);
(F, Mod, import, {Imp, _}) ->
- case filelib:is_file("src/" ++ atom_to_list(Imp) ++ ".erl") of
+ IsFile =
+ case lists:keyfind(Imp, 1, Modules) of
+ false -> false;
+ {_, FilePath} -> filelib:is_file(FilePath)
+ end,
+ case IsFile of
false -> ok;
true -> Add(Mod, Imp)
end;
(_, _, _, _) -> ok
end,
- MakeDepend = fun(F, Fd, Mod) ->
- case io:parse_erl_form(Fd, undefined) of
- {ok, {attribute, _, Key, Value}, _} ->
- Attr(F, Mod, Key, Value),
- F(F, Fd, Mod);
- {eof, _} ->
- file:close(Fd);
- _ ->
- F(F, Fd, Mod)
- end
+ MakeDepend = fun
+ (F, Fd, Mod, StartLocation) ->
+ {ok, Filename} = file:pid2name(Fd),
+ case io:parse_erl_form(Fd, undefined, StartLocation) of
+ {ok, AbsData, EndLocation} ->
+ case AbsData of
+ {attribute, _, Key, Value} ->
+ Attr(F, Mod, Key, Value),
+ F(F, Fd, Mod, EndLocation);
+ _ -> F(F, Fd, Mod, EndLocation)
+ end;
+ {eof, _ } -> file:close(Fd);
+ {error, ErrorDescription } ->
+ file:close(Fd);
+ {error, ErrorInfo, ErrorLocation} ->
+ F(F, Fd, Mod, ErrorLocation)
+ end,
+ ok
end,
[begin
Mod = list_to_atom(filename:basename(F, ".erl")),
{ok, Fd} = file:open(F, [read]),
- MakeDepend(MakeDepend, Fd, Mod)
+ MakeDepend(MakeDepend, Fd, Mod,0)
end || F <- ErlFiles],
Depend = sofs:to_external(sofs:relation_to_family(sofs:relation(ets:tab2list(E)))),
CompileFirst = [X || X <- lists:reverse(digraph_utils:topsort(G)), [] =/= digraph:in_neighbours(G, X)],
+ TargetPath = fun(Target) ->
+ case lists:keyfind(Target, 1, Modules) of
+ false -> "";
+ {_, DepFile} ->
+ DirSubname = tl(string:tokens(filename:dirname(DepFile), "/")),
+ string:join(DirSubname ++ [atom_to_list(Target)], "/")
+ end
+ end,
ok = file:write_file("$(1)", [
[[F, "::", [[" ", D] || D <- Deps], "; @touch \$$@\n"] || {F, Deps} <- Depend],
- "\nCOMPILE_FIRST +=", [[" ", atom_to_list(CF)] || CF <- CompileFirst], "\n"
+ "\nCOMPILE_FIRST +=", [[" ", TargetPath(CF)] || CF <- CompileFirst], "\n"
]),
halt()
endef
@@ -5052,7 +5212,7 @@ $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):
ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change
endif
--include $(PROJECT).d
+include $(wildcard $(PROJECT).d)
ebin/$(PROJECT).app:: ebin/
@@ -5113,7 +5273,7 @@ ifneq ($(SKIP_DEPS),)
doc-deps:
else
doc-deps: $(ALL_DOC_DEPS_DIRS)
- $(verbose) for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
+ $(verbose) set -e; for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
endif
# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
@@ -5133,7 +5293,7 @@ ifneq ($(SKIP_DEPS),)
rel-deps:
else
rel-deps: $(ALL_REL_DEPS_DIRS)
- $(verbose) for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
+ $(verbose) set -e; for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done
endif
# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
@@ -5158,7 +5318,7 @@ ifneq ($(SKIP_DEPS),)
test-deps:
else
test-deps: $(ALL_TEST_DEPS_DIRS)
- $(verbose) for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done
+ $(verbose) set -e; for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done
endif
ifneq ($(wildcard $(TEST_DIR)),)
@@ -5170,17 +5330,17 @@ endif
ifeq ($(wildcard src),)
test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS)
test-build:: clean deps test-deps
- $(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)"
+ $(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
else
ifeq ($(wildcard ebin/test),)
test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS)
test-build:: clean deps test-deps $(PROJECT).d
- $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)"
+ $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
$(gen_verbose) touch ebin/test
else
test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS)
test-build:: deps test-deps $(PROJECT).d
- $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)"
+ $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(call escape_dquotes,$(TEST_ERLC_OPTS))"
endif
clean:: clean-test-dir
@@ -5277,6 +5437,7 @@ MAN_VERSION ?= $(PROJECT_VERSION)
define asciidoc2man.erl
try
[begin
+ io:format(" ADOC ~s~n", [F]),
ok = asciideck:to_manpage(asciideck:parse_file(F), #{
compress => gzip,
outdir => filename:dirname(F),
@@ -5285,7 +5446,8 @@ try
})
end || F <- [$(shell echo $(addprefix $(comma)\",$(addsuffix \",$1)) | sed 's/^.//')]],
halt(0)
-catch _:_ ->
+catch C:E ->
+ io:format("Exception ~p:~p~nStacktrace: ~p~n", [C, E, erlang:get_stacktrace()]),
halt(1)
end.
endef
@@ -5301,7 +5463,7 @@ install-docs:: install-asciidoc
install-asciidoc: asciidoc-manual
$(foreach s,$(MAN_SECTIONS),\
mkdir -p $(MAN_INSTALL_PATH)/man$s/ && \
- install -g `id -u` -o `id -g` -m 0644 doc/man$s/*.gz $(MAN_INSTALL_PATH)/man$s/;)
+ install -g `id -g` -o `id -u` -m 0644 doc/man$s/*.gz $(MAN_INSTALL_PATH)/man$s/;)
distclean-asciidoc-manual:
$(gen_verbose) rm -rf $(addprefix doc/man,$(MAN_SECTIONS))
@@ -5562,6 +5724,51 @@ code_change(_OldVsn, StateName, StateData, _Extra) ->
{ok, StateName, StateData}.
endef
+define tpl_gen_statem
+-module($(n)).
+-behaviour(gen_statem).
+
+%% API.
+-export([start_link/0]).
+
+%% gen_statem.
+-export([callback_mode/0]).
+-export([init/1]).
+-export([state_name/3]).
+-export([handle_event/4]).
+-export([terminate/3]).
+-export([code_change/4]).
+
+-record(state, {
+}).
+
+%% API.
+
+-spec start_link() -> {ok, pid()}.
+start_link() ->
+ gen_statem:start_link(?MODULE, [], []).
+
+%% gen_statem.
+
+callback_mode() ->
+ state_functions.
+
+init([]) ->
+ {ok, state_name, #state{}}.
+
+state_name(_EventType, _EventData, StateData) ->
+ {next_state, state_name, StateData}.
+
+handle_event(_EventType, _EventData, StateName, StateData) ->
+ {next_state, StateName, StateData}.
+
+terminate(_Reason, _StateName, _StateData) ->
+ ok.
+
+code_change(_OldVsn, StateName, StateData, _Extra) ->
+ {ok, StateName, StateData}.
+endef
+
define tpl_cowboy_loop
-module($(n)).
-behaviour(cowboy_loop_handler).
@@ -5754,20 +5961,18 @@ endif
ifndef t
$(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP])
endif
-ifndef tpl_$(t)
- $(error Unknown template)
-endif
ifndef n
$(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP])
endif
ifdef in
- $(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new t=$t n=$n in=
+ $(call render_template,tpl_$(t),$(APPS_DIR)/$(in)/src/$(n).erl)
else
$(call render_template,tpl_$(t),src/$(n).erl)
endif
list-templates:
- $(verbose) echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES))))
+ $(verbose) @echo Available templates:
+ $(verbose) printf " %s\n" $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES))))
# Copyright (c) 2014-2016, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -6004,10 +6209,10 @@ else
$(call render_template,bs_erl_nif,src/$n.erl)
endif
-# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
+# Copyright (c) 2015-2017, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
-.PHONY: ci ci-prepare ci-setup distclean-kerl
+.PHONY: ci ci-prepare ci-setup
CI_OTP ?=
CI_HIPE ?=
@@ -6025,24 +6230,9 @@ ifeq ($(strip $(CI_OTP) $(CI_HIPE) $(CI_ERLLVM)),)
ci::
else
-ifeq ($(strip $(KERL)),)
-KERL := $(ERLANG_MK_TMP)/kerl/kerl
-endif
-
-export KERL
-
-KERL_GIT ?= https://github.com/kerl/kerl
-KERL_COMMIT ?= master
-
-KERL_MAKEFLAGS ?=
-
-OTP_GIT ?= https://github.com/erlang/otp
-
-CI_INSTALL_DIR ?= $(HOME)/erlang
-
ci:: $(addprefix ci-,$(CI_OTP) $(addsuffix -native,$(CI_HIPE)) $(addsuffix -erllvm,$(CI_ERLLVM)))
-ci-prepare: $(addprefix $(CI_INSTALL_DIR)/,$(CI_OTP) $(addsuffix -native,$(CI_HIPE)))
+ci-prepare: $(addprefix $(KERL_INSTALL_DIR)/,$(CI_OTP) $(addsuffix -native,$(CI_HIPE)))
ci-setup::
@@ -6052,10 +6242,10 @@ ci_verbose_0 = @echo " CI " $(1);
ci_verbose = $(ci_verbose_$(V))
define ci_target
-ci-$1: $(CI_INSTALL_DIR)/$2
+ci-$1: $(KERL_INSTALL_DIR)/$2
$(verbose) $(MAKE) --no-print-directory clean
$(ci_verbose) \
- PATH="$(CI_INSTALL_DIR)/$2/bin:$(PATH)" \
+ PATH="$(KERL_INSTALL_DIR)/$2/bin:$(PATH)" \
CI_OTP_RELEASE="$1" \
CT_OPTS="-label $1" \
CI_VM="$3" \
@@ -6067,32 +6257,8 @@ $(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp),$(otp),otp)))
$(foreach otp,$(CI_HIPE),$(eval $(call ci_target,$(otp)-native,$(otp)-native,native)))
$(foreach otp,$(CI_ERLLVM),$(eval $(call ci_target,$(otp)-erllvm,$(otp)-native,erllvm)))
-define ci_otp_target
-ifeq ($(wildcard $(CI_INSTALL_DIR)/$(1)),)
-$(CI_INSTALL_DIR)/$(1): $(KERL)
- MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $(1) $(1)
- $(KERL) install $(1) $(CI_INSTALL_DIR)/$(1)
-endif
-endef
-
-$(foreach otp,$(CI_OTP),$(eval $(call ci_otp_target,$(otp))))
-
-define ci_hipe_target
-ifeq ($(wildcard $(CI_INSTALL_DIR)/$1-native),)
-$(CI_INSTALL_DIR)/$1-native: $(KERL)
- KERL_CONFIGURE_OPTIONS=--enable-native-libs \
- MAKEFLAGS="$(KERL_MAKEFLAGS)" $(KERL) build git $(OTP_GIT) $1 $1-native
- $(KERL) install $1-native $(CI_INSTALL_DIR)/$1-native
-endif
-endef
-
-$(foreach otp,$(sort $(CI_HIPE) $(CI_ERLLLVM)),$(eval $(call ci_hipe_target,$(otp))))
-
-$(KERL):
- $(verbose) mkdir -p $(ERLANG_MK_TMP)
- $(gen_verbose) git clone --depth 1 $(KERL_GIT) $(ERLANG_MK_TMP)/kerl
- $(verbose) cd $(ERLANG_MK_TMP)/kerl && git checkout $(KERL_COMMIT)
- $(verbose) chmod +x $(KERL)
+$(foreach otp,$(CI_OTP),$(eval $(call kerl_otp_target,$(otp))))
+$(foreach otp,$(sort $(CI_HIPE) $(CI_ERLLLVM)),$(eval $(call kerl_hipe_target,$(otp))))
help::
$(verbose) printf "%s\n" "" \
@@ -6102,10 +6268,6 @@ help::
"The CI_OTP variable must be defined with the Erlang versions" \
"that must be tested. For example: CI_OTP = OTP-17.3.4 OTP-17.5.3"
-distclean:: distclean-kerl
-
-distclean-kerl:
- $(gen_verbose) rm -rf $(KERL)
endif
# Copyright (c) 2013-2016, Loïc Hoguin <[email protected]>
@@ -6123,6 +6285,7 @@ CT_SUITES := $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*
endif
endif
CT_SUITES ?=
+CT_LOGS_DIR ?= $(CURDIR)/logs
# Core targets.
@@ -6145,15 +6308,18 @@ CT_RUN = ct_run \
-noinput \
-pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \
-dir $(TEST_DIR) \
- -logdir $(CURDIR)/logs
+ -logdir $(CT_LOGS_DIR)
ifeq ($(CT_SUITES),)
ct: $(if $(IS_APP),,apps-ct)
else
+# We do not run tests if we are in an apps/* with no test directory.
+ifneq ($(IS_APP)$(wildcard $(TEST_DIR)),1)
ct: test-build $(if $(IS_APP),,apps-ct)
- $(verbose) mkdir -p $(CURDIR)/logs/
+ $(verbose) mkdir -p $(CT_LOGS_DIR)
$(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS)
endif
+endif
ifneq ($(ALL_APPS_DIRS),)
define ct_app_target
@@ -6179,14 +6345,14 @@ endif
define ct_suite_target
ct-$(1): test-build
- $(verbose) mkdir -p $(CURDIR)/logs/
+ $(verbose) mkdir -p $(CT_LOGS_DIR)
$(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(1)) $(CT_EXTRA) $(CT_OPTS)
endef
$(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test))))
distclean-ct:
- $(gen_verbose) rm -rf $(CURDIR)/logs/
+ $(gen_verbose) rm -rf $(CT_LOGS_DIR)
# Copyright (c) 2013-2016, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -6201,6 +6367,7 @@ export DIALYZER_PLT
PLT_APPS ?=
DIALYZER_DIRS ?= --src -r $(wildcard src) $(ALL_APPS_DIRS)
DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions -Wunmatched_returns # -Wunderspecs
+DIALYZER_PLT_OPTS ?=
# Core targets.
@@ -6232,8 +6399,10 @@ define filter_opts.erl
endef
$(DIALYZER_PLT): deps app
- $(verbose) dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) \
- `test -f $(ERLANG_MK_TMP)/deps.log && cat $(ERLANG_MK_TMP)/deps.log`
+ $(eval DEPS_LOG := $(shell test -f $(ERLANG_MK_TMP)/deps.log && \
+ while read p; do test -d $$p/ebin && echo $$p/ebin; done <$(ERLANG_MK_TMP)/deps.log))
+ $(verbose) dialyzer --build_plt $(DIALYZER_PLT_OPTS) --apps \
+ erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS_LOG)
plt: $(DIALYZER_PLT)
@@ -6245,7 +6414,7 @@ dialyze:
else
dialyze: $(DIALYZER_PLT)
endif
- $(verbose) dialyzer --no_native `$(ERL) -eval "$(subst $(newline),,$(subst ",\",$(call filter_opts.erl)))" -extra $(ERLC_OPTS)` $(DIALYZER_DIRS) $(DIALYZER_OPTS)
+ $(verbose) dialyzer --no_native `$(ERL) -eval "$(subst $(newline),,$(call escape_dquotes,$(call filter_opts.erl)))" -extra $(ERLC_OPTS)` $(DIALYZER_DIRS) $(DIALYZER_OPTS)
# Copyright (c) 2013-2016, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -6255,10 +6424,21 @@ endif
# Configuration.
EDOC_OPTS ?=
+EDOC_SRC_DIRS ?=
+EDOC_OUTPUT ?= doc
+
+define edoc.erl
+ SrcPaths = lists:foldl(fun(P, Acc) ->
+ filelib:wildcard(atom_to_list(P) ++ "/{src,c_src}") ++ Acc
+ end, [], [$(call comma_list,$(patsubst %,'%',$(EDOC_SRC_DIRS)))]),
+ DefaultOpts = [{dir, "$(EDOC_OUTPUT)"}, {source_path, SrcPaths}, {subpackages, false}],
+ edoc:application($(1), ".", [$(2)] ++ DefaultOpts),
+ halt(0).
+endef
# Core targets.
-ifneq ($(wildcard doc/overview.edoc),)
+ifneq ($(strip $(EDOC_SRC_DIRS)$(wildcard doc/overview.edoc)),)
docs:: edoc
endif
@@ -6267,10 +6447,73 @@ distclean:: distclean-edoc
# Plugin-specific targets.
edoc: distclean-edoc doc-deps
- $(gen_verbose) $(ERL) -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), halt().'
+ $(gen_verbose) $(call erlang,$(call edoc.erl,$(PROJECT),$(EDOC_OPTS)))
distclean-edoc:
- $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info
+ $(gen_verbose) rm -f $(EDOC_OUTPUT)/*.css $(EDOC_OUTPUT)/*.html $(EDOC_OUTPUT)/*.png $(EDOC_OUTPUT)/edoc-info
+
+# Copyright (c) 2013-2016, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+# Configuration.
+
+DTL_FULL_PATH ?=
+DTL_PATH ?= templates/
+DTL_SUFFIX ?= _dtl
+DTL_OPTS ?=
+
+# Verbosity.
+
+dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F));
+dtl_verbose = $(dtl_verbose_$(V))
+
+# Core targets.
+
+DTL_PATH := $(abspath $(DTL_PATH))
+DTL_FILES := $(sort $(call core_find,$(DTL_PATH),*.dtl))
+
+ifneq ($(DTL_FILES),)
+
+DTL_NAMES = $(addsuffix $(DTL_SUFFIX),$(DTL_FILES:$(DTL_PATH)/%.dtl=%))
+DTL_MODULES = $(if $(DTL_FULL_PATH),$(subst /,_,$(DTL_NAMES)),$(notdir $(DTL_NAMES)))
+BEAM_FILES += $(addsuffix .beam,$(addprefix ebin/,$(DTL_MODULES)))
+
+ifneq ($(words $(DTL_FILES)),0)
+# Rebuild templates when the Makefile changes.
+$(ERLANG_MK_TMP)/last-makefile-change-erlydtl: $(MAKEFILE_LIST)
+ @mkdir -p $(ERLANG_MK_TMP)
+ @if test -f $@; then \
+ touch $(DTL_FILES); \
+ fi
+ @touch $@
+
+ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change-erlydtl
+endif
+
+define erlydtl_compile.erl
+ [begin
+ Module0 = case "$(strip $(DTL_FULL_PATH))" of
+ "" ->
+ filename:basename(F, ".dtl");
+ _ ->
+ "$(DTL_PATH)/" ++ F2 = filename:rootname(F, ".dtl"),
+ re:replace(F2, "/", "_", [{return, list}, global])
+ end,
+ Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"),
+ case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors]) of
+ ok -> ok;
+ {ok, _} -> ok
+ end
+ end || F <- string:tokens("$(1)", " ")],
+ halt().
+endef
+
+ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/
+ $(if $(strip $?),\
+ $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$(call core_native_path,$?)),\
+ -pa ebin/ $(DEPS_DIR)/erlydtl/ebin/))
+
+endif
# Copyright (c) 2016, Loïc Hoguin <[email protected]>
# Copyright (c) 2014, Dave Cottlehuber <[email protected]>
@@ -6319,7 +6562,7 @@ escript:: escript-zip
$(verbose) chmod +x $(ESCRIPT_FILE)
distclean-escript:
- $(gen_verbose) rm -f $(ESCRIPT_NAME)
+ $(gen_verbose) rm -f $(ESCRIPT_FILE)
# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
# Copyright (c) 2014, Enrique Fernandez <[email protected]>
@@ -6344,22 +6587,27 @@ help::
# Plugin-specific targets.
define eunit.erl
- case "$(COVER)" of
- "" -> ok;
+ Enabled = case "$(COVER)" of
+ "" -> false;
_ ->
- case cover:compile_beam_directory("ebin") of
- {error, _} -> halt(1);
- _ -> ok
+ case filelib:is_dir("ebin") of
+ false -> false;
+ true ->
+ case cover:compile_beam_directory("ebin") of
+ {error, _} -> halt(1);
+ _ -> true
+ end
end
end,
case eunit:test($1, [$(EUNIT_OPTS)]) of
ok -> ok;
error -> halt(2)
end,
- case "$(COVER)" of
- "" -> ok;
+ case {Enabled, "$(COVER)"} of
+ {false, _} -> ok;
+ {_, ""} -> ok;
_ ->
- cover:export("eunit.coverdata")
+ cover:export("$(COVER_DATA_DIR)/eunit.coverdata")
end,
halt()
endef
@@ -6368,10 +6616,10 @@ EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(CURDIR
ifdef t
ifeq (,$(findstring :,$(t)))
-eunit: test-build
+eunit: test-build cover-data-dir
$(gen_verbose) $(call erlang,$(call eunit.erl,['$(t)']),$(EUNIT_ERL_OPTS))
else
-eunit: test-build
+eunit: test-build cover-data-dir
$(gen_verbose) $(call erlang,$(call eunit.erl,fun $(t)/0),$(EUNIT_ERL_OPTS))
endif
else
@@ -6381,12 +6629,72 @@ EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.erl)))
EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \
$(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)')
-eunit: test-build $(if $(IS_APP),,apps-eunit)
+eunit: test-build $(if $(IS_APP),,apps-eunit) cover-data-dir
$(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_ERL_OPTS))
ifneq ($(ALL_APPS_DIRS),)
apps-eunit:
- $(verbose) for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; done
+ $(verbose) eunit_retcode=0 ; for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; \
+ [ $$? -ne 0 ] && eunit_retcode=1 ; done ; \
+ exit $$eunit_retcode
+endif
+endif
+
+# Copyright (c) 2015-2017, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+ifeq ($(filter proper,$(DEPS) $(TEST_DEPS)),proper)
+.PHONY: proper
+
+# Targets.
+
+tests:: proper
+
+define proper_check.erl
+ code:add_pathsa([
+ "$(call core_native_path,$(CURDIR)/ebin)",
+ "$(call core_native_path,$(DEPS_DIR)/*/ebin)",
+ "$(call core_native_path,$(TEST_DIR))"]),
+ Module = fun(M) ->
+ [true] =:= lists:usort([
+ case atom_to_list(F) of
+ "prop_" ++ _ ->
+ io:format("Testing ~p:~p/0~n", [M, F]),
+ proper:quickcheck(M:F(), nocolors);
+ _ ->
+ true
+ end
+ || {F, 0} <- M:module_info(exports)])
+ end,
+ try
+ case $(1) of
+ all -> [true] =:= lists:usort([Module(M) || M <- [$(call comma_list,$(3))]]);
+ module -> Module($(2));
+ function -> proper:quickcheck($(2), nocolors)
+ end
+ of
+ true -> halt(0);
+ _ -> halt(1)
+ catch error:undef ->
+ io:format("Undefined property or module?~n~p~n", [erlang:get_stacktrace()]),
+ halt(0)
+ end.
+endef
+
+ifdef t
+ifeq (,$(findstring :,$(t)))
+proper: test-build
+ $(verbose) $(call erlang,$(call proper_check.erl,module,$(t)))
+else
+proper: test-build
+ $(verbose) echo Testing $(t)/0
+ $(verbose) $(call erlang,$(call proper_check.erl,function,$(t)()))
+endif
+else
+proper: test-build
+ $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \
+ $(wildcard ebin/*.beam) $(call core_find,$(TEST_DIR)/,*.beam))))))
+ $(gen_verbose) $(call erlang,$(call proper_check.erl,all,undefined,$(MODULES)))
endif
endif
@@ -6400,9 +6708,15 @@ endif
RELX ?= $(ERLANG_MK_TMP)/relx
RELX_CONFIG ?= $(CURDIR)/relx.config
-RELX_URL ?= https://github.com/erlware/relx/releases/download/v3.19.0/relx
+RELX_URL ?= https://erlang.mk/res/relx-v3.24.5
RELX_OPTS ?=
RELX_OUTPUT_DIR ?= _rel
+RELX_REL_EXT ?=
+RELX_TAR ?= 1
+
+ifdef SFX
+ RELX_TAR = 1
+endif
ifeq ($(firstword $(RELX_OPTS)),-o)
RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS))
@@ -6425,14 +6739,15 @@ distclean:: distclean-relx-rel
# Plugin-specific targets.
$(RELX):
+ $(verbose) mkdir -p $(ERLANG_MK_TMP)
$(gen_verbose) $(call core_http_get,$(RELX),$(RELX_URL))
$(verbose) chmod +x $(RELX)
relx-rel: $(RELX) rel-deps app
- $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release tar
+ $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release $(if $(filter 1,$(RELX_TAR)),tar)
relx-relup: $(RELX) rel-deps app
- $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release relup tar
+ $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) release relup $(if $(filter 1,$(RELX_TAR)),tar)
distclean-relx-rel:
$(gen_verbose) rm -rf $(RELX_OUTPUT_DIR)
@@ -6440,12 +6755,18 @@ distclean-relx-rel:
# Run target.
ifeq ($(wildcard $(RELX_CONFIG)),)
-run:
+run::
else
define get_relx_release.erl
- {ok, Config} = file:consult("$(RELX_CONFIG)"),
- {release, {Name, Vsn}, _} = lists:keyfind(release, 1, Config),
+ {ok, Config} = file:consult("$(call core_native_path,$(RELX_CONFIG))"),
+ {release, {Name, Vsn0}, _} = lists:keyfind(release, 1, Config),
+ Vsn = case Vsn0 of
+ {cmd, Cmd} -> os:cmd(Cmd);
+ semver -> "";
+ {semver, _} -> "";
+ VsnStr -> Vsn0
+ end,
io:format("~s ~s", [Name, Vsn]),
halt(0).
endef
@@ -6454,8 +6775,12 @@ RELX_REL := $(shell $(call erlang,$(get_relx_release.erl)))
RELX_REL_NAME := $(word 1,$(RELX_REL))
RELX_REL_VSN := $(word 2,$(RELX_REL))
-run: all
- $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME) console
+ifeq ($(PLATFORM),msys2)
+RELX_REL_EXT := .cmd
+endif
+
+run:: all
+ $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_REL_NAME)/bin/$(RELX_REL_NAME)$(RELX_REL_EXT) console
help::
$(verbose) printf "%s\n" "" \
@@ -6473,7 +6798,7 @@ endif
# Configuration.
SHELL_ERL ?= erl
-SHELL_PATHS ?= $(CURDIR)/ebin $(APPS_DIR)/*/ebin $(DEPS_DIR)/*/ebin
+SHELL_PATHS ?= $(CURDIR)/ebin $(APPS_DIR)/*/ebin $(DEPS_DIR)/*/ebin $(TEST_DIR)
SHELL_OPTS ?=
ALL_SHELL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(SHELL_DEPS))
@@ -6490,11 +6815,88 @@ help::
$(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep))))
build-shell-deps: $(ALL_SHELL_DEPS_DIRS)
- $(verbose) for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done
+ $(verbose) set -e; for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done
shell: build-shell-deps
$(gen_verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) $(SHELL_OPTS)
+# Copyright 2017, Stanislaw Klekot <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: distclean-sphinx sphinx
+
+# Configuration.
+
+SPHINX_BUILD ?= sphinx-build
+SPHINX_SOURCE ?= doc
+SPHINX_CONFDIR ?=
+SPHINX_FORMATS ?= html
+SPHINX_DOCTREES ?= $(ERLANG_MK_TMP)/sphinx.doctrees
+SPHINX_OPTS ?=
+
+#sphinx_html_opts =
+#sphinx_html_output = html
+#sphinx_man_opts =
+#sphinx_man_output = man
+#sphinx_latex_opts =
+#sphinx_latex_output = latex
+
+# Helpers.
+
+sphinx_build_0 = @echo " SPHINX" $1; $(SPHINX_BUILD) -N -q
+sphinx_build_1 = $(SPHINX_BUILD) -N
+sphinx_build_2 = set -x; $(SPHINX_BUILD)
+sphinx_build = $(sphinx_build_$(V))
+
+define sphinx.build
+$(call sphinx_build,$1) -b $1 -d $(SPHINX_DOCTREES) $(if $(SPHINX_CONFDIR),-c $(SPHINX_CONFDIR)) $(SPHINX_OPTS) $(sphinx_$1_opts) -- $(SPHINX_SOURCE) $(call sphinx.output,$1)
+
+endef
+
+define sphinx.output
+$(if $(sphinx_$1_output),$(sphinx_$1_output),$1)
+endef
+
+# Targets.
+
+ifneq ($(wildcard $(if $(SPHINX_CONFDIR),$(SPHINX_CONFDIR),$(SPHINX_SOURCE))/conf.py),)
+docs:: sphinx
+distclean:: distclean-sphinx
+endif
+
+help::
+ $(verbose) printf "%s\n" "" \
+ "Sphinx targets:" \
+ " sphinx Generate Sphinx documentation." \
+ "" \
+ "ReST sources and 'conf.py' file are expected in directory pointed by" \
+ "SPHINX_SOURCE ('doc' by default). SPHINX_FORMATS lists formats to build (only" \
+ "'html' format is generated by default); target directory can be specified by" \
+ 'setting sphinx_$${format}_output, for example: sphinx_html_output = output/html' \
+ "Additional Sphinx options can be set in SPHINX_OPTS."
+
+# Plugin-specific targets.
+
+sphinx:
+ $(foreach F,$(SPHINX_FORMATS),$(call sphinx.build,$F))
+
+distclean-sphinx:
+ $(gen_verbose) rm -rf $(filter-out $(SPHINX_SOURCE),$(foreach F,$(SPHINX_FORMATS),$(call sphinx.output,$F)))
+
+# Copyright (c) 2017, Jean-Sébastien Pédron <[email protected]>
+# This file is contributed to erlang.mk and subject to the terms of the ISC License.
+
+.PHONY: show-ERL_LIBS show-ERLC_OPTS show-TEST_ERLC_OPTS
+
+show-ERL_LIBS:
+ @echo $(ERL_LIBS)
+
+show-ERLC_OPTS:
+ @$(foreach opt,$(ERLC_OPTS) -pa ebin -I include,echo "$(opt)";)
+
+show-TEST_ERLC_OPTS:
+ @$(foreach opt,$(TEST_ERLC_OPTS) -pa ebin -I include,echo "$(opt)";)
+
# Copyright (c) 2015-2016, Loïc Hoguin <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -6506,7 +6908,10 @@ ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq)
tests:: triq
define triq_check.erl
- code:add_pathsa(["$(call core_native_path,$(CURDIR)/ebin)", "$(call core_native_path,$(DEPS_DIR)/*/ebin)"]),
+ code:add_pathsa([
+ "$(call core_native_path,$(CURDIR)/ebin)",
+ "$(call core_native_path,$(DEPS_DIR)/*/ebin)",
+ "$(call core_native_path,$(TEST_DIR))"]),
try
case $(1) of
all -> [true] =:= lists:usort([triq:check(M) || M <- [$(call comma_list,$(3))]]);
@@ -6517,7 +6922,7 @@ define triq_check.erl
true -> halt(0);
_ -> halt(1)
catch error:undef ->
- io:format("Undefined property or module~n"),
+ io:format("Undefined property or module?~n~p~n", [erlang:get_stacktrace()]),
halt(0)
end.
endef
@@ -6533,7 +6938,8 @@ triq: test-build
endif
else
triq: test-build
- $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(wildcard ebin/*.beam))))))
+ $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \
+ $(wildcard ebin/*.beam) $(call core_find,$(TEST_DIR)/,*.beam))))))
$(gen_verbose) $(call erlang,$(call triq_check.erl,all,undefined,$(MODULES)))
endif
endif
@@ -6555,14 +6961,14 @@ endif
XREFR ?= $(CURDIR)/xrefr
export XREFR
-XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/0.2.2/xrefr
+XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/1.1.0/xrefr
# Core targets.
help::
- $(verbose) printf "%s\n" "" \
- "Xref targets:" \
- " xref Run Xrefr using $XREF_CONFIG as config file if defined"
+ $(verbose) printf '%s\n' '' \
+ 'Xref targets:' \
+ ' xref Run Xrefr using $$XREF_CONFIG as config file if defined'
distclean:: distclean-xref
@@ -6582,26 +6988,25 @@ distclean-xref:
# Copyright (c) 2015, Viktor Söderqvist <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
-COVER_REPORT_DIR = cover
+COVER_REPORT_DIR ?= cover
+COVER_DATA_DIR ?= $(CURDIR)
# Hook in coverage to ct
ifdef COVER
ifdef CT_RUN
-# All modules in 'ebin'
-COVER_MODS = $(notdir $(basename $(call core_ls,ebin/*.beam)))
-
+ifneq ($(wildcard $(TEST_DIR)),)
test-build:: $(TEST_DIR)/ct.cover.spec
-$(TEST_DIR)/ct.cover.spec:
- $(verbose) echo Cover mods: $(COVER_MODS)
+$(TEST_DIR)/ct.cover.spec: cover-data-dir
$(gen_verbose) printf "%s\n" \
- '{incl_mods,[$(subst $(space),$(comma),$(COVER_MODS))]}.' \
- '{export,"$(CURDIR)/ct.coverdata"}.' > $@
+ "{incl_app, '$(PROJECT)', details}." \
+ '{export,"$(abspath $(COVER_DATA_DIR))/ct.coverdata"}.' > $@
CT_RUN += -cover $(TEST_DIR)/ct.cover.spec
endif
endif
+endif
# Core targets
@@ -6610,6 +7015,13 @@ ifneq ($(COVER_REPORT_DIR),)
tests::
$(verbose) $(MAKE) --no-print-directory cover-report
endif
+
+cover-data-dir: | $(COVER_DATA_DIR)
+
+$(COVER_DATA_DIR):
+ $(verbose) mkdir -p $(COVER_DATA_DIR)
+else
+cover-data-dir:
endif
clean:: coverdata-clean
@@ -6623,7 +7035,7 @@ help::
"Cover targets:" \
" cover-report Generate a HTML coverage report from previously collected" \
" cover data." \
- " all.coverdata Merge {eunit,ct}.coverdata into one coverdata file." \
+ " all.coverdata Merge all coverdata files into all.coverdata." \
"" \
"If COVER=1 is set, coverage data is generated by the targets eunit and ct. The" \
"target tests additionally generates a HTML coverage report from the combined" \
@@ -6632,17 +7044,20 @@ help::
# Plugin specific targets
-COVERDATA = $(filter-out all.coverdata,$(wildcard *.coverdata))
+COVERDATA = $(filter-out $(COVER_DATA_DIR)/all.coverdata,$(wildcard $(COVER_DATA_DIR)/*.coverdata))
.PHONY: coverdata-clean
coverdata-clean:
- $(gen_verbose) rm -f *.coverdata ct.cover.spec
+ $(gen_verbose) rm -f $(COVER_DATA_DIR)/*.coverdata $(TEST_DIR)/ct.cover.spec
# Merge all coverdata files into one.
-all.coverdata: $(COVERDATA)
- $(gen_verbose) $(ERL) -eval ' \
- $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) \
- cover:export("$@"), halt(0).'
+define cover_export.erl
+ $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),)
+ cover:export("$(COVER_DATA_DIR)/$@"), halt(0).
+endef
+
+all.coverdata: $(COVERDATA) cover-data-dir
+ $(gen_verbose) $(call erlang,$(cover_export.erl))
# These are only defined if COVER_REPORT_DIR is non-empty. Set COVER_REPORT_DIR to
# empty if you want the coverdata files but not the HTML report.
@@ -6652,6 +7067,7 @@ ifneq ($(COVER_REPORT_DIR),)
cover-report-clean:
$(gen_verbose) rm -rf $(COVER_REPORT_DIR)
+ $(if $(shell ls -A $(COVER_DATA_DIR)/),,$(verbose) rmdir $(COVER_DATA_DIR))
ifeq ($(COVERDATA),)
cover-report:
@@ -6660,7 +7076,7 @@ else
# Modules which include eunit.hrl always contain one line without coverage
# because eunit defines test/0 which is never called. We compensate for this.
EUNIT_HRL_MODS = $(subst $(space),$(comma),$(shell \
- grep -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \
+ grep -H -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \
| sed "s/^src\/\(.*\)\.erl:.*/'\1'/" | uniq))
define cover_report.erl
@@ -6695,7 +7111,7 @@ define cover_report.erl
endef
cover-report:
- $(gen_verbose) mkdir -p $(COVER_REPORT_DIR)
+ $(verbose) mkdir -p $(COVER_REPORT_DIR)
$(gen_verbose) $(call erlang,$(cover_report.erl))
endif
@@ -6748,6 +7164,18 @@ sfx:
endif
endif
+# Copyright (c) 2013-2017, Loïc Hoguin <[email protected]>
+# This file is part of erlang.mk and subject to the terms of the ISC License.
+
+# External plugins.
+
+DEP_PLUGINS ?=
+
+$(foreach p,$(DEP_PLUGINS),\
+ $(eval $(if $(findstring /,$p),\
+ $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\
+ $(call core_dep_plugin,$p/plugins.mk,$p))))
+
# Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
# Copyright (c) 2015-2016, Jean-Sébastien Pédron <[email protected]>
# This file is part of erlang.mk and subject to the terms of the ISC License.
@@ -6815,22 +7243,20 @@ ifeq ($(IS_APP)$(IS_DEP),)
$(verbose) rm -f $(ERLANG_MK_RECURSIVE_TMP_LIST)
endif
ifndef IS_APP
- $(verbose) for dep in $(ALL_APPS_DIRS) ; do \
+ $(verbose) set -e; for dep in $(ALL_APPS_DIRS) ; do \
$(MAKE) -C $$dep $@ \
IS_APP=1 \
- ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST) \
- || exit $$?; \
+ ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST); \
done
endif
- $(verbose) for dep in $^ ; do \
+ $(verbose) set -e; for dep in $^ ; do \
if ! grep -qs ^$$dep$$ $(ERLANG_MK_RECURSIVE_TMP_LIST); then \
echo $$dep >> $(ERLANG_MK_RECURSIVE_TMP_LIST); \
- if grep -qs -E "^[[:blank:]]*include[[:blank:]]+(erlang\.mk|.*/erlang\.mk)$$" \
+ if grep -qs -E "^[[:blank:]]*include[[:blank:]]+(erlang\.mk|.*/erlang\.mk|.*ERLANG_MK_FILENAME.*)$$" \
$$dep/GNUmakefile $$dep/makefile $$dep/Makefile; then \
$(MAKE) -C $$dep fetch-deps \
IS_DEP=1 \
- ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST) \
- || exit $$?; \
+ ERLANG_MK_RECURSIVE_TMP_LIST=$(ERLANG_MK_RECURSIVE_TMP_LIST); \
fi \
fi \
done
diff --git a/src/asciideck.erl b/src/asciideck.erl
index 749ccec..bd5792c 100644
--- a/src/asciideck.erl
+++ b/src/asciideck.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2016-2018, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -32,8 +32,15 @@ parse_file(Filename, St) ->
parse(Data) ->
parse(Data, #{}).
-parse(Data, St) when is_binary(Data) ->
- asciideck_parser:parse(Data, St);
+parse(Data, _St) when is_binary(Data) ->
+ Passes = [
+ asciideck_attributes_pass,
+ asciideck_lists_pass,
+ asciideck_tables_pass,
+ asciideck_inline_pass
+ ],
+ lists:foldl(fun(M, AST) -> M:run(AST) end,
+ asciideck_block_parser:parse(Data), Passes);
parse(Data, St) ->
parse(iolist_to_binary(Data), St).
diff --git a/src/asciideck_attributes_parser.erl b/src/asciideck_attributes_parser.erl
new file mode 100644
index 0000000..b89c3f4
--- /dev/null
+++ b/src/asciideck_attributes_parser.erl
@@ -0,0 +1,120 @@
+%% Copyright (c) 2017-2018, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% Asciidoc User Guide 29
+-module(asciideck_attributes_parser).
+
+-export([parse/1]).
+
+-type attributes() :: #{
+ %% The raw attribute list.
+ 0 := binary(),
+ %% Positional attributes.
+ pos_integer() => binary(),
+ %% Named attributes.
+ binary() => binary()
+}.
+-export_type([attributes/0]).
+
+-define(IS_WS(C), (C =:= $\s) or (C =:= $\t)).
+
+-spec parse(binary()) -> attributes().
+parse(Data) ->
+ parse(Data, #{0 => Data}, 1).
+
+parse(<<>>, Attrs, _) ->
+ Attrs;
+parse(Data, Attrs, Nth) ->
+ case parse_attr(Data, <<>>) of
+ {Value, Rest} when Nth =/= undefined ->
+ parse(Rest, Attrs#{Nth => Value}, Nth + 1);
+ {Name, Value, Rest} ->
+ parse(Rest, Attrs#{Name => Value}, undefined)
+ end.
+
+parse_attr(<<>>, Acc) ->
+ {Acc, <<>>};
+%% Skip preceding whitespace.
+parse_attr(<<C, R/bits>>, <<>>) when ?IS_WS(C) ->
+ parse_attr(R, <<>>);
+%% Parse quoted positional attributes in their own function.
+parse_attr(<<$", R/bits>>, <<>>) ->
+ parse_quoted_attr(R, <<>>);
+%% We have a named attribute, parse the value.
+parse_attr(<<$=, R/bits>>, Name) when Name =/= <<>> ->
+ parse_attr_value(R, asciideck_block_parser:trim(Name, trailing), <<>>);
+%% We have a positional attribute.
+parse_attr(<<$,, R/bits>>, Value) ->
+ {asciideck_block_parser:trim(Value, trailing), R};
+%% Continue.
+parse_attr(<<C, R/bits>>, Acc) when C =/= $= ->
+ parse_attr(R, <<Acc/binary, C>>).
+
+%% Get everything until the next double quote.
+parse_quoted_attr(<<$", R/bits>>, Acc) ->
+ parse_quoted_attr_end(R, Acc);
+parse_quoted_attr(<<$\\, $", R/bits>>, Acc) ->
+ parse_quoted_attr(R, <<Acc/binary, $">>);
+parse_quoted_attr(<<C, R/bits>>, Acc) ->
+ parse_quoted_attr(R, <<Acc/binary, C>>).
+
+%% Skip the whitespace until the next comma or eof.
+parse_quoted_attr_end(<<>>, Value) ->
+ {Value, <<>>};
+parse_quoted_attr_end(<<$,, R/bits>>, Value) ->
+ {Value, R};
+parse_quoted_attr_end(<<C, R/bits>>, Value) when ?IS_WS(C) ->
+ parse_quoted_attr_end(R, Value).
+
+parse_attr_value(<<>>, Name, Acc) ->
+ {Name, Acc, <<>>};
+%% Skip preceding whitespace.
+parse_attr_value(<<C, R/bits>>, Name, <<>>) when ?IS_WS(C) ->
+ parse_attr_value(R, Name, <<>>);
+%% Parse quoted positional attributes in their own function.
+parse_attr_value(<<$", R/bits>>, Name, <<>>) ->
+ {Value, Rest} = parse_quoted_attr(R, <<>>),
+ {Name, Value, Rest};
+%% Done.
+parse_attr_value(<<$,, R/bits>>, Name, Value) ->
+ {Name, asciideck_block_parser:trim(Value, trailing), R};
+%% Continue.
+parse_attr_value(<<C, R/bits>>, Name, Acc) ->
+ parse_attr_value(R, Name, <<Acc/binary, C>>).
+
+-ifdef(TEST).
+attribute_0_test() ->
+ #{0 := <<"Hello,world,width=\"50\"">>} = parse(<<"Hello,world,width=\"50\"">>),
+ ok.
+
+parse_test() ->
+ #{} = parse(<<>>),
+ #{
+ 1 := <<"Hello">>
+ } = parse(<<"Hello">>),
+ #{
+ 1 := <<"quote">>,
+ 2 := <<"Bertrand Russell">>,
+ 3 := <<"The World of Mathematics (1956)">>
+ } = parse(<<"quote, Bertrand Russell, The World of Mathematics (1956)">>),
+ #{
+ 1 := <<"22 times">>,
+ <<"backcolor">> := <<"#0e0e0e">>,
+ <<"options">> := <<"noborders,wide">>
+ } = parse(<<"\"22 times\", backcolor=\"#0e0e0e\", options=\"noborders,wide\"">>),
+ #{
+ 1 := <<"A footnote&#44; &#34;with an image&#34; image:smallnew.png[]">>
+ } = parse(<<"A footnote&#44; &#34;with an image&#34; image:smallnew.png[]">>),
+ ok.
+-endif.
diff --git a/src/asciideck_attributes_pass.erl b/src/asciideck_attributes_pass.erl
new file mode 100644
index 0000000..393b57d
--- /dev/null
+++ b/src/asciideck_attributes_pass.erl
@@ -0,0 +1,112 @@
+%% Copyright (c) 2017-2018, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% The purpose of this pass is to apply attributes to
+%% their corresponding blocks. For macros the attributes
+%% are already applied. For inline elements the inline
+%% pass is taking care of it.
+-module(asciideck_attributes_pass).
+
+-export([run/1]).
+
+run([]) ->
+ [];
+%% A block identifier is an alternative way of specifying
+%% the id attribute for a block.
+run([{block_id, #{id := ID}, <<>>, _}|Tail0]) ->
+ Tail = apply_attributes(Tail0, #{<<"id">> => ID}),
+ run(Tail);
+%% A block title is ultimately treated as an attribute
+%% for the following block.
+run([{block_title, _, Title, _}|Tail0]) ->
+ Tail = apply_attributes(Tail0, #{<<"title">> => Title}),
+ run(Tail);
+run([{attribute_list, Attrs, <<>>, _}|Tail0]) ->
+ Tail = apply_attributes(Tail0, Attrs),
+ run(Tail);
+run([Block|Tail]) ->
+ [Block|run(Tail)].
+
+%% Find the next block to apply the attributes.
+apply_attributes([], _) ->
+ [];
+apply_attributes(AST=[Element0={Type, Attrs0, Content, Ann}|Tail], Attrs) ->
+ case can_apply(Type) of
+ drop ->
+ AST;
+ skip ->
+ [Element0|apply_attributes(Tail, Attrs)];
+ apply ->
+ Element = {Type, maps:merge(Attrs0, Attrs), Content, Ann},
+ [Element|Tail]
+ end.
+
+%% Block macros already come with a mandatory attribute list.
+%% Just to play it safe we drop the attributes for now.
+can_apply(block_macro) -> drop;
+%% If we hit a list item continuation, drop the attributes for now.
+can_apply(list_item_continuation) -> drop;
+%% We skip attribute lists and alike and let it sort itself out.
+can_apply(block_id) -> skip;
+can_apply(attribute_list) -> skip;
+can_apply(block_title) -> skip;
+%% Everything else is a block.
+can_apply(_) -> apply.
+
+-ifdef(TEST).
+attribute_list_test() ->
+ AST0 = [
+ {attribute_list, #{
+ 0 => <<"width=400">>,
+ <<"width">> => <<"400">>
+ }, <<>>, #{line => 1}},
+ {listing_block, #{}, <<"Hello!">>, #{line => 2}}
+ ],
+ AST = [
+ {listing_block, #{
+ 0 => <<"width=400">>,
+ <<"width">> => <<"400">>
+ }, <<"Hello!">>, #{line => 2}}
+ ],
+ AST = run(AST0),
+ ok.
+
+block_id_test() ->
+ AST0 = [
+ {block_id, #{
+ id => <<"cowboy_req">>
+ }, <<>>, #{line => 1}},
+ {listing_block, #{}, <<"Hello!">>, #{line => 2}}
+ ],
+ AST = [
+ {listing_block, #{
+ <<"id">> => <<"cowboy_req">>
+ }, <<"Hello!">>, #{line => 2}}
+ ],
+ AST = run(AST0),
+ ok.
+
+block_title_test() ->
+ AST0 = [
+ {block_title, #{}, <<"Title">>, #{line => 1}},
+ {listing_block, #{}, <<"Hello!">>, #{line => 2}}
+ ],
+ AST = [
+ {listing_block, #{
+ <<"title">> => <<"Title">>
+ }, <<"Hello!">>, #{line => 2}}
+ ],
+ AST = run(AST0),
+ ok.
+-endif.
diff --git a/src/asciideck_block_parser.erl b/src/asciideck_block_parser.erl
new file mode 100644
index 0000000..ad63fa6
--- /dev/null
+++ b/src/asciideck_block_parser.erl
@@ -0,0 +1,1116 @@
+%% Copyright (c) 2016-2018, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% The block parser is the first pass of the parsing of Asciidoc
+%% files. It only isolates the different top-level blocks and
+%% produces a representation that can then be manipulated.
+%%
+%% Further passes are necessary to propagate the parsed lists
+%% of attributes to their respective blocks, to create actual
+%% lists from the parsed list items or to parse the contents
+%% of tables. Finally a final pass will parse inline elements.
+%%
+%% This module may be called again for parsing the content
+%% of individual table cells.
+-module(asciideck_block_parser).
+
+-export([parse/1]).
+
+%% @todo Temporary export. Move somewhere else.
+-export([trim/1]).
+-export([trim/2]).
+-export([while/2]).
+
+-type ast() :: list(). %% @todo
+
+-record(state, {
+ reader :: pid()
+}).
+
+-define(IS_WS(C), (C =:= $\s) or (C =:= $\t)).
+
+-ifdef(TEST).
+-define(NOT(Type, Value), true = Type =/= element(1, hd(Value))).
+
+define_NOT_test() ->
+ %% This succeeds.
+ ?NOT(block_id, parse(<<"[[block,id]]">>)),
+ %% This fails.
+ {'EXIT', _} = (catch ?NOT(block_id, parse(<<"[[block_id]]">>))),
+ ok.
+-endif.
+
+-spec parse(binary()) -> ast().
+parse(Data) ->
+ %% @todo Might want to start it supervised.
+ %% @todo Might want to stop it also.
+ {ok, ReaderPid} = asciideck_line_reader:start_link(Data),
+ blocks(#state{reader=ReaderPid}).
+
+blocks(St) ->
+ case block(St) of
+ eof -> [];
+ Block -> [Block|blocks(St)]
+ end.
+
+%% Asciidoc parsing never fails. If a block is not
+%% formatted properly, it will be treated as a paragraph.
+block(St) ->
+ skip(fun empty_line/1, St),
+ oneof([
+ fun eof/1,
+ %% Section titles.
+ fun section_title/1,
+ fun long_section_title/1,
+ %% Block macros.
+ fun block_id/1,
+ fun block_macro/1,
+ %% Lists.
+ fun bulleted_list/1,
+ fun numbered_list/1,
+ fun labeled_list/1,
+ fun callout_list/1,
+ fun list_item_continuation/1,
+ %% Delimited blocks.
+ fun listing_block/1,
+ fun literal_block/1,
+ fun sidebar_block/1,
+ fun comment_block/1,
+ fun passthrough_block/1,
+ fun quote_block/1,
+ fun example_block/1,
+ fun open_block/1,
+ %% Table.
+ fun table/1,
+ %% Attributes.
+ fun attribute_entry/1,
+ fun attribute_list/1,
+ %% Block title.
+ fun block_title/1,
+ %% Comment lines.
+ fun comment_line/1,
+ %% Paragraphs.
+ fun literal_para/1,
+ fun admonition_para/1,
+ fun para/1
+ ], St).
+
+eof(St) ->
+ eof = read_line(St).
+
+-ifdef(TEST).
+eof_test() ->
+ [] = parse(<<>>).
+-endif.
+
+empty_line(St) ->
+ <<>> = trim(read_line(St)).
+
+-ifdef(TEST).
+empty_line_test() ->
+ [] = parse(<<
+ "\n"
+ " \n"
+ " \n"
+ "\n"
+ >>).
+-endif.
+
+%% Asciidoc User Guide 11.2
+section_title(St) ->
+ {Level, Title0} = case read_line(St) of
+ <<"=", C, R/bits>> when ?IS_WS(C) -> {0, R};
+ <<"==", C, R/bits>> when ?IS_WS(C) -> {1, R};
+ <<"===", C, R/bits>> when ?IS_WS(C) -> {2, R};
+ <<"====", C, R/bits>> when ?IS_WS(C) -> {3, R};
+ <<"=====", C, R/bits>> when ?IS_WS(C) -> {4, R}
+ end,
+ Ann = ann(St),
+ Title1 = trim(Title0),
+ %% Optional: trailing title delimiter.
+ Trailer = case Level of
+ 0 -> <<"=">>;
+ 1 -> <<"==">>;
+ 2 -> <<"===">>;
+ 3 -> <<"====">>;
+ 4 -> <<"=====">>
+ end,
+ Len = byte_size(Title1) - Level - 2,
+ Title = case Title1 of
+ <<Title2:Len/binary, WS, Trailer/binary>> when ?IS_WS(WS) -> trim(Title2);
+ _ -> trim(Title1)
+ end,
+ %% Section titles must be followed by at least one empty line.
+ _ = empty_line(St),
+ %% Good!
+ {section_title, #{level => Level}, Title, Ann}.
+
+-ifdef(TEST).
+section_title_test() ->
+ %% With trailing title delimiter.
+ [{section_title, #{level := 0}, <<"Document Title (level 0)">>, _}]
+ = parse(<<"= Document Title (level 0) =">>),
+ [{section_title, #{level := 1}, <<"Section Title (level 1)">>, _}]
+ = parse(<<"== Section Title (level 1) ==">>),
+ [{section_title, #{level := 2}, <<"Section Title (level 2)">>, _}]
+ = parse(<<"=== Section Title (level 2) ===">>),
+ [{section_title, #{level := 3}, <<"Section Title (level 3)">>, _}]
+ = parse(<<"==== Section Title (level 3) ====">>),
+ [{section_title, #{level := 4}, <<"Section Title (level 4)">>, _}]
+ = parse(<<"===== Section Title (level 4) =====">>),
+ %% Without trailing title delimiter.
+ [{section_title, #{level := 0}, <<"Document Title (level 0)">>, _}]
+ = parse(<<"= Document Title (level 0)">>),
+ [{section_title, #{level := 1}, <<"Section Title (level 1)">>, _}]
+ = parse(<<"== Section Title (level 1)">>),
+ [{section_title, #{level := 2}, <<"Section Title (level 2)">>, _}]
+ = parse(<<"=== Section Title (level 2)">>),
+ [{section_title, #{level := 3}, <<"Section Title (level 3)">>, _}]
+ = parse(<<"==== Section Title (level 3)">>),
+ [{section_title, #{level := 4}, <<"Section Title (level 4)">>, _}]
+ = parse(<<"===== Section Title (level 4)">>),
+ %% Accept more spaces before/after delimiters.
+ [{section_title, #{level := 0}, <<"Document Title (level 0)">>, _}]
+ = parse(<<"= Document Title (level 0)">>),
+ [{section_title, #{level := 0}, <<"Document Title (level 0)">>, _}]
+ = parse(<<"= Document Title (level 0) =">>),
+ [{section_title, #{level := 0}, <<"Document Title (level 0)">>, _}]
+ = parse(<<"= Document Title (level 0) =">>),
+ [{section_title, #{level := 0}, <<"Document Title (level 0)">>, _}]
+ = parse(<<"= Document Title (level 0) = ">>),
+ %% A space before the first delimiter is not a title.
+ ?NOT(section_title, parse(<<" = Document Title (level 0)">>)),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 11.1
+long_section_title(St) ->
+ %% Title must be hard against the left margin.
+ <<C, _/bits>> = Title0 = read_line(St),
+ Ann = ann(St),
+ false = ?IS_WS(C),
+ Title = trim(Title0),
+ %% Read the underline.
+ {Level, Char, Underline0} = case read_line(St) of
+ U = <<"=", _/bits >> -> {0, $=, U};
+ U = <<"-", _/bits >> -> {1, $-, U};
+ U = <<"~", _/bits >> -> {2, $~, U};
+ U = <<"^", _/bits >> -> {3, $^, U};
+ U = <<"+", _/bits >> -> {4, $+, U}
+ end,
+ Underline = trim(Underline0, trailing),
+ %% Underline must be the same character repeated over the entire line.
+ repeats(Underline, Char),
+ %% Underline must be the same size as the title, +/- 2 characters.
+ TLen = byte_size(Title),
+ ULen = byte_size(Underline),
+ true = (TLen >= ULen - 2) andalso (TLen =< ULen + 2),
+ %% Good!
+ {section_title, #{level => Level}, Title, Ann}.
+
+-ifdef(TEST).
+long_section_title_test() ->
+ %% Same amount of characters for the underline.
+ [{section_title, #{level := 0}, <<"Document Title (level 0)">>, _}] = parse(<<
+ "Document Title (level 0)\n"
+ "========================">>),
+ [{section_title, #{level := 1}, <<"Section Title (level 1)">>, _}] = parse(<<
+ "Section Title (level 1)\n"
+ "-----------------------">>),
+ [{section_title, #{level := 2}, <<"Section Title (level 2)">>, _}] = parse(<<
+ "Section Title (level 2)\n"
+ "~~~~~~~~~~~~~~~~~~~~~~~">>),
+ [{section_title, #{level := 3}, <<"Section Title (level 3)">>, _}] = parse(<<
+ "Section Title (level 3)\n"
+ "^^^^^^^^^^^^^^^^^^^^^^^">>),
+ [{section_title, #{level := 4}, <<"Section Title (level 4)">>, _}] = parse(<<
+ "Section Title (level 4)\n"
+ "+++++++++++++++++++++++">>),
+ %% A shorter title to confirm we are not cheating.
+ [{section_title, #{level := 0}, <<"Hello!">>, _}] = parse(<<
+ "Hello!\n"
+ "======">>),
+ %% Underline can be +/- 2 characters.
+ [{section_title, #{level := 0}, <<"Hello!">>, _}] = parse(<<
+ "Hello!\n"
+ "====">>),
+ [{section_title, #{level := 0}, <<"Hello!">>, _}] = parse(<<
+ "Hello!\n"
+ "=====">>),
+ [{section_title, #{level := 0}, <<"Hello!">>, _}] = parse(<<
+ "Hello!\n"
+ "=======">>),
+ [{section_title, #{level := 0}, <<"Hello!">>, _}] = parse(<<
+ "Hello!\n"
+ "========">>),
+ %% Underline too short/long results in a different block.
+ ?NOT(section_title, parse(<<
+ "Hello!\n"
+ "===">>)),
+ ?NOT(section_title, parse(<<
+ "Hello!\n"
+ "=========">>)),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 21.2.1
+%%
+%% We currently do not implement the <xreflabel> value.
+%% I am also not sure what characters are allowed,
+%% so what is here is what I came up with guessing.
+block_id(St) ->
+ <<"[[", Line0/bits>> = read_line(St),
+ Line = trim(Line0),
+ Len = byte_size(Line) - 2,
+ <<BlockID:Len/binary, "]]">> = Line,
+ %% Make sure there are only valid characters.
+ {BlockID, <<>>} = while(fun(C) ->
+ (C =/= $,) andalso (C =/= $[) andalso (C =/= $])
+ andalso (C =/= $\s) andalso (C =/= $\t)
+ end, BlockID),
+ %% Good!
+ {block_id, #{id => BlockID}, <<>>, ann(St)}.
+
+-ifdef(TEST).
+block_id_test() ->
+ %% Valid.
+ [{block_id, #{id := <<"X30">>}, <<>>, _}] = parse(<<"[[X30]]">>),
+ %% Invalid.
+ ?NOT(block_id, parse(<<"[[block,id]]">>)),
+ ?NOT(block_id, parse(<<"[[block[id]]">>)),
+ ?NOT(block_id, parse(<<"[[block]id]]">>)),
+ ?NOT(block_id, parse(<<"[[block id]]">>)),
+ ?NOT(block_id, parse(<<"[[block\tid]]">>)),
+ %% Must be hard on the left of the line.
+ ?NOT(block_id, parse(<<" [[block_id]]">>)),
+ ?NOT(block_id, parse(<<"\t[[block_id]]">>)),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 21.2.3
+comment_line(St) ->
+ <<"//", Comment0/bits>> = read_line(St),
+ Comment = trim(Comment0),
+ %% Good!
+ {comment_line, #{<<"subs">> => <<"verbatim">>}, Comment, ann(St)}.
+
+-ifdef(TEST).
+comment_line_test() ->
+ [{comment_line, _, <<"This is a comment.">>, _}] = parse(<<"// This is a comment.">>),
+ %% We trim the whitespace around the comment.
+ [{comment_line, _, <<"This is a comment.">>, _}] = parse(<<"// This is a comment.">>),
+ [{comment_line, _, <<"This is a comment.">>, _}] = parse(<<"// This is a comment. ">>),
+ [{comment_line, _, <<"This is a comment.">>, _}] = parse(<<"//\tThis is a comment.">>),
+ [{comment_line, _, <<"This is a comment.">>, _}] = parse(<<"// This is a comment.\t">>),
+ [
+ {comment_line, _, <<"First line.">>, _},
+ {comment_line, _, <<"Second line.">>, _}
+ ] = parse(<<
+ "// First line.\n"
+ "// Second line.\n">>),
+ %% Must be hard on the left of the line.
+ ?NOT(comment_line, parse(<<" // This is a comment.">>)),
+ ?NOT(comment_line, parse(<<"\t// This is a comment.">>)),
+ ok.
+-endif.
+
+%% We currently implement the following block macros
+%% from the Asciidoc User Guide:
+%%
+%% - image (21.2.2)
+%% - include (21.3.1)
+%% - ifdef (21.3.2)
+%% - ifndef (21.3.2)
+%% - endif (21.3.2)
+block_macro(St) ->
+ Line0 = read_line(St),
+ Ann = ann(St),
+ %% Name must contain letters, digits or dash characters.
+ {Name, <<"::", Line1/bits>>} = while(fun(C) ->
+ ((C >= $a) andalso (C =< $z))
+ orelse ((C >= $A) andalso (C =< $Z))
+ orelse ((C >= $0) andalso (C =< $9))
+ orelse (C =:= $-)
+ end, Line0),
+ %% Name must not begin with a dash.
+ true = binary:at(Name, 0) =/= $-,
+ %% Target must not contain whitespace characters.
+ %% It is followed by an [attribute list].
+ {Target, AttrList0 = <<"[", _/bits>>} = while(fun(C) ->
+ (C =/= $[) andalso (C =/= $\s) andalso (C =/= $\t)
+ end, Line1),
+ AttrList1 = trim(AttrList0),
+ {attribute_list, AttrList, <<>>, _} = attribute_list(St, AttrList1),
+ %% Block macros must be followed by at least one empty line.
+ _ = empty_line(St),
+ {block_macro, AttrList#{
+ name => Name,
+ target => Target
+ }, <<>>, Ann}.
+
+-ifdef(TEST).
+block_macro_image_test() ->
+ [{block_macro, #{
+ name := <<"image">>,
+ target := <<"images/layout.png">>,
+ 1 := <<"J14P main circuit board">>
+ }, <<>>, _}] = parse(<<"image::images/layout.png[J14P main circuit board]">>),
+ [{block_macro, #{
+ name := <<"image">>,
+ target := <<"images/layout.png">>,
+ 1 := <<"J14P main circuit board">>,
+ <<"title">> := <<"Main circuit board">>
+ }, <<>>, _}] = parse(
+ <<"image::images/layout.png[\"J14P main circuit board\", "
+ "title=\"Main circuit board\"]">>),
+ ok.
+
+block_macro_include_test() ->
+ [{block_macro, #{
+ name := <<"include">>,
+ target := <<"chapter1.txt">>,
+ <<"tabsize">> := <<"4">>
+ }, <<>>, _}] = parse(<<"include::chapter1.txt[tabsize=4]">>),
+ ok.
+
+block_macro_ifdef_test() ->
+ [{block_macro, #{
+ name := <<"ifdef">>,
+ target := <<"revnumber">>,
+ 0 := <<>>
+ }, <<>>, _}] = parse(<<"ifdef::revnumber[]">>),
+ [{block_macro, #{
+ name := <<"ifdef">>,
+ target := <<"revnumber">>,
+ 1 := <<"Version number 42">>
+ }, <<>>, _}] = parse(<<"ifdef::revnumber[Version number 42]">>),
+ ok.
+
+block_macro_ifndef_test() ->
+ [{block_macro, #{
+ name := <<"ifndef">>,
+ target := <<"revnumber">>,
+ 0 := <<>>
+ }, <<>>, _}] = parse(<<"ifndef::revnumber[]">>),
+ ok.
+
+block_macro_endif_test() ->
+ [{block_macro, #{
+ name := <<"endif">>,
+ target := <<"revnumber">>,
+ 0 := <<>>
+ }, <<>>, _}] = parse(<<"endif::revnumber[]">>),
+ %% Some macros accept an empty target.
+ [{block_macro, #{
+ name := <<"endif">>,
+ target := <<>>,
+ 0 := <<>>
+ }, <<>>, _}] = parse(<<"endif::[]">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 17.1
+bulleted_list(St) ->
+ Line0 = read_line(St),
+ Line1 = trim(Line0),
+ {Type0, Level, ListItem} = case Line1 of
+ <<"-", C, R/bits>> when ?IS_WS(C) -> {dash, 1, R};
+ <<"*", C, R/bits>> when ?IS_WS(C) -> {star, 1, R};
+ <<"**", C, R/bits>> when ?IS_WS(C) -> {star, 2, R};
+ <<"***", C, R/bits>> when ?IS_WS(C) -> {star, 3, R};
+ <<"****", C, R/bits>> when ?IS_WS(C) -> {star, 4, R};
+ <<"*****", C, R/bits>> when ?IS_WS(C) -> {star, 5, R}
+ end,
+ Type = case Type0 of
+ dash -> bulleted_alt;
+ star -> bulleted
+ end,
+ list_item(St, #{
+ type => Type,
+ level => Level
+ }, ListItem).
+
+-ifdef(TEST).
+bulleted_list_test() ->
+ [{list_item, #{
+ type := bulleted_alt,
+ level := 1
+ }, [{paragraph, _, <<"List item.">>, _}], _}] = parse(<<"- List item.">>),
+ [{list_item, #{
+ type := bulleted,
+ level := 1
+ }, [{paragraph, _, <<"List item.">>, _}], _}] = parse(<<"* List item.">>),
+ [{list_item, #{
+ type := bulleted,
+ level := 2
+ }, [{paragraph, _, <<"List item.">>, _}], _}] = parse(<<"** List item.">>),
+ [{list_item, #{
+ type := bulleted,
+ level := 3
+ }, [{paragraph, _, <<"List item.">>, _}], _}] = parse(<<"*** List item.">>),
+ [{list_item, #{
+ type := bulleted,
+ level := 4
+ }, [{paragraph, _, <<"List item.">>, _}], _}] = parse(<<"**** List item.">>),
+ [{list_item, #{
+ type := bulleted,
+ level := 5
+ }, [{paragraph, _, <<"List item.">>, _}], _}] = parse(<<"***** List item.">>),
+ %% Two list items one after the other.
+ [
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, _, <<"List item 1.">>, _}], _},
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, _, <<"List item 2.">>, _}], _}
+ ] = parse(<<"* List item 1.\n* List item 2.">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 17.2
+%%
+%% We currently only implement implicit numbering.
+numbered_list(St) ->
+ Line0 = read_line(St),
+ Line1 = trim(Line0),
+ {Level, ListItem} = case Line1 of
+ <<".", C, R/bits>> when ?IS_WS(C) -> {1, R};
+ <<"..", C, R/bits>> when ?IS_WS(C) -> {2, R};
+ <<"...", C, R/bits>> when ?IS_WS(C) -> {3, R};
+ <<"....", C, R/bits>> when ?IS_WS(C) -> {4, R};
+ <<".....", C, R/bits>> when ?IS_WS(C) -> {5, R}
+ end,
+ list_item(St, #{
+ type => numbered,
+ level => Level
+ }, ListItem).
+
+-ifdef(TEST).
+numbered_list_test() ->
+ [{list_item, #{
+ type := numbered,
+ level := 1
+ }, [{paragraph, _, <<"Arabic (decimal) numbered list item.">>, _}], _}]
+ = parse(<<". Arabic (decimal) numbered list item.">>),
+ [{list_item, #{
+ type := numbered,
+ level := 2
+ }, [{paragraph, _, <<"Lower case alpha (letter) numbered list item.">>, _}], _}]
+ = parse(<<".. Lower case alpha (letter) numbered list item.">>),
+ [{list_item, #{
+ type := numbered,
+ level := 3
+ }, [{paragraph, _, <<"Lower case roman numbered list item.">>, _}], _}]
+ = parse(<<"... Lower case roman numbered list item.">>),
+ [{list_item, #{
+ type := numbered,
+ level := 4
+ }, [{paragraph, _, <<"Upper case alpha (letter) numbered list item.">>, _}], _}]
+ = parse(<<".... Upper case alpha (letter) numbered list item.">>),
+ [{list_item, #{
+ type := numbered,
+ level := 5
+ }, [{paragraph, _, <<"Upper case roman numbered list item.">>, _}], _}]
+ = parse(<<"..... Upper case roman numbered list item.">>),
+ %% Two list items one after the other.
+ [
+ {list_item, #{type := numbered, level := 1},
+ [{paragraph, _, <<"List item 1.">>, _}], _},
+ {list_item, #{type := numbered, level := 1},
+ [{paragraph, _, <<"List item 2.">>, _}], _}
+ ] = parse(<<". List item 1.\n. List item 2.">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 17.3
+%%
+%% The Asciidoc User Guide makes it sound like the
+%% label must be hard on the left margin but we don't
+%% enforce that to simplify the implementation.
+labeled_list(St) ->
+ Line0 = read_line(St),
+ %% We can't match directly to find the list separator,
+ %% we have to search for it.
+ {Label0, Sep, ListItem0} = find_labeled_list(Line0),
+ Label = trim(Label0),
+ ListItem = trim(ListItem0),
+ %% The label must not be empty.
+ true = trim(Label) =/= <<>>,
+ list_item(St, #{
+ type => labeled,
+ separator => Sep,
+ label => Label
+ }, ListItem).
+
+find_labeled_list(Line) ->
+ find_labeled_list(Line, <<>>).
+
+%% We don't have a final clause with an empty binary because
+%% we want to crash if we don't find a labeled list.
+find_labeled_list(<<"::">>, Acc) -> {Acc, <<"::">>, <<>>};
+find_labeled_list(<<":::">>, Acc) -> {Acc, <<":::">>, <<>>};
+find_labeled_list(<<"::::">>, Acc) -> {Acc, <<"::::">>, <<>>};
+find_labeled_list(<<";;">>, Acc) -> {Acc, <<";;">>, <<>>};
+find_labeled_list(<<"::", C, R/bits>>, Acc) when ?IS_WS(C) -> {Acc, <<"::">>, R};
+find_labeled_list(<<":::", C, R/bits>>, Acc) when ?IS_WS(C) -> {Acc, <<":::">>, R};
+find_labeled_list(<<"::::", C, R/bits>>, Acc) when ?IS_WS(C) -> {Acc, <<"::::">>, R};
+find_labeled_list(<<";;", C, R/bits>>, Acc) when ?IS_WS(C) -> {Acc, <<";;">>, R};
+find_labeled_list(<<C, R/bits>>, Acc) -> find_labeled_list(R, <<Acc/binary, C>>).
+
+-ifdef(TEST).
+labeled_list_test() ->
+ [{list_item, #{type := labeled, separator := <<"::">>, label := <<"Question">>},
+ [{paragraph, _, <<"Answer!">>, _}], _}] = parse(<<"Question:: Answer!">>),
+ [{list_item, #{type := labeled, separator := <<"::">>, label := <<"Question">>},
+ [{paragraph, _, <<"Answer!">>, _}], _}] = parse(<<"Question::\n Answer!">>),
+ %% Long snippet from the Asciidoc User Guide, minus literal paragraph.
+ %% @todo Add the literal paragraph back once they are implemented.
+ [
+ {list_item, #{type := labeled, separator := <<"::">>, label := <<"In">>},
+ [{paragraph, _, <<>>, _}], _},
+ {list_item, #{type := labeled, separator := <<"::">>, label := <<"Lorem">>},
+ [{paragraph, _, <<"Fusce euismod commodo velit.">>, _}], _},
+ {list_item, #{type := labeled, separator := <<"::">>, label := <<"Ipsum">>},
+ [{paragraph, _, <<"Vivamus fringilla mi eu lacus.">>, _}], _},
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, _, <<"Vivamus fringilla mi eu lacus.">>, _}], _},
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, _, <<"Donec eget arcu bibendum nunc consequat lobortis.">>, _}], _},
+ {list_item, #{type := labeled, separator := <<"::">>, label := <<"Dolor">>},
+ [{paragraph, _, <<"Donec eget arcu bibendum nunc consequat lobortis.">>, _}], _},
+ {list_item, #{type := labeled, separator := <<";;">>, label := <<"Suspendisse">>},
+ [{paragraph, _, <<"A massa id sem aliquam auctor.">>, _}], _},
+ {list_item, #{type := labeled, separator := <<";;">>, label := <<"Morbi">>},
+ [{paragraph, _, <<"Pretium nulla vel lorem.">>, _}], _},
+ {list_item, #{type := labeled, separator := <<";;">>, label := <<"In">>},
+ [{paragraph, _, <<"Dictum mauris in urna.">>, _}], _},
+ {list_item, #{type := labeled, separator := <<":::">>, label := <<"Vivamus">>},
+ [{paragraph, _, <<"Fringilla mi eu lacus.">>, _}], _},
+ {list_item, #{type := labeled, separator := <<":::">>, label := <<"Donec">>},
+ [{paragraph, _, <<"Eget arcu bibendum nunc consequat lobortis.">>, _}], _}
+ ] = parse(<<
+ "In::\n"
+ "Lorem::\n"
+ " Fusce euismod commodo velit.\n"
+ %% @todo Add literal paragraph back here.
+ "Ipsum:: Vivamus fringilla mi eu lacus.\n"
+ " * Vivamus fringilla mi eu lacus.\n"
+ " * Donec eget arcu bibendum nunc consequat lobortis.\n"
+ "Dolor::\n"
+ " Donec eget arcu bibendum nunc consequat lobortis.\n"
+ " Suspendisse;;\n"
+ " A massa id sem aliquam auctor.\n"
+ " Morbi;;\n"
+ " Pretium nulla vel lorem.\n"
+ " In;;\n"
+ " Dictum mauris in urna.\n"
+ " Vivamus::: Fringilla mi eu lacus.\n"
+ " Donec::: Eget arcu bibendum nunc consequat lobortis.\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 20
+-spec callout_list(_) -> no_return().
+callout_list(St) -> throw({not_implemented, St}). %% @todo
+
+%% Asciidoc User Guide 17
+%%
+%% We do not apply rules about blocks being contained in
+%% the list item at this stage of parsing. We only concern
+%% ourselves with identifying blocks, and then another pass
+%% will build a tree from the result of this pass.
+list_item(St, Attrs, ListItem0) ->
+ ListItem1 = trim(ListItem0),
+ Ann = ann(St),
+ %% For labeled lists, we may need to skip empty lines
+ %% until the start of the list item contents, since
+ %% it can begin on a separate line from the label.
+ _ = case {ListItem1, Attrs} of
+ {<<>>, #{type := labeled}} ->
+ read_while(St, fun skip_empty_lines/1, <<>>);
+ _ ->
+ ok
+ end,
+ %% A list item ends on end of file, empty line or when a new list starts.
+ %% Any indentation is optional and therefore removed.
+ ListItem = read_while(St, fun fold_list_item/1, ListItem1),
+ {list_item, Attrs, [{paragraph, #{}, ListItem, Ann}], Ann}.
+
+skip_empty_lines(eof) ->
+ done;
+skip_empty_lines(Line) ->
+ case trim(Line) of
+ <<>> -> {more, <<>>};
+ _ -> done
+ end.
+
+fold_list_item(eof) ->
+ done;
+fold_list_item(Line0) ->
+ case trim(Line0) of
+ <<>> -> done;
+ <<"+">> -> done;
+ <<"//", _/bits >> -> done;
+ <<"-", C, _/bits>> when ?IS_WS(C) -> done;
+ <<"*", C, _/bits>> when ?IS_WS(C) -> done;
+ <<"**", C, _/bits>> when ?IS_WS(C) -> done;
+ <<"***", C, _/bits>> when ?IS_WS(C) -> done;
+ <<"****", C, _/bits>> when ?IS_WS(C) -> done;
+ <<"*****", C, _/bits>> when ?IS_WS(C) -> done;
+ <<".", C, _/bits>> when ?IS_WS(C) -> done;
+ <<"..", C, _/bits>> when ?IS_WS(C) -> done;
+ <<"...", C, _/bits>> when ?IS_WS(C) -> done;
+ <<"....", C, _/bits>> when ?IS_WS(C) -> done;
+ <<".....", C, _/bits>> when ?IS_WS(C) -> done;
+ Line ->
+ try find_labeled_list(Line) of
+ {_, _, _} -> done
+ catch _:_ ->
+ {more, Line}
+ end
+ end.
+
+-ifdef(TEST).
+list_item_test() ->
+ [
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, #{}, <<"List item.">>, _}], _},
+ {list_item, #{type := bulleted, level := 2},
+ [{paragraph, #{}, <<"List item.">>, _}], _},
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, #{}, <<"List item.">>, _}], _},
+ {list_item, #{type := numbered, level := 1},
+ [{paragraph, #{}, <<"List item.">>, _}], _},
+ {list_item, #{type := numbered, level := 1},
+ [{paragraph, #{}, <<"List item.">>, _}], _},
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, #{}, <<"List item.">>, _}], _}
+ ] = parse(<<
+ "* List item.\n"
+ "** List item.\n"
+ "* List item.\n"
+ " . List item.\n"
+ " . List item.\n"
+ "* List item.\n">>),
+ %% Properly detect a labeled list.
+ [
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, #{}, <<"List item.\nMultiline.">>, _}], _},
+ {list_item, #{type := labeled, label := <<"Question">>},
+ [{paragraph, #{}, <<"Answer!">>, _}], _}
+ ] = parse(<<
+ "* List item.\n"
+ "Multiline.\n"
+ "Question:: Answer!\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 17.7
+list_item_continuation(St) ->
+ %% Continuations are a single + hard against the left margin.
+ <<$+, Whitespace/bits>> = read_line(St),
+ <<>> = trim(Whitespace),
+ {list_item_continuation, #{}, <<>>, ann(St)}.
+
+-ifdef(TEST).
+list_item_continuation_test() ->
+ [{list_item_continuation, _, _, _}] = parse(<<"+">>),
+ [{list_item_continuation, _, _, _}] = parse(<<"+ ">>),
+ [{list_item_continuation, _, _, _}] = parse(<<"+\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16.2
+listing_block(St) ->
+ delimited_block(St, listing_block, $-, #{<<"subs">> => <<"verbatim">>}).
+
+-ifdef(TEST).
+listing_block_test() ->
+ Block = <<
+ "#include <stdio.h>\n"
+ "\n"
+ "int main() {\n"
+ " printf(\"Hello World!\n\");\n"
+ " exit(0);\n"
+ "}">>,
+ [{listing_block, _, Block, _}] = parse(<<
+ "--------------------------------------\n",
+ Block/binary, "\n"
+ "--------------------------------------\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16.3
+literal_block(St) ->
+ delimited_block(St, literal_block, $., #{<<"subs">> => <<"verbatim">>}).
+
+-ifdef(TEST).
+literal_block_test() ->
+ Block = <<
+ "Consul *necessitatibus* per id,\n"
+ "consetetur, eu pro everti postulant\n"
+ "homero verear ea mea, qui.">>,
+ [{literal_block, _, Block, _}] = parse(<<
+ "...................................\n",
+ Block/binary, "\n"
+ "...................................\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16.4
+sidebar_block(St) ->
+ delimited_block(St, sidebar_block, $*).
+
+-ifdef(TEST).
+sidebar_block_test() ->
+ Block = <<
+ "Any AsciiDoc SectionBody element (apart from\n"
+ "SidebarBlocks) can be placed inside a sidebar.">>,
+ [{sidebar_block, _, Block, _}] = parse(<<
+ "************************************************\n",
+ Block/binary, "\n"
+ "************************************************\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16.5
+comment_block(St) ->
+ delimited_block(St, comment_block, $/).
+
+-ifdef(TEST).
+comment_block_test() ->
+ Block = <<
+ "CommentBlock contents are not processed by\n"
+ "asciidoc(1).">>,
+ [{comment_block, _, Block, _}] = parse(<<
+ "//////////////////////////////////////////\n",
+ Block/binary, "\n"
+ "//////////////////////////////////////////\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16.6
+passthrough_block(St) ->
+ delimited_block(St, passthrough_block, $+).
+
+-ifdef(TEST).
+passthrough_block_test() ->
+ Block = <<
+ "<table border=\"1\"><tr>\n"
+ " <td>*Cell 1*</td>\n"
+ " <td>*Cell 2*</td>\n"
+ "</tr></table>">>,
+ [{passthrough_block, _, Block, _}] = parse(<<
+ "++++++++++++++++++++++++++++++++++++++\n",
+ Block/binary, "\n"
+ "++++++++++++++++++++++++++++++++++++++\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16.7
+quote_block(St) ->
+ delimited_block(St, quote_block, $_).
+
+-ifdef(TEST).
+quote_block_test() ->
+ Block = <<
+ "As he spoke there was the sharp sound of horses' hoofs and\n"
+ "grating wheels against the curb, followed by a sharp pull at the\n"
+ "bell. Holmes whistled.\n"
+ "\n"
+ "\"A pair, by the sound,\" said he. \"Yes,\" he continued, glancing\n"
+ "out of the window. \"A nice little brougham and a pair of\n"
+ "beauties. A hundred and fifty guineas apiece. There's money in\n"
+ "this case, Watson, if there is nothing else.\"">>,
+ [{quote_block, _, Block, _}] = parse(<<
+ "____________________________________________________________________\n",
+ Block/binary, "\n"
+ "____________________________________________________________________\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16.8
+example_block(St) ->
+ delimited_block(St, example_block, $=).
+
+-ifdef(TEST).
+example_block_test() ->
+ Block = <<
+ "Qui in magna commodo, est labitur dolorum an. Est ne magna primis\n"
+ "adolescens.">>,
+ [{example_block, _, Block, _}] = parse(<<
+ "=====================================================================\n",
+ Block/binary, "\n"
+ "=====================================================================\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16
+delimited_block(St, Name, Char) ->
+ delimited_block(St, Name, Char, #{}, <<Char, Char, Char, Char>>).
+
+delimited_block(St, Name, Char, Attrs) ->
+ delimited_block(St, Name, Char, Attrs, <<Char, Char, Char, Char>>).
+
+delimited_block(St, Name, Char, Attrs, Four) ->
+ %% A delimiter block begins by a series of four or more repeated characters.
+ <<Four:4/binary, Line0/bits>> = read_line(St),
+ Ann = ann(St),
+ Line = trim(Line0, trailing),
+ repeats(Line, Char),
+ %% Get the content of the block as-is.
+ Block = read_while(St, fun(L) -> fold_delimited_block(L, Four, Char) end, <<>>),
+ %% Skip the trailing delimiter line.
+ _ = read_line(St),
+ {Name, Attrs, Block, Ann}.
+
+%% Accept eof as a closing delimiter.
+fold_delimited_block(eof, _, _) ->
+ done;
+fold_delimited_block(Line0, Four, Char) ->
+ case Line0 of
+ <<Four:4/binary, Line1/bits>> ->
+ try
+ Line = trim(Line1, trailing),
+ repeats(Line, Char),
+ done
+ catch _:_ ->
+ {more, Line0}
+ end;
+ _ ->
+ {more, Line0}
+ end.
+
+-ifdef(TEST).
+delimited_block_test() ->
+ %% Confirm that the block ends at eof.
+ %%
+ %% We see an extra line break because asciideck_line_reader adds
+ %% one at the end of every files to ease processing.
+ [{listing_block, _, <<"Hello!\n\n">>, _}] = parse(<<
+ "----\n"
+ "Hello!\n">>),
+ %% Same without a trailing line break.
+ %%
+ %% We also see an extra line break for the aforementioned reasons.
+ [{listing_block, _, <<"Hello!\n">>, _}] = parse(<<
+ "----\n"
+ "Hello!">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 16.10
+-spec open_block(_) -> no_return().
+open_block(St) -> throw({not_implemented, St}). %% @todo
+
+%% Asciidoc User Guide 23
+%%
+%% We do not parse the table in this pass. Instead we
+%% treat it like any other delimited block.
+table(St) ->
+ delimited_block(St, table, $=, #{}, <<"|===">>).
+
+-ifdef(TEST).
+table_test() ->
+ Block = <<
+ "|1 |2 |A\n"
+ "|3 |4 |B\n"
+ "|5 |6 |C">>,
+ [{table, _, Block, _}] = parse(<<
+ "|=======\n",
+ Block/binary, "\n"
+ "|=======\n">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 28
+-spec attribute_entry(_) -> no_return().
+attribute_entry(St) -> throw({not_implemented, St}). %% @todo
+
+%% Asciidoc User Guide 14, 29
+attribute_list(St) ->
+ AttrList = read_line(St),
+ attribute_list(St, AttrList).
+
+attribute_list(St, AttrList0) ->
+ %% First we remove the enclosing square brackets.
+ <<$[, AttrList1/bits>> = AttrList0,
+ AttrList2 = trim(AttrList1),
+ Len = byte_size(AttrList2) - 1,
+ <<AttrList3:Len/binary, $]>> = AttrList2,
+ AttrList = asciideck_attributes_parser:parse(AttrList3),
+ {attribute_list, AttrList, <<>>, ann(St)}.
+
+-ifdef(TEST).
+attribute_list_test() ->
+ [{attribute_list, #{0 := <<"Hello">>, 1 := <<"Hello">>}, <<>>, _}]
+ = parse(<<"[Hello]">>),
+ [{attribute_list, #{
+ 1 := <<"quote">>,
+ 2 := <<"Bertrand Russell">>,
+ 3 := <<"The World of Mathematics (1956)">>
+ }, <<>>, _}]
+ = parse(<<"[quote, Bertrand Russell, The World of Mathematics (1956)]">>),
+ [{attribute_list, #{
+ 1 := <<"22 times">>,
+ <<"backcolor">> := <<"#0e0e0e">>,
+ <<"options">> := <<"noborders,wide">>
+ }, <<>>, _}]
+ = parse(<<"[\"22 times\", backcolor=\"#0e0e0e\", options=\"noborders,wide\"]">>),
+ [{attribute_list, #{
+ 1 := <<"A footnote&#44; &#34;with an image&#34; image:smallnew.png[]">>
+ }, <<>>, _}]
+ = parse(<<"[A footnote&#44; &#34;with an image&#34; image:smallnew.png[]]">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 12
+block_title(St) ->
+ %% A block title line begins with a period and is followed by the title text.
+ <<$., Title0/bits>> = read_line(St),
+ Ann = ann(St),
+ Title = trim(Title0),
+ {block_title, #{}, Title, Ann}.
+
+-ifdef(TEST).
+block_title_test() ->
+ %% Valid.
+ [{block_title, _, <<"Notes">>, _}] = parse(<<".Notes">>),
+ [{block_title, _, <<"Notes">>, _}] = parse(<<".Notes ">>),
+ %% Invalid.
+ ?NOT(block_title, parse(<<". Notes">>)),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 15.2
+-spec literal_para(_) -> no_return().
+literal_para(St) -> throw({not_implemented, St}). %% @todo
+
+%% Asciidoc User Guide 15.4
+-spec admonition_para(_) -> no_return().
+admonition_para(St) -> throw({not_implemented, St}). %% @todo
+
+%% Asciidoc User Guide 15.1
+para(St) ->
+ %% Paragraph must be hard against the left margin.
+ <<C, _/bits>> = Para0 = read_line(St),
+ Ann = ann(St),
+ %% @todo Uncomment this line once everything else has been implemented.
+ _ = ?IS_WS(C), % false = ?IS_WS(C),
+ Para1 = trim(Para0),
+ %% Paragraph ends at blank line, end of file or start of delimited block or list.
+ Para = read_while(St, fun fold_para/1, Para1),
+ {paragraph, #{}, Para, Ann}.
+
+fold_para(eof) ->
+ done;
+fold_para(Line0) ->
+ case trim(Line0) of
+ <<>> -> done;
+ <<"+">> -> done;
+ %% @todo Detect delimited block or list.
+ Line -> {more, Line}
+ end.
+
+-ifdef(TEST).
+para_test() ->
+ LoremIpsum = <<
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit,\n"
+ "sed do eiusmod tempor incididunt ut labore et dolore\n"
+ "magna aliqua. Ut enim ad minim veniam, quis nostrud\n"
+ "exercitation ullamco laboris nisi ut aliquip ex ea\n"
+ "commodo consequat. Duis aute irure dolor in reprehenderit\n"
+ "in voluptate velit esse cillum dolore eu fugiat nulla\n"
+ "pariatur. Excepteur sint occaecat cupidatat non proident,\n"
+ "sunt in culpa qui officia deserunt mollit anim id est laborum."
+ >>,
+ %% Paragraph followed by end of file.
+ [{paragraph, _, LoremIpsum, _}] = parse(<< LoremIpsum/binary, "\n">>),
+ %% Paragraph followed by end of file with no trailing line break..
+ [{paragraph, _, LoremIpsum, _}] = parse(LoremIpsum),
+ %% Two paragraphs.
+ [{paragraph, _, LoremIpsum, _}, {paragraph, _, LoremIpsum, _}]
+ = parse(<<
+ LoremIpsum/binary,
+ "\n\n",
+ LoremIpsum/binary >>),
+ ok.
+-endif.
+
+%% Control functions.
+
+oneof([], St) ->
+ throw({error, St}); %% @todo
+oneof([Parse|Tail], St=#state{reader=ReaderPid}) ->
+ Ln = asciideck_line_reader:get_position(ReaderPid),
+ try
+ Parse(St)
+ catch _:_ ->
+ asciideck_line_reader:set_position(ReaderPid, Ln),
+ oneof(Tail, St)
+ end.
+
+skip(Parse, St=#state{reader=ReaderPid}) ->
+ Ln = asciideck_line_reader:get_position(ReaderPid),
+ try
+ _ = Parse(St),
+ skip(Parse, St)
+ catch _:_ ->
+ asciideck_line_reader:set_position(ReaderPid, Ln),
+ ok
+ end.
+
+%% Line functions.
+
+read_line(#state{reader=ReaderPid}) ->
+ asciideck_line_reader:read_line(ReaderPid).
+
+read_while(St=#state{reader=ReaderPid}, F, Acc) ->
+ Ln = asciideck_line_reader:get_position(ReaderPid),
+ case F(read_line(St)) of
+ done ->
+ asciideck_line_reader:set_position(ReaderPid, Ln),
+ Acc;
+ {more, Line} ->
+ case Acc of
+ <<>> -> read_while(St, F, Line);
+ _ -> read_while(St, F, <<Acc/binary, $\n, Line/binary>>)
+ end
+ end.
+
+ann(#state{reader=ReaderPid}) ->
+ #{line => asciideck_line_reader:get_position(ReaderPid)}.
+
+trim(Line) ->
+ trim(Line, both).
+
+trim(Line, Direction) ->
+ Regex = case Direction of
+ both -> "^[ \\t\\r\\n]+|[ \\t\\r\\n]+$";
+ trailing -> "[ \\t\\r\\n]+$"
+ end,
+ iolist_to_binary(re:replace(Line, Regex, <<>>, [global])).
+
+repeats(<<>>, _) -> ok;
+repeats(<<C, Rest/bits>>, C) -> repeats(Rest, C).
+
+while(F, Bin) ->
+ while(Bin, F, <<>>).
+
+while(<<>>, _, Acc) ->
+ {Acc, <<>>};
+while(<<C, R/bits>>, F, Acc) ->
+ case F(C) of
+ true -> while(R, F, <<Acc/binary, C>>);
+ false -> {Acc, <<C, R/bits>>}
+ end.
diff --git a/src/asciideck_inline_pass.erl b/src/asciideck_inline_pass.erl
new file mode 100644
index 0000000..3ed79b1
--- /dev/null
+++ b/src/asciideck_inline_pass.erl
@@ -0,0 +1,308 @@
+%% Copyright (c) 2017-2018, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% This pass walks over the tree and parses inline elements.
+-module(asciideck_inline_pass).
+
+-export([run/1]).
+
+-import(asciideck_block_parser, [trim/1, while/2]).
+
+-type inline_ast() :: list(). %% @todo
+-export_type([inline_ast/0]).
+
+run([]) ->
+ [];
+run([Data|Tail]) when is_binary(Data) ->
+ [inline(Data)|run(Tail)];
+%% We do not do any inline formatting for verbatim blocks,
+%% for example listing blocks.
+%%
+%% @todo subs is a list of values.
+run([Item={_, #{<<"subs">> := <<"verbatim">>}, _, _}|Tail]) ->
+ [Item|run(Tail)];
+%% Labeled lists' labels can also have inline formatting.
+run([{Type, Attrs=#{label := Label}, Items, Ann}|Tail]) when is_list(Items) ->
+ [{Type, Attrs#{label => inline(Label)}, run(Items), Ann}|run(Tail)];
+run([{Type, Attrs, Items, Ann}|Tail]) when is_list(Items) ->
+ [{Type, Attrs, run(Items), Ann}|run(Tail)];
+run([{Type, Attrs, Data, Ann}|Tail]) ->
+ [{Type, Attrs, inline(Data), Ann}|run(Tail)].
+
+%% We reduce inline content with a single text element
+%% with no formatting to a simple binary.
+inline(<<>>) ->
+ <<>>;
+inline(Data) ->
+ case inline(Data, <<>>, []) of
+ [] -> <<>>;
+ [Text] when is_binary(Text) -> Text;
+ AST -> AST
+ end.
+
+-spec inline(binary(), binary(), inline_ast()) -> inline_ast().
+inline(<<>>, <<>>, Acc) ->
+ lists:reverse(Acc);
+inline(<<>>, BinAcc, Acc) ->
+ lists:reverse([BinAcc|Acc]);
+inline(Data, BinAcc, Acc) ->
+ oneof(Data, BinAcc, Acc, [
+ %% Links.
+ fun xref/2,
+ fun link/2,
+ fun http_link/2,
+ fun https_link/2,
+ %% Quoted text.
+ fun emphasized_single_quote/2,
+ fun emphasized_underline/2,
+ fun strong/2,
+ %% Passthrough macros.
+ fun inline_literal_passthrough/2
+ ]).
+
+%% The inline pass replaces \r\n and \n with a simple space
+%% when it occurs within normal text.
+oneof(<<$\r, $\n, Rest/bits>>, BinAcc, Acc, []) ->
+ inline(Rest, <<BinAcc/binary, $\s>>, Acc);
+oneof(<<$\n, Rest/bits>>, BinAcc, Acc, []) ->
+ inline(Rest, <<BinAcc/binary, $\s>>, Acc);
+oneof(<<C, Rest/bits>>, BinAcc, Acc, []) ->
+ inline(Rest, <<BinAcc/binary, C>>, Acc);
+oneof(Data, BinAcc, Acc, [Parse|Tail]) ->
+ Prev = case BinAcc of
+ <<>> -> undefined;
+ _ -> binary:last(BinAcc)
+ end,
+ try Parse(Data, Prev) of
+ {ok, Inline, Rest} when BinAcc =:= <<>> ->
+ inline(Rest, BinAcc, [Inline|Acc]);
+ {ok, Inline, Rest} ->
+ inline(Rest, <<>>, [Inline, BinAcc|Acc]);
+ {skip, Text, Rest} ->
+ oneof(Rest, <<BinAcc/binary, Text/binary>>, Acc, Tail)
+ catch _:_ ->
+ oneof(Data, BinAcc, Acc, Tail)
+ end.
+
+-ifdef(TEST).
+text_test() ->
+ <<>> = inline(<<>>),
+ <<"Hello, Robert">> = inline(<<"Hello, Robert">>),
+ ok.
+-endif.
+
+-define(IS_BOUNDARY(C), C =:= undefined; C =:= $\s; C =:= $\t; C =:= $\r; C =:= $\n; C =:= $().
+
+%% Asciidoc User Guide 21.2.1
+%%
+%% We currently do not implement the <<...>> form.
+xref(<<"xref:", IDAndCaption/bits>>, Prev) when ?IS_BOUNDARY(Prev) ->
+ %% ID must not contain whitespace characters.
+ {ID, <<"[", Caption0/bits>>} = while(fun(C) ->
+ (C =/= $[) andalso (C =/= $\s) andalso (C =/= $\t)
+ end, IDAndCaption),
+ %% It is followed by a caption.
+ {Caption1, <<"]", Rest/bits>>} = while(fun(C) ->
+ C =/= $]
+ end, Caption0),
+ Caption = trim(Caption1),
+ {ok, {xref, #{
+ id => ID
+ }, Caption, inline}, Rest}.
+
+-ifdef(TEST).
+xref_test() ->
+ [{xref, #{
+ id := <<"tiger_image">>
+ }, <<"face of a tiger">>, _}] = inline(<<"xref:tiger_image[face of a tiger]">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 21.1.3
+link(<<"link:", TargetAndCaption/bits>>, Prev) when ?IS_BOUNDARY(Prev) ->
+ %% Target must not contain whitespace characters.
+ {Target, <<"[", Caption0/bits>>} = while(fun(C) ->
+ (C =/= $[) andalso (C =/= $\s) andalso (C =/= $\t)
+ andalso (C =/= $\r) andalso (C =/= $\n)
+ end, TargetAndCaption),
+ %% It is followed by a caption.
+ {Caption1, <<"]", Rest/bits>>} = while(fun(C) ->
+ C =/= $]
+ end, Caption0),
+ Caption = trim(Caption1),
+ {ok, {link, #{
+ target => Target
+ }, Caption, inline}, Rest}.
+
+-ifdef(TEST).
+link_test() ->
+ [{link, #{
+ target := <<"downloads/foo.zip">>
+ }, <<"download foo.zip">>, _}] = inline(<<"link:downloads/foo.zip[download foo.zip]">>),
+ [{link, #{
+ target := <<"chapter1.asciidoc#fragment">>
+ }, <<"Chapter 1.">>, _}] = inline(<<"link:chapter1.asciidoc#fragment[Chapter 1.]">>),
+ [
+ {link, #{target := <<"first.zip">>}, <<"first">>, _},
+ <<", ">>,
+ {link, #{target := <<"second.zip">>}, <<"second">>, _}
+ ] = inline(<<"link:first.zip[first],\nlink:second.zip[second]">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 21.1.3
+http_link(<<"http:", Rest/bits>>, Prev) when ?IS_BOUNDARY(Prev) ->
+ direct_link(Rest, <<"http:">>).
+
+direct_link(Data, Prefix) ->
+ %% Target must not contain whitespace characters.
+ {Target0, Rest0} = while(fun(C) ->
+ (C =/= $[) andalso (C =/= $\s) andalso (C =/= $\t)
+ andalso (C =/= $\r) andalso (C =/= $\n)
+ end, Data),
+ Target = <<Prefix/binary, Target0/binary>>,
+ %% It is optionally followed by a caption. Otherwise
+ %% the link itself is the caption.
+ case Rest0 of
+ <<"[", Caption0/bits>> ->
+ {Caption1, <<"]", Rest/bits>>} = while(fun(C) ->
+ C =/= $]
+ end, Caption0),
+ Caption = trim(Caption1),
+ {ok, {link, #{
+ target => Target
+ }, Caption, inline}, Rest};
+ _ ->
+ {ok, {link, #{
+ target => Target
+ }, Target, inline}, Rest0}
+ end.
+
+-ifdef(TEST).
+http_link_test() ->
+ [
+ <<"If you have ">>,
+ {link, #{
+ target := <<"http://example.org/hello#fragment">>
+ }, <<"http://example.org/hello#fragment">>, _},
+ <<" then:">>
+ ] = inline(<<"If you have http://example.org/hello#fragment then:">>),
+ [
+ <<"If you have ">>,
+ {link, #{
+ target := <<"http://example.org/hello#fragment">>
+ }, <<"http://example.org/hello#fragment">>, _},
+ <<" then:">>
+ ] = inline(<<"If you have http://example.org/hello#fragment\nthen:">>),
+ [
+ <<"Oh, ">>,
+ {link, #{
+ target := <<"http://example.org/hello#fragment">>
+ }, <<"hello there">>, _},
+ <<", young lad.">>
+ ] = inline(<<"Oh, http://example.org/hello#fragment[hello there], young lad.">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 21.1.3
+https_link(<<"https:", Rest/bits>>, Prev) when ?IS_BOUNDARY(Prev) ->
+ direct_link(Rest, <<"https:">>).
+
+-ifdef(TEST).
+https_link_test() ->
+ [
+ <<"If you have ">>,
+ {link, #{
+ target := <<"https://example.org/hello#fragment">>
+ }, <<"https://example.org/hello#fragment">>, _},
+ <<" then:">>
+ ] = inline(<<"If you have https://example.org/hello#fragment then:">>),
+ [
+ <<"If you have ">>,
+ {link, #{
+ target := <<"https://example.org/hello#fragment">>
+ }, <<"https://example.org/hello#fragment">>, _},
+ <<" then:">>
+ ] = inline(<<"If you have https://example.org/hello#fragment\nthen:">>),
+ [
+ <<"Oh, ">>,
+ {link, #{
+ target := <<"https://example.org/hello#fragment">>
+ }, <<"hello there">>, _},
+ <<", young lad.">>
+ ] = inline(<<"Oh, https://example.org/hello#fragment[hello there], young lad.">>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 10.1
+%% @todo <<"\\**"
+%% @todo <<"\\*"
+%% @todo <<"**"
+emphasized_single_quote(Data, Prev) ->
+ quoted_text(Data, Prev, emphasized, $', $').
+emphasized_underline(Data, Prev) ->
+ quoted_text(Data, Prev, emphasized, $_, $_).
+strong(Data, Prev) ->
+ quoted_text(Data, Prev, strong, $*, $*).
+
+quoted_text(<<Left, Rest0/bits>>, Prev, Type, Left, Right) when ?IS_BOUNDARY(Prev) ->
+ {Content, <<Right, Rest/bits>>} = while(fun(C) -> C =/= Right end, Rest0),
+ {ok, {Type, #{
+ left => Left,
+ right => Right
+ }, inline(Content), inline}, Rest}.
+
+-ifdef(TEST).
+emphasized_test() ->
+ [
+ <<"Word phrases ">>,
+ {emphasized, #{left := $', right := $'},
+ <<"enclosed in single quote characters">>, _},
+ <<" (acute accents) or ">>,
+ {emphasized, #{left := $_, right := $_},
+ <<"underline characters">>, _},
+ <<" are emphasized.">>
+ ] = inline(<<
+ "Word phrases 'enclosed in single quote characters' (acute accents) "
+ "or _underline characters_ are emphasized."
+ >>),
+ ok.
+
+strong_test() ->
+ [
+ <<"Word phrases ">>,
+ {strong, #{left := $*, right := $*},
+ <<"enclosed in asterisk characters">>, _},
+ <<" are rendered in a strong font (usually bold).">>
+ ] = inline(<<
+ "Word phrases *enclosed in asterisk characters* "
+ "are rendered in a strong font (usually bold)."
+ >>),
+ ok.
+-endif.
+
+%% Asciidoc User Guide 21.4
+inline_literal_passthrough(<<"`", Rest0/bits>>, Prev) when ?IS_BOUNDARY(Prev) ->
+ {Content, <<"`", Rest/bits>>} = while(fun(C) -> C =/= $` end, Rest0),
+ {ok, {inline_literal_passthrough, #{}, Content, inline}, Rest}.
+
+-ifdef(TEST).
+inline_literal_passthrough_test() ->
+ [
+ <<"Word phrases ">>,
+ {inline_literal_passthrough, #{}, <<"enclosed in backtick characters">>, _},
+ <<" (grave accents)...">>
+ ] = inline(<<"Word phrases `enclosed in backtick characters` (grave accents)...">>),
+ ok.
+-endif.
diff --git a/src/asciideck_line_reader.erl b/src/asciideck_line_reader.erl
new file mode 100644
index 0000000..240c70b
--- /dev/null
+++ b/src/asciideck_line_reader.erl
@@ -0,0 +1,94 @@
+%% Copyright (c) 2017-2018, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(asciideck_line_reader).
+-behaviour(gen_server).
+
+%% API.
+-export([start_link/1]).
+-export([read_line/1]).
+-export([get_position/1]).
+-export([set_position/2]).
+
+%% gen_server.
+-export([init/1]).
+-export([handle_call/3]).
+-export([handle_cast/2]).
+-export([handle_info/2]).
+-export([terminate/2]).
+-export([code_change/3]).
+
+-record(state, {
+ lines :: [binary()],
+ length :: non_neg_integer(),
+ pos = 1 :: non_neg_integer()
+}).
+
+%% API.
+
+-spec start_link(binary()) -> {ok, pid()}.
+start_link(Data) ->
+ gen_server:start_link(?MODULE, [Data], []).
+
+-spec read_line(pid()) -> binary() | eof.
+read_line(Pid) ->
+ gen_server:call(Pid, read_line).
+
+%% @todo peek_line
+
+-spec get_position(pid()) -> pos_integer().
+get_position(Pid) ->
+ gen_server:call(Pid, get_position).
+
+-spec set_position(pid(), pos_integer()) -> ok.
+set_position(Pid, Pos) ->
+ gen_server:cast(Pid, {set_position, Pos}).
+
+%% gen_server.
+
+init([Data]) ->
+ Lines0 = binary:split(Data, <<"\n">>, [global]),
+ %% We add an empty line at the end to simplify parsing.
+ %% This has the inconvenient that when parsing blocks
+ %% this empty line will be included in the result if
+ %% the block is not properly closed.
+ Lines = lists:append(Lines0, [<<>>]),
+ {ok, #state{lines=Lines, length=length(Lines)}}.
+
+handle_call(read_line, _From, State=#state{length=Length, pos=Pos})
+ when Pos > Length ->
+ {reply, eof, State};
+%% @todo I know this isn't the most efficient. We could keep
+%% the lines read separately and roll back when set_position
+%% wants us to. But it works fine for now.
+handle_call(read_line, _From, State=#state{lines=Lines, pos=Pos}) ->
+ {reply, lists:nth(Pos, Lines), State#state{pos=Pos + 1}};
+handle_call(get_position, _From, State=#state{pos=Pos}) ->
+ {reply, Pos, State};
+handle_call(_Request, _From, State) ->
+ {reply, ignored, State}.
+
+handle_cast({set_position, Pos}, State) ->
+ {noreply, State#state{pos=Pos}};
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
diff --git a/src/asciideck_lists_pass.erl b/src/asciideck_lists_pass.erl
new file mode 100644
index 0000000..efb8e87
--- /dev/null
+++ b/src/asciideck_lists_pass.erl
@@ -0,0 +1,155 @@
+%% Copyright (c) 2017-2018, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% The purpose of this pass is to aggregate list_item
+%% blocks into proper lists. This involves building a
+%% tree based on the rules for list items.
+%%
+%% The general rules are:
+%%
+%% - Any list item of different type/level than the
+%% current list item is a child of the latter.
+%%
+%% - The level ultimately does not matter when building
+%% the tree, * then **** then ** is accepted just fine.
+%%
+%% - Lists of the same type as a parent are not allowed.
+%% On the other hand reusing a type in different parts
+%% of the tree is not a problem.
+%%
+%% - Any literal paragraph following a list item is a
+%% child of that list item. @todo
+%%
+%% - Any other block can be included as a child by using
+%% list continuations.
+-module(asciideck_lists_pass).
+
+-export([run/1]).
+
+run(AST) ->
+ list(AST, []).
+
+list([], Acc) ->
+ lists:reverse(Acc);
+%% Any trailing block continuation is ignored.
+list([{list_item_continuation, _, _, _}], Acc) ->
+ lists:reverse(Acc);
+%% The first list item contains the attributes for the list.
+list([LI={list_item, Attrs, _, Ann}|Tail0], Acc) ->
+ {Items, Tail} = item(Tail0, LI, [type(Attrs)], []),
+ list(Tail, [{list, Attrs, Items, Ann}|Acc]);
+list([Block|Tail], Acc) ->
+ list(Tail, [Block|Acc]).
+
+%% Bulleted/numbered list item of the same type.
+item([NextLI={list_item, #{type := T, level := L}, _, _}|Tail],
+ CurrentLI={list_item, #{type := T, level := L}, _, _}, Parents, Acc) ->
+ item(Tail, NextLI, Parents, [reverse_children(CurrentLI)|Acc]);
+%% Labeled list item of the same type.
+item([NextLI={list_item, #{type := T, separator := S}, _, _}|Tail],
+ CurrentLI={list_item, #{type := T, separator := S}, _, _}, Parents, Acc) ->
+ item(Tail, NextLI, Parents, [reverse_children(CurrentLI)|Acc]);
+%% Other list items are either parent or children lists.
+item(FullTail=[NextLI={list_item, Attrs, _, Ann}|Tail0], CurrentLI, Parents, Acc) ->
+ case lists:member(type(Attrs), Parents) of
+ %% We have a parent list item. This is the end of this child list.
+ true ->
+ {lists:reverse([reverse_children(CurrentLI)|Acc]), FullTail};
+ %% We have a child list item. This is the beginning of a new list.
+ false ->
+ {Items, Tail} = item(Tail0, NextLI, [type(Attrs)|Parents], []),
+ item(Tail, add_child(CurrentLI, {list, Attrs, Items, Ann}), Parents, Acc)
+ end;
+%% Ignore multiple contiguous list continuations.
+item([LIC={list_item_continuation, _, _, _},
+ {list_item_continuation, _, _, _}|Tail], CurrentLI, Parents, Acc) ->
+ item([LIC|Tail], CurrentLI, Parents, Acc);
+%% Blocks that immediately follow list_item_continuation are children,
+%% unless they are list_item themselves in which case it depends on the
+%% type and level of the list item.
+item([{list_item_continuation, _, _, _}, LI={list_item, _, _, _}|Tail], CurrentLI, Parents, Acc) ->
+ item([LI|Tail], CurrentLI, Parents, Acc);
+item([{list_item_continuation, _, _, _}, Block|Tail], CurrentLI, Parents, Acc) ->
+ item(Tail, add_child(CurrentLI, Block), Parents, Acc);
+%% Anything else is the end of the list.
+item(Tail, CurrentLI, _, Acc) ->
+ {lists:reverse([reverse_children(CurrentLI)|Acc]), Tail}.
+
+type(Attrs) ->
+ maps:with([type, level, separator], Attrs).
+
+add_child({list_item, Attrs, Children, Ann}, Child) ->
+ {list_item, Attrs, [Child|Children], Ann}.
+
+reverse_children({list_item, Attrs, Children, Ann}) ->
+ {list_item, Attrs, lists:reverse(Children), Ann}.
+
+-ifdef(TEST).
+list_test() ->
+ [{list, #{type := bulleted, level := 1}, [
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, #{}, <<"Hello!">>, _}], #{line := 1}},
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, #{}, <<"World!">>, _}], #{line := 2}}
+ ], #{line := 1}}] = run([
+ {list_item, #{type => bulleted, level => 1},
+ [{paragraph, #{}, <<"Hello!">>, #{line => 1}}], #{line => 1}},
+ {list_item, #{type => bulleted, level => 1},
+ [{paragraph, #{}, <<"World!">>, #{line => 2}}], #{line => 2}}
+ ]),
+ ok.
+
+list_of_list_test() ->
+ [{list, #{type := bulleted, level := 1}, [
+ {list_item, #{type := bulleted, level := 1}, [
+ {paragraph, #{}, <<"Hello!">>, _},
+ {list, #{type := bulleted, level := 2}, [
+ {list_item, #{type := bulleted, level := 2},
+ [{paragraph, #{}, <<"Cat!">>, _}], #{line := 2}},
+ {list_item, #{type := bulleted, level := 2},
+ [{paragraph, #{}, <<"Dog!">>, _}], #{line := 3}}
+ ], #{line := 2}}
+ ], #{line := 1}},
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, #{}, <<"World!">>, _}], #{line := 4}}
+ ], #{line := 1}}] = run([
+ {list_item, #{type => bulleted, level => 1},
+ [{paragraph, #{}, <<"Hello!">>, #{line => 1}}], #{line => 1}},
+ {list_item, #{type => bulleted, level => 2},
+ [{paragraph, #{}, <<"Cat!">>, #{line => 2}}], #{line => 2}},
+ {list_item, #{type => bulleted, level => 2},
+ [{paragraph, #{}, <<"Dog!">>, #{line => 3}}], #{line => 3}},
+ {list_item, #{type => bulleted, level => 1},
+ [{paragraph, #{}, <<"World!">>, #{line => 4}}], #{line => 4}}
+ ]),
+ ok.
+
+list_continuation_test() ->
+ [{list, #{type := bulleted, level := 1}, [
+ {list_item, #{type := bulleted, level := 1}, [
+ {paragraph, #{}, <<"Hello!">>, _},
+ {listing_block, #{}, <<"hello() -> world.">>, #{line := 3}}
+ ], #{line := 1}},
+ {list_item, #{type := bulleted, level := 1},
+ [{paragraph, #{}, <<"World!">>, _}], #{line := 6}}
+ ], #{line := 1}}] = run([
+ {list_item, #{type => bulleted, level => 1},
+ [{paragraph, #{}, <<"Hello!">>, #{line => 1}}], #{line => 1}},
+ {list_item_continuation, #{}, <<>>, #{line => 2}},
+ {listing_block, #{}, <<"hello() -> world.">>, #{line => 3}},
+ {list_item, #{type => bulleted, level => 1},
+ [{paragraph, #{}, <<"World!">>, #{line => 6}}], #{line => 6}}
+ ]),
+ ok.
+-endif.
diff --git a/src/asciideck_parser.erl b/src/asciideck_parser.erl
deleted file mode 100644
index 8016395..0000000
--- a/src/asciideck_parser.erl
+++ /dev/null
@@ -1,388 +0,0 @@
-%% Copyright (c) 2016, Loïc Hoguin <[email protected]>
-%%
-%% Permission to use, copy, modify, and/or distribute this software for any
-%% purpose with or without fee is hereby granted, provided that the above
-%% copyright notice and this permission notice appear in all copies.
-%%
-%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
--module(asciideck_parser).
-
--export([parse/2]).
-
-%% @todo
-%% All nodes in the AST are of type {Type, Attrs, Text | Nodes, Ann}
-%% except for text formatting nodes at the moment. Text formatting
-%% nodes will be converted to this form in a future change.
-
-%% Parsing occurs in a few passes:
-%%
-%% * p1: Line-based parsing of the raw Asciidoc document
-%% * p2: Deal with more compp1 structures like lists and tables
-
-parse(Data, St) ->
- Lines0 = binary:split(Data, <<"\n">>, [global]),
- %% Ensure there's an empty line at the end, to simplify parsing.
- Lines1 = lists:append(Lines0, [<<>>]),
- LineNumbers = lists:seq(1, length(Lines1)),
- Lines = lists:zip(LineNumbers, Lines1),
- %% @todo Document header, if any. Recognized by the author info/doc attributes?
- %% Alternatively, don't recognize it, and only use attribute entries for the same info.
- p2(p1(Lines, [], St), []).
-
-%% First pass.
-
-%% @todo When a block element is encountered asciidoc(1) determines the type of block by checking in the following order (first to last): (section) Titles, BlockMacros, Lists, DelimitedBlocks, Tables, AttributeEntrys, AttributeLists, BlockTitles, Paragraphs.
-
-%% @todo And this function is parsing, not p1ing.
-p1([], AST, _St) ->
- lists:reverse(AST);
-%% Extra empty lines.
-p1([{_, <<>>}|Tail], AST, St) ->
- p1(Tail, AST, St);
-%% Comments.
-p1([{LN, <<"//", Comment/bits >>}|Tail], AST, St) ->
- p1(Tail, [comment(trim_ws(Comment), ann(LN, St))|AST], St);
-%% Section titles.
-p1([{LN, <<"= ", Title/bits >>}, {_, <<>>}|Tail], AST, St) ->
- p1_title_short(Tail, AST, St, LN, Title, 0);
-p1([{LN, <<"== ", Title/bits >>}, {_, <<>>}|Tail], AST, St) ->
- p1_title_short(Tail, AST, St, LN, Title, 1);
-p1([{LN, <<"=== ", Title/bits >>}, {_, <<>>}|Tail], AST, St) ->
- p1_title_short(Tail, AST, St, LN, Title, 2);
-p1([{LN, <<"==== ", Title/bits >>}, {_, <<>>}|Tail], AST, St) ->
- p1_title_short(Tail, AST, St, LN, Title, 3);
-p1([{LN, <<"===== ", Title/bits >>}, {_, <<>>}|Tail], AST, St) ->
- p1_title_short(Tail, AST, St, LN, Title, 4);
-%% Block titles.
-p1([{_LN, <<".", Title/bits >>}|Tail], AST, St) ->
- p1(Tail, [{block_title, Title}|AST], St);
-%% Attribute lists.
-p1([{_LN, <<"[", Attrs/bits >>}|Tail], AST, St) ->
- p1(Tail, [{attribute_list, p1_attr_list(Attrs)}|AST], St);
-%% Listing blocks.
-p1([{LN, <<"----", _/bits >>}|Tail], AST, St) ->
- p1_listing(Tail, AST, St, LN, []);
-%% Lists.
-p1([{LN, <<"* ", Text/bits >>}|Tail], AST, St) ->
- p1_li(Tail, AST, St, uli1, {LN, Text});
-p1([{LN, <<"** ", Text/bits >>}|Tail], AST, St) ->
- p1_li(Tail, AST, St, uli2, {LN, Text});
-p1([{LN, <<"*** ", Text/bits >>}|Tail], AST, St) ->
- p1_li(Tail, AST, St, uli3, {LN, Text});
-p1([{LN, <<"**** ", Text/bits >>}|Tail], AST, St) ->
- p1_li(Tail, AST, St, uli4, {LN, Text});
-p1([{LN, <<"***** ", Text/bits >>}|Tail], AST, St) ->
- p1_li(Tail, AST, St, uli5, {LN, Text});
-%% Tables.
-p1([{LN, <<"|===", _/bits >>}|Tail], AST, St) ->
- p1_table(Tail, AST, St, LN);
-p1([{LN, <<"|", Text/bits >>}|Tail], AST, St) ->
- p1_cell(Tail, AST, St, LN, Text);
-%% Prefix-based or paragraph.
-p1(Lines, AST, St) ->
- p1_text(Lines, AST, St).
-
-p1_title_short(Tail, AST, St, LN, Text0, Level) ->
- %% Remove the trailer, if any.
- Text1 = trim_ws(Text0),
- Trailer = case Level of
- 0 -> <<" =">>;
- 1 -> <<" ==">>;
- 2 -> <<" ===">>;
- 3 -> <<" ====">>;
- 4 -> <<" =====">>
- end,
- TrailerSize = byte_size(Trailer),
- Size = byte_size(Text1) - TrailerSize,
- Text3 = case Text1 of
- << Text2:Size/binary, Trailer:TrailerSize/binary >> -> Text2;
- _ -> Text1
- end,
- Text = trim_ws(Text3),
- p1(Tail, [title(Text, #{level => Level}, ann(LN, St))|AST], St).
-
-p1_attr_list(AttrList0) ->
- [AttrList|_] = binary:split(AttrList0, <<"]">>),
- binary:split(AttrList, <<",">>).
-
-%% @todo Parse attributes properly.
-p1_table(Tail, [{attribute_list, Attrs}, {block_title, Title}|AST], St, LN) ->
- p1(Tail, [{begin_table, #{title => Title, todo => Attrs}, ann(LN, St)}|AST], St);
-p1_table(Tail, [{attribute_list, Attrs}|AST], St, LN) ->
- p1(Tail, [{begin_table, #{todo => Attrs}, ann(LN, St)}|AST], St);
-p1_table(Tail, AST=[nl, {cell, _, _, _}|_], St, _) ->
- p1(Tail, [end_table|AST], St);
-p1_table(Tail, AST=[{cell, _, _, _}|_], St, _) ->
- p1(Tail, [end_table|AST], St);
-p1_table(Tail, AST, St, LN) ->
- p1(Tail, [{begin_table, #{}, ann(LN, St)}|AST], St).
-
-%% @todo Multiline cells.
-%% @todo Styled cells.
-%% @todo Strip whitespace at the beginning of the cell if on the same line.
-p1_cell(Tail=[{_, NextLine}|_], AST0, St, LN, Text) ->
- case p1_cell_split(Text, <<>>) of
- [Cell] ->
- AST1 = [nl, cell(p1([{LN, trim_ws(Cell)}, {LN, <<>>}], [], St), ann(LN, St))|AST0],
- AST = case NextLine of
- <<>> -> [nl|AST1];
- _ -> AST1
- end,
- p1(Tail, AST, St);
- [Cell, Rest] ->
- p1_cell(Tail, [cell(p1([{LN, trim_ws(Cell)}, {LN, <<>>}], [], St), ann(LN, St))|AST0], St, LN, Rest)
- end.
-
-p1_cell_split(<<>>, Acc) ->
- [Acc];
-p1_cell_split(<< $\\, $|, Rest/bits >>, Acc) ->
- p1_cell_split(Rest, << Acc/binary, $| >>);
-p1_cell_split(<< $|, Rest/bits >>, Acc) ->
- [Acc, Rest];
-p1_cell_split(<< C, Rest/bits >>, Acc) ->
- p1_cell_split(Rest, << Acc/binary, C >>).
-
-p1_listing([{_, <<"----", _/bits >>}, {_, <<>>}|Tail], AST0, St, LN, [_|Acc]) ->
- Text = iolist_to_binary(lists:reverse(Acc)),
- case AST0 of
- [{attribute_list, [<<"source">>, Lang]}, {block_title, Title}|AST] ->
- p1(Tail, [listing(Text, #{title => Title, language => Lang}, ann(LN, St))|AST], St);
- [{block_title, Title}, {attribute_list, [<<"source">>, Lang]}|AST] ->
- p1(Tail, [listing(Text, #{title => Title, language => Lang}, ann(LN, St))|AST], St);
- [{attribute_list, [<<"source">>, Lang]}|AST] ->
- p1(Tail, [listing(Text, #{language => Lang}, ann(LN, St))|AST], St);
- [{block_title, Title}|AST] ->
- p1(Tail, [listing(Text, #{title => Title}, ann(LN, St))|AST], St);
- AST ->
- p1(Tail, [listing(Text, #{}, ann(LN, St))|AST], St)
- end;
-p1_listing([{_, Line}|Tail], AST, St, LN, Acc) ->
- p1_listing(Tail, AST, St, LN, [<<"\n">>, Line|Acc]).
-
-p1_li(Lines, AST, St, Type, FirstLine = {LN, _}) ->
- {Tail, Glob} = p1_li_glob(Lines, []),
- p1(Tail, [{Type, p1([FirstLine|Glob], [], St), ann(LN, St)}|AST], St).
-
-%% Glob everything until next list or empty line.
-p1_li_glob(Tail = [{LN, << "*", _/bits >>}|_], Acc) ->
- {Tail, lists:reverse([{LN, <<>>}|Acc])};
-p1_li_glob(Tail = [{LN, <<>>}|_], Acc) ->
- {Tail, lists:reverse([{LN, <<>>}|Acc])};
-p1_li_glob([{LN, <<"+">>}|Tail], Acc) ->
- p1_li_glob(Tail, [{LN, <<>>}|Acc]);
-p1_li_glob([Line|Tail], Acc) ->
- p1_li_glob(Tail, [Line|Acc]).
-
-%% Skip initial empty lines and then glob like normal lists.
-p1_ll_glob(Lines=[{_, Line}|Tail]) ->
- case trim_ws(Line) of
- <<>> -> p1_ll_glob(Tail);
- _ -> p1_ll_glob(Lines, [])
- end.
-
-%% Glob everything until empty line.
-%% @todo Detect next list.
-p1_ll_glob(Tail = [{LN, <<>>}|_], Acc) ->
- {Tail, lists:reverse([{LN, <<>>}|Acc])};
-p1_ll_glob([{LN, <<"+">>}|Tail], Acc) ->
- p1_ll_glob(Tail, [{LN, <<>>}|Acc]);
-p1_ll_glob([{LN, <<" ", Line/bits>>}|Tail], Acc) ->
- p1_ll_glob([{LN, trim_ws(Line)}|Tail], Acc);
-p1_ll_glob(Lines=[Line={LN, Text}|Tail], Acc) ->
- case binary:split(<< Text/binary, $\s >>, <<":: ">>) of
- [_, _] ->
- {Lines, lists:reverse([{LN, <<>>}|Acc])};
- _ ->
- p1_ll_glob(Tail, [Line|Acc])
- end.
-
-p1_text(Lines=[{LN, Line}|Tail], AST, St) ->
- case binary:split(<< Line/binary, $\s >>, <<":: ">>) of
- %% Nothing else on the line.
- [Label, <<>>] ->
- {Tail1, Glob} = p1_ll_glob(Tail),
- p1(Tail1, [{label, Label, p1(Glob, [], St), ann(LN, St)}|AST], St);
- %% Text on the same line.
- [Label, Text0] ->
- Size = byte_size(Text0) - 1,
- << Text:Size/binary, _ >> = Text0,
- {Tail1, Glob} = p1_ll_glob([{LN, Text}|Tail]),
- %% Text on the same line is necessarily a paragraph I believe.
- p1_p(Tail1, [{label, Label, p1(Glob, [], St), ann(LN, St)}|AST], St, LN, []);
- %% Not a labeled list.
- _ ->
- p1_maybe_p(Lines, AST, St)
- end.
-
-%% @todo Literal paragraphs.
-p1_maybe_p([{_LN, << " ", Line/bits >>}|Tail], AST, St) ->
- <<>> = trim_ws(Line),
- p1(Tail, AST, St);
-p1_maybe_p(Lines=[{LN, _}|_], AST, St) ->
- p1_p(Lines, AST, St, LN, []).
-
-p1_p([{_, <<>>}|Tail], AST0, St, LN, [_|Acc]) ->
- Text = format(iolist_to_binary(lists:reverse(Acc)), LN, St),
- case AST0 of
- [{block_title, Title}|AST] ->
- p1(Tail, [paragraph(Text, #{title => Title}, ann(LN, St))|AST], St);
- AST ->
- p1(Tail, [paragraph(Text, #{}, ann(LN, St))|AST], St)
- end;
-%% Ignore comments inside paragraphs.
-%% @todo Keep in the AST.
-p1_p([{_, <<"//", _/bits>>}|Tail], AST, St, LN, Acc) ->
- p1_p(Tail, AST, St, LN, Acc);
-p1_p([{_, Line}|Tail], AST, St, LN, Acc) ->
- %% @todo We need to keep line/col information. To do this
- %% we probably should keep an index of character number -> line/col
- %% that we pass to the format function. Otherwise the line/col
- %% information on text will point to the paragraph start.
- p1_p(Tail, AST, St, LN, [<<" ">>, Line|Acc]).
-
-%% Inline formatting.
-
-%% @todo Probably do it as part of the node functions that require it.
-format(Text, LN, St) ->
- case format(Text, LN, St, [], <<>>, $\s) of
- [Bin] when is_binary(Bin) -> Bin;
- Formatted -> Formatted
- end.
-
-format(<<>>, _, _, Acc, <<>>, _) ->
- lists:reverse(Acc);
-format(<<>>, _, _, Acc, BinAcc, _) ->
- lists:reverse([BinAcc|Acc]);
-format(<< "link:", Rest0/bits >>, LN, St, Acc0, BinAcc, Prev) when Prev =:= $\s ->
- case re:run(Rest0, "^([^[]*)\\[([^]]*)\\](.*)", [{capture, all, binary}]) of
- nomatch ->
- format(Rest0, LN, St, Acc0, << BinAcc/binary, "link:" >>, $:);
- {match, [_, Link, Text, Rest]} ->
- Acc = case BinAcc of
- <<>> -> Acc0;
- _ -> [BinAcc|Acc0]
- end,
- format(Rest, LN, St, [rel_link(Text, Link, ann(LN, St))|Acc], <<>>, $])
- end;
-format(<< C, Rest0/bits >>, LN, St, Acc0, BinAcc, Prev) when Prev =:= $\s ->
- %% @todo In some cases we must format inside the quoted text too.
- %% Therefore we need to have some information about what to do here.
- Quotes = #{
- $* => {strong, text},
- $` => {mono, literal}
- },
- case maps:get(C, Quotes, undefined) of
- undefined ->
- format(Rest0, LN, St, Acc0, << BinAcc/binary, C >>, C);
- {NodeType, QuotedType} ->
- case binary:split(Rest0, << C >>) of
- [_] ->
- format(Rest0, LN, St, Acc0, << BinAcc/binary, $* >>, $*);
- [QuotedText0, Rest] ->
- Acc = case BinAcc of
- <<>> -> Acc0;
- _ -> [BinAcc|Acc0]
- end,
- QuotedText = case QuotedType of
- text -> format(QuotedText0, LN, St);
- literal -> QuotedText0
- end,
- format(Rest, LN, St, [quoted(NodeType, QuotedText, ann(LN, St))|Acc], <<>>, $*)
- end
- end;
-format(<< C, Rest/bits >>, LN, St, Acc, BinAcc, _) ->
- format(Rest, LN, St, Acc, << BinAcc/binary, C >>, C).
-
-%% Second pass.
-
-p2([], Acc) ->
- lists:reverse(Acc);
-p2([{label, Label, Items, Ann}|Tail], Acc) ->
- %% @todo Handle this like other lists.
- p2(Tail, [ll([li(p2(Items, []), #{label => Label}, Ann)], #{}, Ann)|Acc]);
-p2(Tail0=[{uli1, _, UlAnn}|_], Acc) ->
- {LIs0, Tail} = lists:splitwith(fun({uli1, _, _}) -> true; (_) -> false end, Tail0),
- LIs = [li(I, LiAnn) || {uli1, I, LiAnn} <- LIs0],
- p2(Tail, [ul(LIs, #{}, UlAnn)|Acc]);
-p2([{begin_table, Attrs, Ann}|Tail0], Acc) ->
- %% @todo Can also get them from Attrs?
- N = count_table_columns(Tail0),
- {Rows, Tail} = p2_rows(Tail0, [], [], N, 1),
- p2(Tail, [table(Rows, Attrs, Ann)|Acc]);
-p2([Item|Tail], Acc) ->
- p2(Tail, [Item|Acc]).
-
-%% @todo One cell per line version.
-count_table_columns(Cells) ->
- length(lists:takewhile(fun({cell, _, _, _}) -> true; (_) -> false end, Cells)).
-
-p2_rows([nl|Tail], Rows, Cols, NumCols, N) ->
- p2_rows(Tail, Rows, Cols, NumCols, N);
-p2_rows([Cell = {cell, _, _, Ann}|Tail], Rows, Cols, NumCols, NumCols) ->
- p2_rows(Tail, [row(lists:reverse([Cell|Cols]), Ann)|Rows], [], NumCols, 1);
-p2_rows([Cell = {cell, _, _, _}|Tail], Rows, Cols, NumCols, N) ->
- p2_rows(Tail, Rows, [Cell|Cols], NumCols, N + 1);
-p2_rows([end_table|Tail], Rows, [], _, _) ->
- {lists:reverse(Rows), Tail}.
-
-%% Annotations.
-
-ann(Line, St) ->
- ann(Line, 1, St).
-
-%% @todo Take filename too, if any.
-ann(Line, Col, _St) ->
- #{line => Line, col => Col}.
-
-%% Nodes.
-
-cell(Nodes, Ann) ->
- {cell, #{}, Nodes, Ann}.
-
-comment(Text, Ann) ->
- {comment, #{}, Text, Ann}.
-
-li(Nodes, Ann) ->
- li(Nodes, #{}, Ann).
-
-li(Nodes, Attrs, Ann) ->
- {li, Attrs, Nodes, Ann}.
-
-listing(Text, Attrs, Ann) ->
- {listing, Attrs, Text, Ann}.
-
-ll(Nodes, Attrs, Ann) ->
- {ll, Attrs, Nodes, Ann}.
-
-paragraph(Text, Attrs, Ann) ->
- {p, Attrs, Text, Ann}.
-
-quoted(NodeType, Text, Ann) ->
- {NodeType, #{}, Text, Ann}.
-
-rel_link(Text, Link, Ann) ->
- {rel_link, #{target => Link}, Text, Ann}.
-
-row(Nodes, Ann) ->
- {row, #{}, Nodes, Ann}.
-
-table(Nodes, Attrs, Ann) ->
- {table, Attrs, Nodes, Ann}.
-
-title(Text, Attrs, Ann) ->
- {title, Attrs, Text, Ann}.
-
-ul(Nodes, Attrs, Ann) ->
- {ul, Attrs, Nodes, Ann}.
-
-%% Utility functions.
-
-trim_ws(Text) ->
- iolist_to_binary(re:replace(Text, "^[ \\t]+|[ \\t]+$", <<>>, [global])).
diff --git a/src/asciideck_tables_pass.erl b/src/asciideck_tables_pass.erl
new file mode 100644
index 0000000..fdda6ef
--- /dev/null
+++ b/src/asciideck_tables_pass.erl
@@ -0,0 +1,191 @@
+%% Copyright (c) 2017-2018, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% This pass parses and builds a table from the contents
+%% of a table block.
+%%
+%% Asciidoc User Guide 23
+%%
+%% @todo Rows and cells are currently not annotated.
+-module(asciideck_tables_pass).
+
+-export([run/1]).
+
+-define(IS_WS(C), (C =:= $\s) or (C =:= $\t) or (C =:= $\n).
+
+run([]) ->
+ [];
+run([Table={table, _, _, _}|Tail]) ->
+ [table(Table)|run(Tail)];
+run([Block|Tail]) ->
+ [Block|run(Tail)].
+
+table({table, Attrs, Contents, Ann}) ->
+ {Cells, NumCols} = parse_table(Contents, Attrs),
+ Children = rows(Cells, NumCols),
+ {table, Attrs, Children, Ann}.
+
+-ifdef(TEST).
+table_test() ->
+ {table, _, [
+ {row, _, [
+ {cell, _, <<"1">>, _},
+ {cell, _, <<"2">>, _},
+ {cell, _, <<"A">>, _}
+ ], _},
+ {row, _, [
+ {cell, _, <<"3">>, _},
+ {cell, _, <<"4">>, _},
+ {cell, _, <<"B">>, _}
+ ], _},
+ {row, _, [
+ {cell, _, <<"5">>, _},
+ {cell, _, <<"6">>, _},
+ {cell, _, <<"C">>, _}
+ ], _}
+ ], _} = table({table, #{}, <<
+ "|1 |2 |A\n"
+ "|3 |4 |B\n"
+ "|5 |6 |C">>, #{line => 1}}),
+ ok.
+-endif.
+
+%% If the cols attribute is not specified, the number of
+%% columns is the number of cells on the first line.
+parse_table(Contents, #{<<"cols">> := Cols}) ->
+ {parse_cells(Contents, []), num_cols(Cols)};
+%% We get the first line, parse the cells in it then
+%% count the number of columns in the table. Finally
+%% we parse all the remaining cells.
+parse_table(Contents, _) ->
+ case binary:split(Contents, <<$\n>>) of
+ %% We only have the one line. Who writes tables like this?
+ [Line] ->
+ Cells = parse_cells(Line, []),
+ {Cells, length(Cells)};
+ %% We have a useful table with more than one line. Good user!
+ [Line, Rest] ->
+ Cells0 = parse_cells(Line, []),
+ Cells = parse_cells(Rest, lists:reverse(Cells0)),
+ {Cells, length(Cells0)}
+ end.
+
+num_cols(Cols) ->
+ %% @todo Handle column specifiers.
+ Specs = binary:split(Cols, <<$,>>, [global]),
+ length(Specs).
+
+parse_cells(Contents, Acc) ->
+ Cells = split_cells(Contents),%binary:split(Contents, [<<$|>>], [global]),
+ do_parse_cells(Cells, Acc).
+ %% Split on |
+ %% Look at the end of each element see if there's a cell specifier
+ %% Add it as an attribute to the cell for now and consolidate
+ %% when processing rows.
+
+split_cells(Contents) ->
+ split_cells(Contents, <<>>, []).
+
+split_cells(<<>>, Cell, Acc) ->
+ lists:reverse([Cell|Acc]);
+split_cells(<<$\\, $|, R/bits>>, Cell, Acc) ->
+ split_cells(R, <<Cell/binary, $|>>, Acc);
+split_cells(<<$|, R/bits>>, Cell, Acc) ->
+ split_cells(R, <<>>, [Cell|Acc]);
+split_cells(<<C, R/bits>>, Cell, Acc) ->
+ split_cells(R, <<Cell/binary, C>>, Acc).
+
+%% Malformed table (no pipe before cell). Process it like it is a single cell.
+do_parse_cells([Contents], Acc) ->
+ %% @todo Annotations.
+ lists:reverse([{cell, #{specifiers => <<>>}, Contents, #{}}|Acc]);
+%% Last cell. There are no further cell specifiers.
+do_parse_cells([Specs, Contents0], Acc) ->
+ Contents = asciideck_block_parser:trim(Contents0, both),
+ %% @todo Annotations.
+ Cell = {cell, #{specifiers => Specs}, Contents, #{}},
+ lists:reverse([Cell|Acc]);
+%% If there are cell specifiers we need to extract them from the cell
+%% contents. Cell specifiers are everything from the last whitespace
+%% until the end of the binary.
+do_parse_cells([Specs, Contents0|Tail], Acc) ->
+ NextSpecs = <<>>, %% @todo find_r(Contents0, <<>>),
+ Len = byte_size(Contents0) - byte_size(NextSpecs),
+ <<Contents1:Len/binary, _/bits>> = Contents0,
+ Contents = asciideck_block_parser:trim(Contents1, both),
+ %% @todo Annotations.
+ Cell = {cell, #{specifiers => Specs}, Contents, #{}},
+ do_parse_cells([NextSpecs|Tail], [Cell|Acc]).
+
+%% @todo This is not correct. Not all remaining data is specifiers.
+%% In addition, for columns at the end of the line this doesn't apply.
+%% Find the remaining data after the last whitespace character.
+%find_r(<<>>, Acc) ->
+% Acc;
+%find_r(<<C, Rest/bits>>, _) when ?IS_WS(C) ->
+% find_r(Rest, Rest);
+%find_r(<<_, Rest/bits>>, Acc) ->
+% find_r(Rest, Acc).
+
+-ifdef(TEST).
+parse_table_test() ->
+ {[
+ {cell, _, <<"1">>, _},
+ {cell, _, <<"2">>, _},
+ {cell, _, <<"A">>, _},
+ {cell, _, <<"3">>, _},
+ {cell, _, <<"4">>, _},
+ {cell, _, <<"B">>, _},
+ {cell, _, <<"5">>, _},
+ {cell, _, <<"6">>, _},
+ {cell, _, <<"C">>, _}
+ ], 3} = parse_table(<<
+ "|1 |2 |A\n"
+ "|3 |4 |B\n"
+ "|5 |6 |C">>, #{}),
+ ok.
+
+parse_table_escape_pipe_test() ->
+ {[
+ {cell, _, <<"1">>, _},
+ {cell, _, <<"2">>, _},
+ {cell, _, <<"3 |4">>, _},
+ {cell, _, <<"5">>, _}
+ ], 2} = parse_table(<<
+ "|1 |2\n"
+ "|3 \\|4 |5">>, #{}),
+ ok.
+-endif.
+
+%% @todo We currently don't handle colspans and rowspans.
+rows(Cells, NumCols) ->
+ rows(Cells, [], NumCols, [], NumCols).
+
+%% End of row.
+rows(Tail, Acc, NumCols, RowAcc, CurCol) when CurCol =< 0 ->
+ %% @todo Annotations.
+ Row = {row, #{}, lists:reverse(RowAcc), #{}},
+ rows(Tail, [Row|Acc], NumCols, [], NumCols);
+%% Add a cell to the row.
+rows([Cell|Tail], Acc, NumCols, RowAcc, CurCol) ->
+ rows(Tail, Acc, NumCols, [Cell|RowAcc], CurCol - 1);
+%% End of a properly formed table.
+rows([], Acc, _, [], _) ->
+ lists:reverse(Acc);
+%% Malformed table. Even if we expect more columns,
+%% if there are no more cells there's nothing we can do.
+rows([], Acc, _, RowAcc, _) ->
+ %% @todo Annotations.
+ Row = {row, #{}, lists:reverse(RowAcc), #{}},
+ lists:reverse([Row|Acc]).
diff --git a/src/asciideck_to_manpage.erl b/src/asciideck_to_manpage.erl
index bdff90e..37e4e73 100644
--- a/src/asciideck_to_manpage.erl
+++ b/src/asciideck_to_manpage.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2016-2018, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -19,7 +19,7 @@
-export([translate/2]).
translate(AST, Opts) ->
- {Man, Section, Output0} = translate_man(AST, Opts),
+ {Man, Section, Output0} = man(AST, Opts),
{CompressExt, Output} = case Opts of
#{compress := gzip} -> {".gz", zlib:gzip(Output0)};
_ -> {"", Output0}
@@ -32,7 +32,9 @@ translate(AST, Opts) ->
Output
end.
-translate_man([{title, #{level := 0}, Title0, _Ann}|AST], Opts) ->
+%% Header of the man page file.
+
+man([{section_title, #{level := 0}, Title0, _Ann}|AST], Opts) ->
ensure_name_section(AST),
[Title, << Section:1/binary, _/bits >>] = binary:split(Title0, <<"(">>),
Extra1 = maps:get(extra1, Opts, today()),
@@ -42,10 +44,10 @@ translate_man([{title, #{level := 0}, Title0, _Ann}|AST], Opts) ->
".TH \"", Title, "\" \"", Section, "\" \"",
Extra1, "\" \"", Extra2, "\" \"", Extra3, "\"\n"
".ta T 4n\n\\&\n",
- man(AST, [])
+ ast(AST)
]}.
-ensure_name_section([{title, #{level := 1}, Title, _}|_]) ->
+ensure_name_section([{section_title, #{level := 1}, Title, _}|_]) ->
case string:to_lower(string:strip(binary_to_list(Title))) of
"name" -> ok;
_ -> error(badarg)
@@ -57,22 +59,56 @@ today() ->
{{Y, M, D}, _} = calendar:universal_time(),
io_lib:format("~b-~2.10.0b-~2.10.0b", [Y, M, D]).
-man([], Acc) ->
- lists:reverse(Acc);
-man([{title, #{level := 1}, Title, _Ann}|Tail], Acc) ->
- man(Tail, [[".SH ", string:to_upper(binary_to_list(Title)), "\n"]|Acc]);
-man([{title, #{level := 2}, Title, _Ann}|Tail], Acc) ->
- man(Tail, [[".SS ", Title, "\n"]|Acc]);
-man([{p, _Attrs, Text, _Ann}|Tail], Acc) ->
- man(Tail, [[".LP\n", man_format(Text), "\n.sp\n"]|Acc]);
-man([{listing, Attrs, Listing, _Ann}|Tail], Acc0) ->
- Acc1 = case Attrs of
- #{title := Title} ->
- [[".PP\n\\fB", Title, "\\fR\n"]|Acc0];
- _ ->
- Acc0
- end,
- Acc = [[
+%% Loop over all types of AST nodes.
+
+ast(AST) ->
+ fold(AST, fun ast_node/1).
+
+fold(AST, Fun) ->
+ lists:reverse(lists:foldl(
+ fun(Node, Acc) -> [Fun(Node)|Acc] end,
+ [], AST)).
+
+ast_node(Node={Type, _, _, _}) ->
+ try
+ case Type of
+ section_title -> section_title(Node);
+ paragraph -> paragraph(Node);
+ listing_block -> listing_block(Node);
+ list -> list(Node);
+ table -> table(Node);
+ comment_line -> comment_line(Node);
+ _ ->
+ io:format("Ignored AST node ~p~n", [Node]),
+ []
+ end
+ catch _:_ ->
+ io:format("Ignored AST node ~p~n", [Node]),
+ []
+ end.
+
+%% Section titles.
+
+section_title({section_title, #{level := 1}, Title, _}) ->
+ [".SH ", string:to_upper(binary_to_list(Title)), "\n"];
+section_title({section_title, #{level := 2}, Title, _}) ->
+ [".SS ", Title, "\n"].
+
+%% Paragraphs.
+
+paragraph({paragraph, _, Text, _}) ->
+ [".LP\n", inline(Text), "\n.sp\n"].
+
+%% Listing blocks.
+
+listing_block({listing_block, Attrs, Listing, _}) ->
+ [
+ case Attrs of
+ #{<<"title">> := Title} ->
+ [".PP\n\\fB", Title, "\\fR\n"];
+ _ ->
+ []
+ end,
".if n \\{\\\n"
".RS 4\n"
".\\}\n"
@@ -82,55 +118,18 @@ man([{listing, Attrs, Listing, _Ann}|Tail], Acc0) ->
".fi\n"
".if n \\{\\\n"
".RE\n"
- ".\\}\n"]|Acc1],
- man(Tail, Acc);
-man([{ul, _Attrs, Items, _Ann}|Tail], Acc0) ->
- Acc = man_ul(Items, Acc0),
- man(Tail, Acc);
-man([{ll, _Attrs, Items, _Ann}|Tail], Acc0) ->
- Acc = man_ll(Items, Acc0),
- man(Tail, Acc);
-%% @todo Attributes.
-%% Currently acts as if options="headers" was always set.
-man([{table, _TAttrs, [{row, RowAttrs, Headers0, RowAnn}|Rows0], _TAnn}|Tail], Acc0) ->
- Headers = [{cell, CAttrs, [{p, Attrs, [{strong, #{}, P, CAnn}], Ann}], CAnn}
- || {cell, CAttrs, [{p, Attrs, P, Ann}], CAnn} <- Headers0],
- Rows = [{row, RowAttrs, Headers, RowAnn}|Rows0],
- Acc = [[
- ".TS\n"
- "allbox tab(:);\n",
- man_table_style(Rows, []),
- man_table_contents(Rows),
- ".TE\n"
- ".sp 1\n"]|Acc0],
- man(Tail, Acc);
-%% Skip everything we don't understand.
-man([_Ignore|Tail], Acc) ->
- io:format("Ignore ~p~n", [_Ignore]), %% @todo lol io:format
- man(Tail, Acc).
-
-man_ll([], Acc) ->
- Acc;
-man_ll([{li, #{label := Label}, Item, _LiAnn}|Tail], Acc0) ->
- Acc = [[
- ".PP\n"
- "\\fB", Label, "\\fR\n",
- ".RS 4\n",
- man_ll_item(Item),
- ".RE\n"]|Acc0],
- man_ll(Tail, Acc).
-
-man_ll_item([{ul, _Attrs, Items, _Ann}]) ->
- [man_ul(Items, []), "\n"];
-man_ll_item([{p, _PAttrs, Text, _PAnn}]) ->
- [man_format(Text), "\n"];
-man_ll_item([{p, _PAttrs, Text, _PAnn}|Tail]) ->
- [man_format(Text), "\n\n", man_ll_item(Tail)].
-
-man_ul([], Acc) ->
- Acc;
-man_ul([{li, _LiAttrs, [{p, _PAttrs, Text, _PAnn}], _LiAnn}|Tail], Acc0) ->
- Acc = [[
+ ".\\}\n"
+ ].
+
+%% Lists.
+
+list({list, #{type := bulleted}, Items, _}) ->
+ fold(Items, fun bulleted_list_item/1);
+list({list, #{type := labeled}, Items, _}) ->
+ fold(Items, fun labeled_list_item/1).
+
+bulleted_list_item({list_item, _, [{paragraph, _, Text, _}|AST], _}) ->
+ [
".ie n \\{\\\n"
".RS 2\n"
"\\h'-02'\\(bu\\h'+01'\\c\n"
@@ -140,40 +139,85 @@ man_ul([{li, _LiAttrs, [{p, _PAttrs, Text, _PAnn}], _LiAnn}|Tail], Acc0) ->
".sp -1\n"
".IP \\(bu 2.3\n"
".\\}\n",
- man_format(Text), "\n"
- ".RE\n"]|Acc0],
- man_ul(Tail, Acc).
+ inline(Text), "\n",
+ ast(AST),
+ ".RE\n"
+ ].
+
+labeled_list_item({list_item, #{label := Label}, [{paragraph, _, Text, _}|AST], _}) ->
+ [
+ ".PP\n"
+ "\\fB", inline(Label), "\\fR\n",
+ ".RS 4\n",
+ inline(Text), "\n",
+ ast(AST),
+ ".RE\n"
+ ].
+
+%% Tables.
+
+table({table, _, Rows0, _}) ->
+ Rows = table_apply_options(Rows0),
+ [
+ ".TS\n"
+ "allbox tab(:);\n",
+ table_style(Rows), ".\n",
+ table_contents(Rows),
+ ".TE\n"
+ ".sp 1\n"
+ ].
+
+%% @todo Currently acts as if options="headers" was always set.
+table_apply_options([{row, RAttrs, Headers0, RAnn}|Tail]) ->
+ Headers = [{cell, CAttrs, [{strong, #{}, CText, CAnn}], CAnn}
+ || {cell, CAttrs, CText, CAnn} <- Headers0],
+ [{row, RAttrs, Headers, RAnn}|Tail].
+
+table_style(Rows) ->
+ [[table_style_cells(Cells), "\n"]
+ || {row, _, Cells, _} <- Rows].
+
+table_style_cells(Cells) ->
+ ["lt " || {cell, _, _, _} <- Cells].
+
+table_contents(Rows) ->
+ [[table_contents_cells(Cells), "\n"]
+ || {row, _, Cells, _} <- Rows].
+
+table_contents_cells([FirstCell|Cells]) ->
+ [table_contents_cell(FirstCell),
+ [[":", table_contents_cell(Cell)] || Cell <- Cells]].
-man_table_style([], [_|Acc]) ->
- lists:reverse([".\n"|Acc]);
-man_table_style([{row, _, Cols, _}|Tail], Acc) ->
- man_table_style(Tail, [$\n, man_table_style_cols(Cols, [])|Acc]).
+table_contents_cell({cell, _, Text, _}) ->
+ ["T{\n", inline(Text), "\nT}"].
-man_table_style_cols([], [_|Acc]) ->
- lists:reverse(Acc);
-man_table_style_cols([{cell, _, _, _}|Tail], Acc) ->
- man_table_style_cols(Tail, [$\s, "lt"|Acc]).
+%% Comment lines are printed in the generated file
+%% but are not visible in viewers.
-man_table_contents(Rows) ->
- [man_table_contents_cols(Cols, []) || {row, _, Cols, _} <- Rows].
+comment_line({comment_line, _, Text, _}) ->
+ ["\\# ", Text, "\n"].
-man_table_contents_cols([], [_|Acc]) ->
- lists:reverse(["\n"|Acc]);
-man_table_contents_cols([{cell, _CAttrs, [{p, _PAttrs, Text, _PAnn}], _CAnn}|Tail], Acc) ->
- man_table_contents_cols(Tail, [$:, "\nT}", man_format(Text), "T{\n"|Acc]).
+%% Inline formatting.
-man_format(Text) when is_binary(Text) ->
+inline(Text) when is_binary(Text) ->
Text;
-man_format({rel_link, #{target := Link}, Text, _}) ->
+%% When the link is the text we only print it once.
+inline({link, #{target := Link}, Link, _}) ->
+ Link;
+inline({link, #{target := Link}, Text, _}) ->
case re:run(Text, "^([-_:.a-zA-Z0-9]*)(\\([0-9]\\))$", [{capture, all, binary}]) of
nomatch -> [Text, " (", Link, ")"];
{match, [_, ManPage, ManSection]} -> ["\\fB", ManPage, "\\fR", ManSection]
end;
-man_format({strong, _, Text, _}) ->
- ["\\fB", man_format(Text), "\\fR"];
+inline({emphasized, _, Text, _}) ->
+ ["\\fI", inline(Text), "\\fR"];
+inline({strong, _, Text, _}) ->
+ ["\\fB", inline(Text), "\\fR"];
%% We are already using a monospace font.
-%% @todo Maybe there's a readable formatting we could use to differentiate from normal text?
-man_format({mono, _, Text, _}) ->
- man_format(Text);
-man_format(Text) when is_list(Text) ->
- [man_format(T) || T <- Text].
+inline({inline_literal_passthrough, _, Text, _}) ->
+ inline(Text);
+%% Xref links appear as plain text in manuals.
+inline({xref, _, Text, _}) ->
+ inline(Text);
+inline(Text) when is_list(Text) ->
+ [inline(T) || T <- Text].
diff --git a/test/man_SUITE.erl b/test/man_SUITE.erl
index 30eec4f..384bf68 100644
--- a/test/man_SUITE.erl
+++ b/test/man_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2016-2018, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -14,6 +14,7 @@
-module(man_SUITE).
-compile(export_all).
+-compile(nowarn_export_all).
-import(ct_helper, [doc/1]).
diff --git a/test/parser_SUITE.erl b/test/parser_SUITE.erl
index 0f7b393..77b9f7a 100644
--- a/test/parser_SUITE.erl
+++ b/test/parser_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2016, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2016-2018, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -14,14 +14,17 @@
-module(parser_SUITE).
-compile(export_all).
+-compile(nowarn_export_all).
-import(asciideck, [parse/1]).
-import(ct_helper, [doc/1]).
all() ->
- ct_helper:all(?MODULE).
+ [{group, blocks}].
%% @todo Test formatting too!
+groups() ->
+ [{blocks, [parallel], ct_helper:all(?MODULE)}].
%% Empty lines.
@@ -43,20 +46,20 @@ empty_line_spaces(_) ->
quoted_text_strong(_) ->
doc("Strong text formatting. (10.1)"),
- [{p, _, [{strong, _, <<"Hello beautiful world!">>, _}], _}] =
+ [{paragraph, _, [{strong, _, <<"Hello beautiful world!">>, _}], _}] =
parse("*Hello beautiful world!*"),
- [{p, _, [{strong, _, <<"Hello">>, _}, <<" beautiful world!">>], _}] =
+ [{paragraph, _, [{strong, _, <<"Hello">>, _}, <<" beautiful world!">>], _}] =
parse("*Hello* beautiful world!"),
- [{p, _, [<<"Hello ">>, {strong, _, <<"beautiful">>, _}, <<" world!">>], _}] =
+ [{paragraph, _, [<<"Hello ">>, {strong, _, <<"beautiful">>, _}, <<" world!">>], _}] =
parse("Hello *beautiful* world!"),
- [{p, _, [<<"Hello beautiful ">>, {strong, _, <<"world!">>, _}], _}] =
+ [{paragraph, _, [<<"Hello beautiful ">>, {strong, _, <<"world!">>, _}], _}] =
parse("Hello beautiful *world!*"),
- [{p, _, [<<"Hello beautiful ">>, {strong, _, <<"multiline world!">>, _}, <<" lol">>], _}] =
+ [{paragraph, _, [<<"Hello beautiful ">>, {strong, _, <<"multiline world!">>, _}, <<" lol">>], _}] =
parse("Hello beautiful *multiline\nworld!* lol"),
%% Nested formatting.
- [{p, _, [{strong, _, [
+ [{paragraph, _, [{strong, _, [
<<"Hello ">>,
- {rel_link, #{target := <<"downloads/cowboy-2.0.tgz">>}, <<"2.0">>, _},
+ {link, #{target := <<"downloads/cowboy-2.0.tgz">>}, <<"2.0">>, _},
<<" world!">>
], _}], _}] =
parse("*Hello link:downloads/cowboy-2.0.tgz[2.0] world!*"),
@@ -64,18 +67,18 @@ quoted_text_strong(_) ->
quoted_text_literal_mono(_) ->
doc("Literal monospace text formatting. (10.1)"),
- [{p, _, [{mono, _, <<"Hello beautiful world!">>, _}], _}] =
+ [{paragraph, _, [{inline_literal_passthrough, _, <<"Hello beautiful world!">>, _}], _}] =
parse("`Hello beautiful world!`"),
- [{p, _, [{mono, _, <<"Hello">>, _}, <<" beautiful world!">>], _}] =
+ [{paragraph, _, [{inline_literal_passthrough, _, <<"Hello">>, _}, <<" beautiful world!">>], _}] =
parse("`Hello` beautiful world!"),
- [{p, _, [<<"Hello ">>, {mono, _, <<"beautiful">>, _}, <<" world!">>], _}] =
+ [{paragraph, _, [<<"Hello ">>, {inline_literal_passthrough, _, <<"beautiful">>, _}, <<" world!">>], _}] =
parse("Hello `beautiful` world!"),
- [{p, _, [<<"Hello beautiful ">>, {mono, _, <<"world!">>, _}], _}] =
+ [{paragraph, _, [<<"Hello beautiful ">>, {inline_literal_passthrough, _, <<"world!">>, _}], _}] =
parse("Hello beautiful `world!`"),
- [{p, _, [<<"Hello beautiful ">>, {mono, _, <<"multiline world!">>, _}, <<" lol">>], _}] =
+ [{paragraph, _, [<<"Hello beautiful ">>, {inline_literal_passthrough, _, <<"multiline\nworld!">>, _}, <<" lol">>], _}] =
parse("Hello beautiful `multiline\nworld!` lol"),
%% No text formatting must occur inside backticks.
- [{p, _, [{mono, _, <<"Hello *beautiful* world!">>, _}], _}] =
+ [{paragraph, _, [{inline_literal_passthrough, _, <<"Hello *beautiful* world!">>, _}], _}] =
parse("`Hello *beautiful* world!`"),
ok.
@@ -86,110 +89,110 @@ quoted_text_literal_mono(_) ->
title_short(_) ->
doc("The trailing title delimiter is optional. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world!"),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world!"),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world!"),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world!"),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world!"),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world!"),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world!"),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world!"),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world!"),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world!"),
ok.
title_short_no_spaces(_) ->
doc("One or more spaces must fall between the title and the delimiter. (11.2)"),
- [{p, _, <<"=Hello world!">>, _}] = parse("=Hello world!"),
- [{p, _, <<"==Hello world!">>, _}] = parse("==Hello world!"),
- [{p, _, <<"===Hello world!">>, _}] = parse("===Hello world!"),
- [{p, _, <<"====Hello world!">>, _}] = parse("====Hello world!"),
- [{p, _, <<"=====Hello world!">>, _}] = parse("=====Hello world!"),
+ [{paragraph, _, <<"=Hello world!">>, _}] = parse("=Hello world!"),
+ [{paragraph, _, <<"==Hello world!">>, _}] = parse("==Hello world!"),
+ [{paragraph, _, <<"===Hello world!">>, _}] = parse("===Hello world!"),
+ [{paragraph, _, <<"====Hello world!">>, _}] = parse("====Hello world!"),
+ [{paragraph, _, <<"=====Hello world!">>, _}] = parse("=====Hello world!"),
ok.
title_short_trim_spaces_before(_) ->
doc("Spaces between the title and delimiter must be ignored. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world!"),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world!"),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world!"),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world!"),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world!"),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world!"),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world!"),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world!"),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world!"),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world!"),
ok.
title_short_trim_spaces_after(_) ->
doc("Spaces after the title must be ignored. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! "),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! "),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! "),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! "),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! "),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! "),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! "),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! "),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! "),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! "),
ok.
title_short_trim_spaces_before_after(_) ->
doc("Spaces before and after the title must be ignored. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! "),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! "),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! "),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! "),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! "),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! "),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! "),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! "),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! "),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! "),
ok.
title_short_trailer(_) ->
doc("The trailing title delimiter is optional. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! ="),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! =="),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! ==="),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ===="),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ====="),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! ="),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! =="),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! ==="),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ===="),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ====="),
ok.
title_short_trailer_no_spaces(_) ->
doc("One or more spaces must fall between the title and the trailer. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!=">>, _}] = parse("= Hello world!="),
- [{title, #{level := 1}, <<"Hello world!==">>, _}] = parse("== Hello world!=="),
- [{title, #{level := 2}, <<"Hello world!===">>, _}] = parse("=== Hello world!==="),
- [{title, #{level := 3}, <<"Hello world!====">>, _}] = parse("==== Hello world!===="),
- [{title, #{level := 4}, <<"Hello world!=====">>, _}] = parse("===== Hello world!====="),
+ [{section_title, #{level := 0}, <<"Hello world!=">>, _}] = parse("= Hello world!="),
+ [{section_title, #{level := 1}, <<"Hello world!==">>, _}] = parse("== Hello world!=="),
+ [{section_title, #{level := 2}, <<"Hello world!===">>, _}] = parse("=== Hello world!==="),
+ [{section_title, #{level := 3}, <<"Hello world!====">>, _}] = parse("==== Hello world!===="),
+ [{section_title, #{level := 4}, <<"Hello world!=====">>, _}] = parse("===== Hello world!====="),
ok.
title_short_trim_spaces_before_trailer(_) ->
doc("Spaces between the title and trailer must be ignored. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! ="),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! =="),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! ==="),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ===="),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ====="),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! ="),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! =="),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! ==="),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ===="),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ====="),
ok.
title_short_trim_spaces_after_trailer(_) ->
doc("Spaces after the trailer must be ignored. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! = "),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! == "),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! === "),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ==== "),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ===== "),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! = "),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! == "),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! === "),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ==== "),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ===== "),
ok.
title_short_trim_spaces_before_after_trailer(_) ->
doc("Spaces before and after the trailer must be ignored. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! = "),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! == "),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! === "),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ==== "),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ===== "),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! = "),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! == "),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! === "),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ==== "),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ===== "),
ok.
title_short_trim_spaces_before_after_title_trailer(_) ->
doc("Spaces before and after both the title and the trailer must be ignored. (11.2)"),
- [{title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! = "),
- [{title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! == "),
- [{title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! === "),
- [{title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ==== "),
- [{title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ===== "),
+ [{section_title, #{level := 0}, <<"Hello world!">>, _}] = parse("= Hello world! = "),
+ [{section_title, #{level := 1}, <<"Hello world!">>, _}] = parse("== Hello world! == "),
+ [{section_title, #{level := 2}, <<"Hello world!">>, _}] = parse("=== Hello world! === "),
+ [{section_title, #{level := 3}, <<"Hello world!">>, _}] = parse("==== Hello world! ==== "),
+ [{section_title, #{level := 4}, <<"Hello world!">>, _}] = parse("===== Hello world! ===== "),
ok.
title_short_wrong_trailer(_) ->
doc("The delimiters must be the same size when a trailer is present. (11.2)"),
- [{title, #{level := 0}, <<"Hello world! ===">>, _}] = parse("= Hello world! ==="),
- [{title, #{level := 1}, <<"Hello world! ====">>, _}] = parse("== Hello world! ===="),
- [{title, #{level := 2}, <<"Hello world! =====">>, _}] = parse("=== Hello world! ====="),
- [{title, #{level := 3}, <<"Hello world! =">>, _}] = parse("==== Hello world! ="),
- [{title, #{level := 4}, <<"Hello world! ==">>, _}] = parse("===== Hello world! =="),
+ [{section_title, #{level := 0}, <<"Hello world! ===">>, _}] = parse("= Hello world! ==="),
+ [{section_title, #{level := 1}, <<"Hello world! ====">>, _}] = parse("== Hello world! ===="),
+ [{section_title, #{level := 2}, <<"Hello world! =====">>, _}] = parse("=== Hello world! ====="),
+ [{section_title, #{level := 3}, <<"Hello world! =">>, _}] = parse("==== Hello world! ="),
+ [{section_title, #{level := 4}, <<"Hello world! ==">>, _}] = parse("===== Hello world! =="),
ok.
%% Normal paragraphs.
@@ -198,13 +201,13 @@ title_short_wrong_trailer(_) ->
paragraph(_) ->
doc("Normal paragraph. (15.1)"),
- [{p, _, <<"Hello world this is a paragraph peace.">>, _}] = parse(
+ [{paragraph, _, <<"Hello world this is a paragraph peace.">>, _}] = parse(
"Hello world\n"
"this is a paragraph\n"
"peace.\n"),
[
- {p, _, <<"Hello world this is a paragraph peace.">>, _},
- {p, _, <<"This is another paragraph.">>, _}
+ {paragraph, _, <<"Hello world this is a paragraph peace.">>, _},
+ {paragraph, _, <<"This is another paragraph.">>, _}
] = parse(
"Hello world\n"
"this is a paragraph\n"
@@ -215,7 +218,7 @@ paragraph(_) ->
paragraph_title(_) ->
doc("Paragraph preceded by a block title. (12, 15.1)"),
- [{p, #{title := <<"Block title!">>}, <<"Hello world this is a paragraph peace.">>, _}] = parse(
+ [{paragraph, #{<<"title">> := <<"Block title!">>}, <<"Hello world this is a paragraph peace.">>, _}] = parse(
".Block title!\n"
"Hello world\n"
"this is a paragraph\n"
@@ -229,7 +232,7 @@ listing(_) ->
Source = <<
"init(Req, State) ->\n"
" {ok, Req, State}.">>,
- [{listing, _, Source, _}] = parse(iolist_to_binary([
+ [{listing_block, _, Source, _}] = parse(iolist_to_binary([
"----\n",
Source, "\n"
"----\n"])),
@@ -237,7 +240,7 @@ listing(_) ->
listing_title(_) ->
doc("Listing block with title. (12, 16.2)"),
- [{listing, #{title := <<"Block title!">>}, <<"1 = 2.">>, _}] = parse(
+ [{listing_block, #{<<"title">> := <<"Block title!">>}, <<"1 = 2.">>, _}] = parse(
".Block title!\n"
"----\n"
"1 = 2.\n"
@@ -249,7 +252,7 @@ listing_filter_source(_) ->
Source = <<
"init(Req, State) ->\n"
" {ok, Req, State}.">>,
- [{listing, #{language := <<"erlang">>}, Source, _}] = parse(iolist_to_binary([
+ [{listing_block, #{1 := <<"source">>, 2 := <<"erlang">>}, Source, _}] = parse(iolist_to_binary([
"[source,erlang]\n"
"----\n",
Source, "\n"
@@ -258,13 +261,13 @@ listing_filter_source(_) ->
listing_filter_source_title(_) ->
doc("Source code listing filter with title. (12, source-highlight-filter)"),
- [{listing, #{language := <<"erlang">>, title := <<"Block title!">>}, <<"1 = 2.">>, _}] = parse(
+ [{listing_block, #{1 := <<"source">>, 2 := <<"erlang">>, <<"title">> := <<"Block title!">>}, <<"1 = 2.">>, _}] = parse(
".Block title!\n"
"[source,erlang]\n"
"----\n"
"1 = 2.\n"
"----\n"),
- [{listing, #{language := <<"erlang">>, title := <<"Block title!">>}, <<"1 = 2.">>, _}] = parse(
+ [{listing_block, #{1 := <<"source">>, 2 := <<"erlang">>, <<"title">> := <<"Block title!">>}, <<"1 = 2.">>, _}] = parse(
"[source,erlang]\n"
".Block title!\n"
"----\n"
@@ -276,13 +279,13 @@ listing_filter_source_title(_) ->
unordered_list(_) ->
doc("Unoredered lists. (17.1)"),
- [{ul, _, [
- {li, _, [{p, _, <<"Hello!">>, _}], _}
+ [{list, #{type := bulleted}, [
+ {list_item, _, [{paragraph, #{}, <<"Hello!">>, _}], _}
], _}] = parse("* Hello!"),
- [{ul, _, [
- {li, _, [{p, _, <<"Hello!">>, _}], _},
- {li, _, [{p, _, <<"World!">>, _}], _},
- {li, _, [{p, _, <<"Hehe.">>, _}], _}
+ [{list, #{type := bulleted}, [
+ {list_item, _, [{paragraph, #{}, <<"Hello!">>, _}], _},
+ {list_item, _, [{paragraph, #{}, <<"World!">>, _}], _},
+ {list_item, _, [{paragraph, #{}, <<"Hehe.">>, _}], _}
], _}] = parse(
"* Hello!\n"
"* World!\n"
@@ -300,47 +303,92 @@ unordered_list(_) ->
labeled_list(_) ->
doc("Labeled lists. (17.3)"),
- [{ll, _, [
- {li, #{label := <<"The label">>}, [{p, _, <<"The value!">>, _}], _}
+ [{list, #{type := labeled}, [
+ {list_item, #{label := <<"The label">>},
+ [{paragraph, #{}, <<"The value!">>, _}], _}
], _}] = parse("The label:: The value!"),
- %% @todo Currently this returns two ll. This is a bug but it gives
- %% me the result I want, or close enough, for now.
- [{ll, _, [
- {li, #{label := <<"The label">>}, [{p, _, <<"The value!">>, _}], _}
- ], _},
- {ll, _, [
- {li, #{label := <<"More labels">>}, [{p, _, <<"More values!">>, _}], _}
+ [{list, #{type := labeled}, [
+ {list_item, #{label := <<"The label">>},
+ [{paragraph, #{}, <<"The value!">>, _}], _},
+ {list_item, #{label := <<"More labels">>},
+ [{paragraph, #{}, <<"More values!">>, _}], _}
], _}] = parse(
"The label:: The value!\n"
"More labels:: More values!\n"),
- [{ll, _, [
- {li, #{label := <<"The label">>}, [{p, _, <<"The value!">>, _}], _}
+ [{list, #{type := labeled}, [
+ {list_item, #{label := <<"The label">>},
+ [{paragraph, #{}, <<"The value!">>, _}], _}
], _}] = parse(
"The label::\n"
"\n"
"The value!"),
+ [{list, #{type := labeled}, [
+ {list_item, #{label := <<"The label">>},
+ [{paragraph, #{}, <<"The value!">>, _}], _}
+ ], _}] = parse(
+ "The label::\n"
+ " The value!"),
+ [{list, #{type := labeled}, [
+ {list_item, #{label := <<"The label">>}, [
+ {paragraph, _, <<"The value!">>, _},
+ {paragraph, _, <<"With continuations!">>, _},
+ {paragraph, _, <<"OK good.">>, _}
+ ], _}
+ ], _}] = parse(
+ "The label::\n"
+ "\n"
+ "The value!\n"
+ "+\n"
+ "With continuations!\n"
+ "+\n"
+ "OK good."),
+ [{list, #{type := labeled}, [
+ {list_item, #{label := <<"The label">>}, [
+ {paragraph, #{}, <<"The value!">>, _},
+ {list, #{type := bulleted}, [
+ {list_item, _, [{paragraph, #{}, <<"first list item">>, _}], _},
+ {list_item, _, [{paragraph, #{}, <<"second list item">>, _}], _},
+ {list_item, _, [{paragraph, #{}, <<"third list item">>, _}], _}
+ ], _}
+ ], _}
+ ], _}] = parse(
+ "The label::\n"
+ "\n"
+ "The value!\n"
+ "+\n"
+ " * first list item\n"
+ " * second list\n"
+ " item\n"
+ " * third list\n"
+ " item\n"
+ "\n"),
ok.
-%% @todo Very little was implemented from labeled lists. They need more work.
-
%% Macros.
rel_link(_) ->
- doc("Relative links are built using the link:<target>[<caption>] macro. (21.1.3)"),
- [{p, _, [
- {rel_link, #{target := <<"downloads/cowboy-2.0.tgz">>}, <<"2.0">>, _}
+ doc("Relative links are built using the link:Target[Caption] macro. (21.1.3)"),
+ [{paragraph, _, [
+ {link, #{target := <<"downloads/cowboy-2.0.tgz">>}, <<"2.0">>, _}
], _}] = parse("link:downloads/cowboy-2.0.tgz[2.0]"),
- [{p, _, [
+ [{paragraph, _, [
<<"Download ">>,
- {rel_link, #{target := <<"downloads/cowboy-2.0.zip">>}, <<"Cowboy 2.0">>, _},
+ {link, #{target := <<"downloads/cowboy-2.0.zip">>}, <<"Cowboy 2.0">>, _},
<<" as zip">>
], _}] = parse("Download link:downloads/cowboy-2.0.zip[Cowboy 2.0] as zip"),
ok.
comment_line(_) ->
doc("Lines starting with two slashes are treated as comments. (21.2.3)"),
- [{comment, _, <<"This is a comment.">>, _}] = parse("// This is a comment."),
- [{comment, _, <<"This is a comment.">>, _}] = parse("// This is a comment. "),
+ [{comment_line, _, <<"This is a comment.">>, _}] = parse("//This is a comment."),
+ [{comment_line, _, <<"This is a comment.">>, _}] = parse("// This is a comment."),
+ [{comment_line, _, <<"This is a comment.">>, _}] = parse("// This is a comment. "),
+ [
+ {comment_line, _, <<"First line.">>, _},
+ {comment_line, _, <<"Second line.">>, _}
+ ] = parse(
+ "// First line.\n"
+ "// Second line.\n"),
ok.
%% Tables. (23)
@@ -349,19 +397,19 @@ table(_) ->
%% @todo I think I read somewhere that paragraphs are not allowed in cells... Double check.
[{table, _, [
{row, _, [
- {cell, _, [{p, _, <<"1">>, _}], _},
- {cell, _, [{p, _, <<"2">>, _}], _},
- {cell, _, [{p, _, <<"A">>, _}], _}
+ {cell, _, <<"1">>, _},
+ {cell, _, <<"2">>, _},
+ {cell, _, <<"A">>, _}
], _},
{row, _, [
- {cell, _, [{p, _, <<"3">>, _}], _},
- {cell, _, [{p, _, <<"4">>, _}], _},
- {cell, _, [{p, _, <<"B">>, _}], _}
+ {cell, _, <<"3">>, _},
+ {cell, _, <<"4">>, _},
+ {cell, _, <<"B">>, _}
], _},
{row, _, [
- {cell, _, [{p, _, <<"5">>, _}], _},
- {cell, _, [{p, _, <<"6">>, _}], _},
- {cell, _, [{p, _, <<"C">>, _}], _}
+ {cell, _, <<"5">>, _},
+ {cell, _, <<"6">>, _},
+ {cell, _, <<"C">>, _}
], _}
], _}]= parse(
"|=======\n"