diff options
-rw-r--r-- | LICENSE | 13 | ||||
-rw-r--r-- | Makefile | 21 | ||||
-rw-r--r-- | ebin/asciideck.app | 6 | ||||
-rw-r--r-- | erlang.mk | 1490 | ||||
-rw-r--r-- | src/asciideck.erl | 13 | ||||
-rw-r--r-- | src/asciideck_attributes_parser.erl | 120 | ||||
-rw-r--r-- | src/asciideck_attributes_pass.erl | 112 | ||||
-rw-r--r-- | src/asciideck_block_parser.erl | 1116 | ||||
-rw-r--r-- | src/asciideck_inline_pass.erl | 308 | ||||
-rw-r--r-- | src/asciideck_line_reader.erl | 94 | ||||
-rw-r--r-- | src/asciideck_lists_pass.erl | 155 | ||||
-rw-r--r-- | src/asciideck_parser.erl | 388 | ||||
-rw-r--r-- | src/asciideck_tables_pass.erl | 191 | ||||
-rw-r--r-- | src/asciideck_to_manpage.erl | 236 | ||||
-rw-r--r-- | test/man_SUITE.erl | 3 | ||||
-rw-r--r-- | test/parser_SUITE.erl | 286 |
16 files changed, 3402 insertions, 1150 deletions
@@ -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. @@ -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, []} @@ -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, "with an image" image:smallnew.png[]">> + } = parse(<<"A footnote, "with an image" 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, "with an image" image:smallnew.png[]">> + }, <<>>, _}] + = parse(<<"[A footnote, "with an image" 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" |