Repository: joedevivo/chatterbox Branch: master Commit: c0506c707bd1 Files: 122 Total size: 1.1 MB Directory structure: gitextract_bky7wddq/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── config/ │ ├── localhost.crt │ ├── localhost.key │ ├── localhost.key.pub │ └── sys.config ├── include/ │ └── http2.hrl ├── priv/ │ └── index.html ├── rebar.config ├── rebar.config.script ├── src/ │ ├── chatterbox.app.src │ ├── chatterbox.erl │ ├── chatterbox_ranch_protocol.erl │ ├── chatterbox_static_content_handler.erl │ ├── chatterbox_static_stream.erl │ ├── chatterbox_sup.erl │ ├── h2_client.erl │ ├── h2_connection.erl │ ├── h2_frame.erl │ ├── h2_frame_continuation.erl │ ├── h2_frame_data.erl │ ├── h2_frame_goaway.erl │ ├── h2_frame_headers.erl │ ├── h2_frame_ping.erl │ ├── h2_frame_priority.erl │ ├── h2_frame_push_promise.erl │ ├── h2_frame_rst_stream.erl │ ├── h2_frame_settings.erl │ ├── h2_frame_window_update.erl │ ├── h2_padding.erl │ ├── h2_settings.erl │ ├── h2_stream.erl │ ├── h2_stream_set.erl │ └── sock.erl └── test/ ├── chatterbox_test_buddy.erl ├── chatterbox_tests.erl ├── client_server_SUITE.erl ├── client_server_SUITE_data/ │ ├── README.md │ ├── bower.json │ ├── css/ │ │ ├── print/ │ │ │ ├── paper.css │ │ │ └── pdf.css │ │ ├── reveal.css │ │ ├── reveal.scss │ │ └── theme/ │ │ ├── README.md │ │ ├── beige.css │ │ ├── black.css │ │ ├── blood.css │ │ ├── league.css │ │ ├── moon.css │ │ ├── night.css │ │ ├── serif.css │ │ ├── simple.css │ │ ├── sky.css │ │ ├── solarized.css │ │ ├── source/ │ │ │ ├── beige.scss │ │ │ ├── black.scss │ │ │ ├── blood.scss │ │ │ ├── league.scss │ │ │ ├── moon.scss │ │ │ ├── night.scss │ │ │ ├── serif.scss │ │ │ ├── simple.scss │ │ │ ├── sky.scss │ │ │ ├── solarized.scss │ │ │ └── white.scss │ │ ├── template/ │ │ │ ├── mixins.scss │ │ │ ├── settings.scss │ │ │ └── theme.scss │ │ └── white.css │ ├── index.html │ ├── js/ │ │ └── reveal.js │ ├── lib/ │ │ ├── css/ │ │ │ └── zenburn.css │ │ ├── font/ │ │ │ ├── league-gothic/ │ │ │ │ ├── LICENSE │ │ │ │ └── league-gothic.css │ │ │ └── source-sans-pro/ │ │ │ ├── LICENSE │ │ │ └── source-sans-pro.css │ │ └── js/ │ │ ├── classList.js │ │ └── html5shiv.js │ └── plugin/ │ ├── highlight/ │ │ └── highlight.js │ ├── markdown/ │ │ ├── example.html │ │ ├── example.md │ │ ├── markdown.js │ │ └── marked.js │ ├── math/ │ │ └── math.js │ ├── multiplex/ │ │ ├── client.js │ │ ├── index.js │ │ └── master.js │ ├── notes/ │ │ ├── notes.html │ │ └── notes.js │ ├── notes-server/ │ │ ├── client.js │ │ ├── index.js │ │ └── notes.html │ ├── print-pdf/ │ │ └── print-pdf.js │ ├── search/ │ │ └── search.js │ └── zoom-js/ │ └── zoom.js ├── double_body_handler.erl ├── echo_handler.erl ├── flow_control_SUITE.erl ├── flow_control_handler.erl ├── header_continuation_SUITE.erl ├── http2_frame_size_SUITE.erl ├── http2_spec_3_5_SUITE.erl ├── http2_spec_4_3_SUITE.erl ├── http2_spec_5_1_SUITE.erl ├── http2_spec_5_3_SUITE.erl ├── http2_spec_5_5_SUITE.erl ├── http2_spec_6_1_SUITE.erl ├── http2_spec_6_2_SUITE.erl ├── http2_spec_6_4_SUITE.erl ├── http2_spec_6_5_SUITE.erl ├── http2_spec_6_9_SUITE.erl ├── http2_spec_8_1_SUITE.erl ├── http2c.erl ├── peer_test_handler.erl ├── protocol_errors_SUITE.erl ├── server_connection_receive_window.erl ├── server_stream_receive_window.erl ├── settings_handshake_SUITE.erl └── starting_SUITE.erl ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ebin/* *.beam deps/* rel/chatterbox .eunit _rel relx .rebar erln8.config .DS_Store common_test/logs .edts erl_crash.dump _build rebar3 .rebar3 log .*.sw? ================================================ FILE: .travis.yml ================================================ language: erlang otp_release: - 21.0 - 20.0 - 19.3 before_script: kerl list installations before_install: - wget https://github.com/erlang/rebar3/releases/download/3.6.1/rebar3 -O rebar3 - chmod +x rebar3 install: make clean script: make all ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Joe DeVivo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ REBAR3_URL=https://s3.amazonaws.com/rebar3/rebar3 # If there is a rebar in the current directory, use it ifeq ($(wildcard rebar3),rebar3) REBAR3 = $(CURDIR)/rebar3 endif # Fallback to rebar on PATH REBAR3 ?= $(shell which rebar3) # And finally, prep to download rebar if all else fails ifeq ($(REBAR3),) REBAR3 = $(CURDIR)/rebar3 endif clean: $(REBAR3) @$(REBAR3) clean rm -rf _build all: $(REBAR3) @$(REBAR3) do clean, compile, eunit, ct, dialyzer rel: all @$(REBAR3) release $(REBAR3): curl -Lo rebar3 $(REBAR3_URL) || wget $(REBAR3_URL) chmod a+x rebar3 ================================================ FILE: README.md ================================================ # chatterbox # Chatterbox is an HTTP/2 library for Erlang. Use as much of it as you want, but the goal is to implement as much of [RFC-7540](https://tools.ietf.org/html/rfc7540) as possible, 100% of the time. It already pulls in [joedevivo/hpack](https://github.com/joedevivo/hpack) for the implementation of [RFC-7541](https://tools.ietf.org/html/rfc7541). ## Rebar3 Chatterbox is a `rebar3` jam. Get into it! rebar3.org ## HTTP/2 Connections Chatterbox provides a module `h2_connection` which models (you guessed it!) an HTTP/2 connection. This gen_statem can represent either side of an HTTP/2 connection (i.e. a client *or* server). Really, the only difference is in how you start the connection. ### Server Side Connections A server side connection can be started with [ninenines/ranch](https://github.com/ninenines/ranch) using the included `chatterbox_ranch_protocol` like this: ```erlang %% Set up the socket options: Options = [ {port, 8080}, %% you can find certs to play with in ./config {certfile, "localhost.crt"}, {keyfile, "localhost.key"}, {honor_cipher_order, false}, {versions, ['tlsv1.2']}, {next_protocols_advertised, [<<"h2">>]} ], %% You'll also have to set the content root for the chatterbox static %% content handler RootDir = "/path/to/content", application:set_env( chatterbox, stream_callback_mod, chatterbox_static_stream), application:set_env( chatterbox, chatterbox_static_stream, [{root_dir, RootDir}]), {ok, _RanchPid} = ranch:start_listener( chatterbox_ranch_protocol, 10, ranch_ssl, Options, chatterbox_ranch_protocol, []), ``` You can do this in a `rebar3 shell` or in your application. You don't have to use ranch. You can use see chatterbox_sup for an alternative. ### Serving up the EUC 2015 Deck clone chatterbox and [euc2015](https://github.com/joedevivo/euc2015) then set the `RootDir` above to the checkout of euc2015. Then it should be as easy as pointing Firefox to `https://localhost:8080/`. ### Client Side Connections We'll start up h2_connection a little differently. `h2_client:start_link/*` will take care of the differences. Here's how to use it! ```erlang {ok, Pid} = h2_client:start_link(), RequestHeaders = [ {<<":method">>, <<"GET">>}, {<<":path">>, <<"/index.html">>}, {<<":scheme">>, <<"http">>}, {<<":authority">>, <<"localhost:8080">>}, {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip, deflate">>}, {<<"user-agent">>, <<"chatterbox-client/0.0.1">>} ], RequestBody = <<>>, {ok, {ResponseHeaders, ResponseBody}} = h2_client:sync_request(Pid, RequestHeaders, RequestBody). ``` But wait! There's more. If you want to be smart about multiplexing, you can make an async request on a stream like this! ``` erlang {ok, StreamId} = h2_client:send_request(Pid, RequestHeaders, <<>>). ``` Now you can just hang around and wait. I'm pretty sure (read: UNTESTED) that you'll have a process message like `{'END_STREAM', StreamId}` waiting for you in your process mailbox, since that's how `sync_request/3` works, and they use mostly the same codepath. So you can use a receive block, like this ```erlang receive {'END_STREAM', StreamId} -> {ok, {ResponseHeaders, ResponseBody}} = h2_client:get_response(Pid, StreamId) end, ``` Or you can manually check for a response by calling ```erlang h2_client:get_response(Pid, StreamId), ``` which will return an `{error, stream_not_finished}` if it's... well... not finished. There still needs to be some way of detecting that a promise has been pushed from the server, either actively or passively. In theory, if you knew the `StreamId`, you could check for a response on that stream and it would be there. ## Author(s) ## * Joe DeVivo ## Copyright ## Copyright (c) 2015,16 Joe DeVivo, dat MIT License tho. ================================================ FILE: config/localhost.crt ================================================ -----BEGIN CERTIFICATE----- MIIDCDCCAfACCQCyOlNYMIzE4TANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV UzEQMA4GA1UECBMHQXJpem9uYTEQMA4GA1UEBxMHUGhvZW5peDETMBEGA1UEChMK Q2hhdHRlcmJveDAeFw0xNTA2MDcxODIzMjNaFw00NTA3MTkxODIzMjNaMEYxCzAJ BgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRAwDgYDVQQHEwdQaG9lbml4MRMw EQYDVQQKEwpDaGF0dGVyYm94MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAxa9PMgPb7zJmHGswAjZ2h9JvAm9DrRnMcVeQFz+8VbL27gABZ4l+cwK0KbGg WKL5bXJID51XtRs3wtHhX1NYJ7J2tW44fBDczFAYxUUFr9Ts6KM7Kx1Pzq1HAtaW Rc+shXBgV+Q7gVAMnWPagiD2IkOglpA2CJOSSdbY3QzP0Q49TEkO4xcJEBy0MtqA 3R3pb1nisEX5K2UJ/FBPPpozR3aOxGMcF/ULze8sBCxOUDFSjiptzBUV/FCh3F7y +V35xcTLXG7P42pNIDVWUC4tAbvmXgnryoz1qj+vVUoh+dIUDCOggCLydrRhlKSv kvcqZsVBmn8ore09gY/YAH136wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQCwNhg8 rI9cE9+05DTX7b9MyZNtFteFwPO8xfBmZVee0gbwqEdgVoDg+T1/KvzmEeKEim9S JHLn8ZyCkQA4Kj+J8yPpfZC3zw3ovHrZhHtDQIE3UJSx1rrF5h3VSTT76p5z4HNM Cm/leFGDqXtELjONsF//+SFQXP6jopzPqBnXxQ54+1osoi/fmZSg/NGImJA+Q+RR fCeGkEZpA8Hapq8vv7OKIGNNc87EuBRUc1OoQ8xUXp2GQV4UcVyA26BV4kGfeYOF 7D3aj/Ugtp8vOLCD72GzDBe3ng0u1kkIgYgwRuiYqF9qsMOtjQInw9OHO9aT9IXS FkWoN9NOLvEaBUaO -----END CERTIFICATE----- ================================================ FILE: config/localhost.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAxa9PMgPb7zJmHGswAjZ2h9JvAm9DrRnMcVeQFz+8VbL27gAB Z4l+cwK0KbGgWKL5bXJID51XtRs3wtHhX1NYJ7J2tW44fBDczFAYxUUFr9Ts6KM7 Kx1Pzq1HAtaWRc+shXBgV+Q7gVAMnWPagiD2IkOglpA2CJOSSdbY3QzP0Q49TEkO 4xcJEBy0MtqA3R3pb1nisEX5K2UJ/FBPPpozR3aOxGMcF/ULze8sBCxOUDFSjipt zBUV/FCh3F7y+V35xcTLXG7P42pNIDVWUC4tAbvmXgnryoz1qj+vVUoh+dIUDCOg gCLydrRhlKSvkvcqZsVBmn8ore09gY/YAH136wIDAQABAoIBAQCvScPvlXxvnUDt 8h2f2KtBxFaq0f4wf6/I0NvzwZA+bXKHl3mnVUPKt5sJXXfRILILWkqLjfk3nerT 1UcCP+TrTlP0jMeJO2qNwHg2c/2W7DcmEZdlo5ggq4VL/vtA6+UObZcAWGBrSY/l /6TBvniB6XV8DGPdNv2AfAHQAIxF0clLcLraUcyN9oOzLXj8uXsOfAf0mR7uyARY hL2bZ2nUkLH1DDYXgCwdeW2Vs/M8F93GIxzXbVuIKVQNzvDQ9AQoYWaaLVEN+TWY tQGkuAL5sQNDdABVxtrzVKBezrcTAkRDyYSs/DXbXEUakRoOCIZfmVmUaGqVIONM ie9U8rgBAoGBAPpeHxmEqHA9GPehkeojn9x8MOSpYcjasVglkVrOeG24zUzKFL65 aM6Uhrtn2ZIurWIpyuyHf964uPeuZmCqW5hp2dnmsZ9Zjc857B10Yw768eUT8vO0 NHSJAiKx2ZonkHX6IuffCwmTg9wlzctTtiiGLF69thaX8ayjSoZJdQV9AoGBAMoh yPGnriTOxaHhs4fgvc4K4dhQnQ3iSt7OP2vWmCmpwFkSR2h9Um0F7geptV+5tpLw Nefq4a25a9cL+/X5a3NHK7kXe98UP2+5RSisvNhzY2SVH7FHFuFL4q2uOk1Pv4qX BFlgPmG8M8LvlmC6BtaQYxDmFKJy+MelZuNXTk+HAoGAA2Zn0bblerC5uBMvohhd wWbGWzSZqVqe8e2ArdUD+al60EImSfjGnZeSxNTCNaQAosaihNfKOsITcPmjVki5 +bXmSXlCjEFxFZFZzYSZG8j4o/3DXN/jnnmF1+bGZ7uF0LRW6QM0aSrhrYmt48b9 QEuiKp8069WgaJHHH0+8ERkCgYEAlw6Sjk4CrY09UxJKSeRh1GZ7i14LUQHpdALs kJmp05EBp08qwGLPw5wn2+AvJJ+0WrFbh7sX9u1YMzjIjnVcoKTyfvuW3grSsZri nVgiNRxejh+HtMNszOgaOjO3bGmJunfLj0OGuyGcCTVly1manKUA8/MOPqzvULxC XOm1I2cCgYBo0TVdz/Hi+nzwG8vEgXcH5a0bXALvXAyum5YFkTLYaPNQldw7KqUD pWDgA9mhd+cwCodHk2TMfi88vCsZhZyeR/HCwqiWlJcuOlvTXodZr8nozhH2Kivc 9ks9/8Iy5TFL63tMWoYCMraoAocrVWekA1C+2AjbZmyq6ptKhohGqA== -----END RSA PRIVATE KEY----- ================================================ FILE: config/localhost.key.pub ================================================ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFr08yA9vvMmYcazACNnaH0m8Cb0OtGcxxV5AXP7xVsvbuAAFniX5zArQpsaBYovltckgPnVe1GzfC0eFfU1gnsna1bjh8ENzMUBjFRQWv1OzoozsrHU/OrUcC1pZFz6yFcGBX5DuBUAydY9qCIPYiQ6CWkDYIk5JJ1tjdDM/RDj1MSQ7jFwkQHLQy2oDdHelvWeKwRfkrZQn8UE8+mjNHdo7EYxwX9QvN7ywELE5QMVKOKm3MFRX8UKHcXvL5XfnFxMtcbs/jak0gNVZQLi0Bu+ZeCevKjPWqP69VSiH50hQMI6CAIvJ2tGGUpK+S9ypmxUGafyit7T2Bj9gAfXfr root@remdevivo01.local ================================================ FILE: config/sys.config ================================================ [ {chatterbox, [ {port, 8081}, {ssl, true}, {ssl_options, [{certfile, "localhost.crt"}, {keyfile, "localhost.key"}, {honor_cipher_order, false}, {versions, ['tlsv1.2']}, {alpn_preferred_protocols, [<<"h2">>]}]}, {chatterbox_static_content_handler, [ {root_dir, "/home/joe/dev/joedevivo/euc2015"} ]}, {chatterbox_static_stream, [ {root_dir, "/home/joe/dev/joedevivo/euc2015"} ]} ]} ]. ================================================ FILE: include/http2.hrl ================================================ %% FRAME TYPES -define(DATA , 16#0). -define(HEADERS , 16#1). -define(PRIORITY , 16#2). -define(RST_STREAM , 16#3). -define(SETTINGS , 16#4). -define(PUSH_PROMISE , 16#5). -define(PING , 16#6). -define(GOAWAY , 16#7). -define(WINDOW_UPDATE , 16#8). -define(CONTINUATION , 16#9). -type frame_type() :: ?DATA | ?HEADERS | ?PRIORITY | ?RST_STREAM | ?SETTINGS | ?PUSH_PROMISE | ?PING | ?GOAWAY | ?WINDOW_UPDATE | ?CONTINUATION | integer(). %% Currently unsupported future frame types -define(FT, fun(?DATA) -> "DATA"; (?HEADERS) -> "HEADERS"; (?PRIORITY) -> "PRIORITY"; (?RST_STREAM) -> "RST_STREAM"; (?SETTINGS) -> "SETTINGS"; (?PUSH_PROMISE) -> "PUSH_PROMISE"; (?PING) -> "PING"; (?GOAWAY) -> "GOAWAY"; (?WINDOW_UPDATE) -> "WINDOW_UPDATE"; (?CONTINUATION) -> "CONTINUATION"; (_) -> "UNSUPPORTED EXPANSION type" end ). %% ERROR CODES -define(NO_ERROR, 16#0). -define(PROTOCOL_ERROR, 16#1). -define(INTERNAL_ERROR, 16#2). -define(FLOW_CONTROL_ERROR, 16#3). -define(SETTINGS_TIMEOUT, 16#4). -define(STREAM_CLOSED, 16#5). -define(FRAME_SIZE_ERROR, 16#6). -define(REFUSED_STREAM, 16#7). -define(CANCEL, 16#8). -define(COMPRESSION_ERROR, 16#9). -define(CONNECT_ERROR, 16#a). -define(ENHANCE_YOUR_CALM, 16#b). -define(INADEQUATE_SECURITY,16#c). -define(HTTP_1_1_REQUIRED, 16#d). -type error_code() :: ?NO_ERROR | ?PROTOCOL_ERROR | ?INTERNAL_ERROR | ?FLOW_CONTROL_ERROR | ?SETTINGS_TIMEOUT | ?STREAM_CLOSED | ?FRAME_SIZE_ERROR | ?REFUSED_STREAM | ?CANCEL | ?COMPRESSION_ERROR | ?CONNECT_ERROR | ?ENHANCE_YOUR_CALM | ?INADEQUATE_SECURITY | ?HTTP_1_1_REQUIRED. %% FLAGS -define(FLAG_ACK, 16#1 ). -define(FLAG_END_STREAM, 16#1 ). -define(FLAG_END_HEADERS, 16#4 ). -define(FLAG_PADDED, 16#8 ). -define(FLAG_PRIORITY, 16#20). -type flag() :: ?FLAG_ACK | ?FLAG_END_STREAM | ?FLAG_END_HEADERS | ?FLAG_PADDED | ?FLAG_PRIORITY. %% These are macros because they're used in guards a lot -define(IS_FLAG(Flags, Flag), Flags band Flag =:= Flag). -define(NOT_FLAG(Flags, Flag), Flags band Flag =/= Flag). -type stream_id() :: non_neg_integer(). -record(frame_header, { length :: non_neg_integer() | undefined, type :: frame_type() | undefined, flags = 0 :: non_neg_integer(), stream_id :: stream_id() }). -type transport() :: gen_tcp | ssl. -type socket() :: {gen_tcp, inet:socket()|undefined} | {ssl, ssl:sslsocket()|undefined}. -define(PREFACE, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"). -define(DEFAULT_INITIAL_WINDOW_SIZE, 65535). %% Settings are too big to be part of the data type refactor. We'll %% get to it next -record(settings, {header_table_size = 4096, enable_push = 1, max_concurrent_streams = unlimited, initial_window_size = 65535, max_frame_size = 16384, max_header_list_size = unlimited}). -type settings() :: #settings{}. -define(SETTINGS_HEADER_TABLE_SIZE, <<16#1>>). -define(SETTINGS_ENABLE_PUSH, <<16#2>>). -define(SETTINGS_MAX_CONCURRENT_STREAMS, <<16#3>>). -define(SETTINGS_INITIAL_WINDOW_SIZE, <<16#4>>). -define(SETTINGS_MAX_FRAME_SIZE, <<16#5>>). -define(SETTINGS_MAX_HEADER_LIST_SIZE, <<16#6>>). -define(SETTING_NAMES, [?SETTINGS_HEADER_TABLE_SIZE, ?SETTINGS_ENABLE_PUSH, ?SETTINGS_MAX_CONCURRENT_STREAMS, ?SETTINGS_INITIAL_WINDOW_SIZE, ?SETTINGS_MAX_FRAME_SIZE, ?SETTINGS_MAX_HEADER_LIST_SIZE]). -type setting_name() :: binary(). -type settings_property() :: {setting_name(), any()}. -type settings_proplist() :: [settings_property()]. ================================================ FILE: priv/index.html ================================================ Chatterbox Chatterbox ================================================ FILE: rebar.config ================================================ %% -*- mode: erlang -*- %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ts=4 sw=4 ft=erlang et {erl_opts, [ warnings_as_errors, debug_info ]}. {deps, [{hpack, {pkg, hpack_erl}}]}. {cover_enabled, true}. {ct_opts, [{verbose,true}]}. {dialyzer, [unknown]}. {profiles, [ {test, [ {erl_opts, [ {i,["include"]}, nowarn_export_all ]}, {deps, [{ranch, "1.2.1"}]} ] }] }. {relx, [ {release,{chatterbox,"0.0.1"}, [chatterbox]}, {sys_config, "./config/sys.config"}, %%{vm_args, "./config/vm.args"}, {dev_mode, true}, {include_erts, false}, {extended_start_script, true}, {overlay,[ {template,"config/sys.config","sys.config"}, {copy,"config/localhost.crt","."}, {copy,"config/localhost.key","."} ]} ]}. ================================================ FILE: rebar.config.script ================================================ case erlang:function_exported(rebar3, main, 1) of true -> % rebar3 CONFIG; false -> % rebar 2.x or older %% Rebuild deps, possibly including those that have been moved to %% profiles [{deps, [ {hpack, ".*", {git, "https://github.com/joedevivo/hpack.git", {tag, "0.2.3"}}} ]} | lists:keydelete(deps, 1, CONFIG)] end. ================================================ FILE: src/chatterbox.app.src ================================================ %% -*- mode: erlang -*- {application, chatterbox, [ {description, "chatterbox library for http2"}, {vsn, git}, {registered, []}, {applications, [ kernel, stdlib, crypto, public_key, ssl, hpack ]}, {env, [ {port, 80}, {concurrent_acceptors, 20}, %% These are the defaults for ALL HTTP/2 clients. You can change %% any of them that you want to, but these will be the ones used %% by default if you don't. %% I wanted them to be one proplist, but I realized that will be %% annoying for people that only want to change one default %% setting. {client_header_table_size, 4096}, {client_enable_push, 1}, {client_max_concurrent_streams, unlimited}, {client_initial_window_size, 65535}, {client_max_frame_size, 16384}, {client_max_header_list_size, unlimited}, {client_flow_control, auto}, %% Same for server {server_header_table_size, 4096}, {server_enable_push, 1}, {server_max_concurrent_streams, unlimited}, {server_initial_window_size, 65535}, {server_max_frame_size, 16384}, {server_max_header_list_size, unlimited}, {server_flow_control, auto} ]}, {maintainers, ["Joe DeVivo"]}, {licenses, ["MIT"]}, {links, [{"Github", "https://github.com/joedevivo/chatterbox"}]} ]}. %% vim: set filetype=erlang tabstop=2 ================================================ FILE: src/chatterbox.erl ================================================ -module(chatterbox). -include("http2.hrl"). -export([ start/0, settings/0, settings/1, settings/2 ]). start() -> chatterbox_sup:start_link(). settings() -> settings(server). settings(server, Settings=#{}) -> HTS = maps:get(server_header_table_size, Settings, 4096), EP = maps:get(server_enable_push, Settings, 1), MCS = maps:get(server_max_concurrent_streams, Settings, unlimited), IWS = maps:get(server_initial_window_size, Settings, 65535), MFS = maps:get(server_max_frame_size, Settings, 16384), MHLS = maps:get(server_max_header_list_size, Settings, unlimited), #settings{ header_table_size=HTS, enable_push=EP, max_concurrent_streams=MCS, initial_window_size=IWS, max_frame_size=MFS, max_header_list_size=MHLS }. settings(server) -> HTS = application:get_env(?MODULE, server_header_table_size, 4096), EP = application:get_env(?MODULE, server_enable_push, 1), MCS = application:get_env(?MODULE, server_max_concurrent_streams, unlimited), IWS = application:get_env(?MODULE, server_initial_window_size, 65535), MFS = application:get_env(?MODULE, server_max_frame_size, 16384), MHLS = application:get_env(?MODULE, server_max_header_list_size, unlimited), #settings{ header_table_size=HTS, enable_push=EP, max_concurrent_streams=MCS, initial_window_size=IWS, max_frame_size=MFS, max_header_list_size=MHLS }; settings(client) -> HTS = application:get_env(?MODULE, client_header_table_size, 4096), EP = application:get_env(?MODULE, client_enable_push, 1), MCS = application:get_env(?MODULE, client_max_concurrent_streams, unlimited), IWS = application:get_env(?MODULE, client_initial_window_size, 65535), MFS = application:get_env(?MODULE, client_max_frame_size, 16384), MHLS = application:get_env(?MODULE, client_max_header_list_size, unlimited), #settings{ header_table_size=HTS, enable_push=EP, max_concurrent_streams=MCS, initial_window_size=IWS, max_frame_size=MFS, max_header_list_size=MHLS }. ================================================ FILE: src/chatterbox_ranch_protocol.erl ================================================ -module(chatterbox_ranch_protocol). %% While it implements the behaviour, uncommenting the line below %% would fail to compile unless I make ranch a dependency of %% chatterbox, which I don't plan on %%-behaviour(ranch_protocol). -export([ start_link/4, init/4 ]). start_link(Ref, Socket, Transport, Opts) -> Pid = proc_lib:spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), {ok, Pid}. init(Ref, Socket, T, Opts) -> ok = ranch:accept_ack(Ref), Http2Settings = proplists:get_value(http2_settings, Opts, chatterbox:settings(server)), h2_connection:become({transport(T), Socket}, Http2Settings). transport(ranch_ssl) -> ssl; transport(ranch_tcp) -> gen_tcp; transport(tcp) -> gen_tcp; transport(gen_tcp) -> gen_tcp; transport(ssl) -> ssl; transport(_Other) -> error(unknown_protocol). ================================================ FILE: src/chatterbox_static_content_handler.erl ================================================ -module(chatterbox_static_content_handler). -include("http2.hrl"). -export([ spawn_handle/4, handle/4 ]). -spec spawn_handle( pid(), stream_id(), %% Stream Id hpack:headers(), %% Decoded Request Headers iodata() %% Request Body ) -> pid(). spawn_handle(Pid, StreamId, Headers, ReqBody) -> Handler = fun() -> handle(Pid, StreamId, Headers, ReqBody) end, spawn_link(Handler). -spec handle( pid(), stream_id(), hpack:headers(), iodata() ) -> ok. handle(ConnPid, StreamId, Headers, _ReqBody) -> Path = binary_to_list(proplists:get_value(<<":path">>, Headers)), %% QueryString Hack? Path2 = case string:chr(Path, $?) of 0 -> Path; X -> string:substr(Path, 1, X-1) end, %% Dot Hack Path3 = case Path2 of [$.|T] -> T; Other -> Other end, Path4 = case Path3 of [$/|T2] -> [$/|T2]; Other2 -> [$/|Other2] end, %% TODO: Should have a better way of extracting root_dir (i.e. not on every request) StaticHandlerSettings = application:get_env(chatterbox, ?MODULE, []), RootDir = proplists:get_value(root_dir, StaticHandlerSettings, code:priv_dir(chatterbox)), %% TODO: Logic about "/" vs "index.html", "index.htm", etc... %% Directory browsing? File = RootDir ++ Path4, case {filelib:is_file(File), filelib:is_dir(File)} of {_, true} -> ResponseHeaders = [ {<<":status">>,<<"403">>} ], h2_connection:send_headers(ConnPid, StreamId, ResponseHeaders), h2_connection:send_body(ConnPid, StreamId, <<"No soup for you!">>), ok; {true, false} -> Ext = filename:extension(File), MimeType = case Ext of ".js" -> <<"text/javascript">>; ".html" -> <<"text/html">>; ".css" -> <<"text/css">>; ".scss" -> <<"text/css">>; ".woff" -> <<"application/font-woff">>; ".ttf" -> <<"application/font-snft">>; _ -> <<"unknown">> end, {ok, Data} = file:read_file(File), ResponseHeaders = [ {<<":status">>, <<"200">>}, {<<"content-type">>, MimeType} ], h2_connection:send_headers(ConnPid, StreamId, ResponseHeaders), case {MimeType, h2_connection:is_push(ConnPid)} of {<<"text/html">>, true} -> %% Search Data for resources to push {ok, RE} = re:compile(" [dot_hack(lists:last(M)) || M <- Matches]; _ -> [] end, NewStreams = lists:foldl(fun(R, Acc) -> NewStreamId = h2_connection:new_stream(ConnPid), PHeaders = generate_push_promise_headers(Headers, <<$/,R/binary>> ), h2_connection:send_promise(ConnPid, StreamId, NewStreamId, PHeaders), [{NewStreamId, PHeaders}|Acc] end, [], Resources ), [spawn_handle(ConnPid, NewStreamId, PHeaders, <<>>) || {NewStreamId, PHeaders} <- NewStreams], ok; _ -> ok end, h2_connection:send_body(ConnPid, StreamId, Data), ok; {false, false} -> ResponseHeaders = [ {<<":status">>,<<"404">>} ], h2_connection:send_headers(ConnPid, StreamId, ResponseHeaders), h2_connection:send_body(ConnPid, StreamId, <<"No soup for you!">>), ok end, ok. -spec generate_push_promise_headers(hpack:headers(), binary()) -> hpack:headers(). generate_push_promise_headers(Request, Path) -> [ {<<":path">>, Path},{<<":method">>, <<"GET">>}| lists:filter(fun({<<":authority">>,_}) -> true; ({<<":scheme">>, _}) -> true; (_) -> false end, Request) ]. -spec dot_hack(binary()) -> binary(). dot_hack(<<$.,Bin/binary>>) -> Bin; dot_hack(Bin) -> Bin. ================================================ FILE: src/chatterbox_static_stream.erl ================================================ -module(chatterbox_static_stream). -include("http2.hrl"). -behaviour(h2_stream). -export([ init/3, on_receive_request_headers/2, on_send_push_promise/2, on_receive_request_data/2, on_request_end_stream/1 ]). -record(cb_static, { req_headers=[], connection_pid :: pid(), stream_id :: stream_id() }). init(ConnPid, StreamId, _) -> %% You need to pull settings here from application:env or something {ok, #cb_static{connection_pid=ConnPid, stream_id=StreamId}}. on_receive_request_headers(Headers, State) -> {ok, State#cb_static{req_headers=Headers}}. on_send_push_promise(Headers, State) -> {ok, State#cb_static{req_headers=Headers}}. on_receive_request_data(_Bin, State)-> {ok, State}. on_request_end_stream(State=#cb_static{connection_pid=ConnPid, stream_id=StreamId}) -> Headers = State#cb_static.req_headers, Method = proplists:get_value(<<":method">>, Headers), Path = binary_to_list(proplists:get_value(<<":path">>, Headers)), %% QueryString Hack? Path2 = case string:chr(Path, $?) of 0 -> Path; X -> string:substr(Path, 1, X-1) end, %% Dot Hack Path3 = case Path2 of [$.|T] -> T; Other -> Other end, Path4 = case Path3 of [$/|T2] -> [$/|T2]; Other2 -> [$/|Other2] end, %% TODO: Should have a better way of extracting root_dir (i.e. not on every request) StaticHandlerSettings = application:get_env(chatterbox, ?MODULE, []), RootDir = proplists:get_value(root_dir, StaticHandlerSettings, code:priv_dir(chatterbox)), %% TODO: Logic about "/" vs "index.html", "index.htm", etc... %% Directory browsing? File = RootDir ++ Path4, {HeadersToSend, BodyToSend} = case {filelib:is_file(File), filelib:is_dir(File)} of {_, true} -> ResponseHeaders = [ {<<":status">>,<<"403">>} ], {ResponseHeaders, <<"No soup for you!">>}; {true, false} -> Ext = filename:extension(File), MimeType = case Ext of ".js" -> <<"text/javascript">>; ".html" -> <<"text/html">>; ".css" -> <<"text/css">>; ".scss" -> <<"text/css">>; ".woff" -> <<"application/font-woff">>; ".ttf" -> <<"application/font-snft">>; _ -> <<"unknown">> end, {ok, Data} = file:read_file(File), ResponseHeaders = [ {<<":status">>, <<"200">>}, {<<"content-type">>, MimeType} ], case {MimeType, h2_connection:is_push(ConnPid)} of {<<"text/html">>, true} -> %% Search Data for resources to push {ok, RE} = re:compile(" [dot_hack(lists:last(M)) || M <- Matches]; _ -> [] end, lists:foldl( fun(R, Acc) -> NewStreamId = h2_connection:new_stream(ConnPid), PHeaders = generate_push_promise_headers(Headers, <<$/,R/binary>> ), h2_connection:send_promise(ConnPid, StreamId, NewStreamId, PHeaders), [{NewStreamId, PHeaders}|Acc] end, [], Resources ), ok; _ -> ok end, %% For each chunk of data: %% 1. Ask the connection if it's got enough bytes in the %% send window. %% maybe just send the frame header? %% If it doesn't, we need to put this frame in a place %% that will get looked at when our connection window size %% increases. %% If it does, we still need to try and check stream level %% flow control. {ResponseHeaders, Data}; {false, false} -> ResponseHeaders = [ {<<":status">>,<<"404">>} ], {ResponseHeaders, <<"No soup for you!">>} end, case {Method, HeadersToSend, BodyToSend} of {<<"HEAD">>, _, _} -> h2_connection:send_headers(ConnPid, StreamId, HeadersToSend, [{send_end_stream, true}]); %%{<<"GET">>, _, _} -> _ -> h2_connection:send_headers(ConnPid, StreamId, HeadersToSend), h2_connection:send_body(ConnPid, StreamId, BodyToSend) end, {ok, State}. %% Internal -spec generate_push_promise_headers(hpack:headers(), binary()) -> hpack:headers(). generate_push_promise_headers(Request, Path) -> [ {<<":path">>, Path},{<<":method">>, <<"GET">>}| lists:filter(fun({<<":authority">>,_}) -> true; ({<<":scheme">>, _}) -> true; (_) -> false end, Request) ]. -spec dot_hack(binary()) -> binary(). dot_hack(<<$.,Bin/binary>>) -> Bin; dot_hack(Bin) -> Bin. ================================================ FILE: src/chatterbox_sup.erl ================================================ -module(chatterbox_sup). -behaviour(supervisor). -export([ init/1, start_link/0, start_socket/0 ]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> {ok, Port} = application:get_env(chatterbox, port), Options = [ binary, {reuseaddr, true}, {packet, raw}, {backlog, 1024}, {active, false} ], {ok, SSLEnabled} = application:get_env(chatterbox, ssl), {Transport, SSLOptions} = case SSLEnabled of true -> {ok, SSLOpts} = application:get_env(chatterbox, ssl_options), {ssl, SSLOpts}; false -> {gen_tcp, []} end, Http2Settings = chatterbox:settings(server), spawn_link(fun empty_listeners/0), {ok, ListenSocket} = gen_tcp:listen(Port, Options), Restart = {simple_one_for_one, 60, 3600}, Children = [{socket, {h2_connection, start_server_link, [{Transport, ListenSocket}, SSLOptions, Http2Settings]}, temporary, 1000, worker, [h2_connection]}], {ok, {Restart, Children}}. start_socket() -> supervisor:start_child(?MODULE, []). empty_listeners() -> {ok, ConcurrentAcceptors} = application:get_env(chatterbox, concurrent_acceptors), [ start_socket() || _ <- lists:seq(1,ConcurrentAcceptors)]. ================================================ FILE: src/h2_client.erl ================================================ -module(h2_client). -include("http2.hrl"). %% Today's the day! We need to turn this gen_server into a gen_statem %% which means this is going to look a lot like the "opposite of %% http2_connection". This is the way to take advantage of the %% abstraction of http2_socket. Still, the client API is way more %% important than the server API so we're going to have to work %% backwards from that API to get it right %% {request, Headers, Data} %% {request, [Frames]} %% A frame that is too big should know how to break itself up. %% That might mean into Continutations %% API -export([ start_link/0, start_link/2, start_link/3, start_link/4, start/4, start_ssl_upgrade_link/4, stop/1, send_request/3, send_ping/1, sync_request/3, get_response/2 ]). %% this is gonna get weird. start_link/* is going to call %% http2_socket's start_link function, which will handle opening the %% socket and sending the HTTP/2 Preface over the wire. Once that's %% working, it's going to call gen_statem:start_link(http2c, [SocketPid], %% []) which will then use our init/1 callback. You can't actually %% start this gen_statem with this API. That's intentional, it will %% eventually get started if things go right. If they don't, you %% wouldn't want one of these anyway. %% No arg version uses a bunch of defaults, which will have to be %% reviewed if/when this module is refactored into it's own library. -spec start_link() -> {ok, pid()} | ignore | {error, any()}. start_link() -> %% Defaults for local server, not sure these defaults are %% "sensible" if this module is removed from this repo {ok, Port} = application:get_env(chatterbox, port), {ok, SSLEnabled} = application:get_env(chatterbox, ssl), case SSLEnabled of true -> start_link(https, "localhost", Port); false -> start_link(http, "localhost", Port) end. %% Start up with scheme and hostname only, default to standard ports %% and options -spec start_link(http | https, string()) -> {ok, pid()} | ignore | {error, term()}. start_link(http, Host) -> start_link(http, Host, 80); start_link(https,Host) -> start_link(https, Host, 443). %% Start up with a specific port, or specific SSL options, but not %% both. -spec start_link(http | https, string(), non_neg_integer() | [ssl:ssl_option()]) -> {ok, pid()} | ignore | {error, term()}. start_link(http, Host, Port) when is_integer(Port) -> start_link(http, Host, Port, []); start_link(https, Host, Port) when is_integer(Port) -> {ok, SSLOptions} = application:get_env(chatterbox, ssl_options), DefaultSSLOptions = [ {client_preferred_next_protocols, {client, [<<"h2">>]}}| SSLOptions ], start_link(https, Host, Port, DefaultSSLOptions); start_link(https, Host, SSLOptions) when is_list(SSLOptions) -> start_link(https, Host, 443, SSLOptions). %% Here's your all access client starter. MAXIMUM TUNABLES! Scheme, %% Hostname, Port and SSLOptions. All of the start_link/* calls come %% through here eventually, so this is where we turn 'http' and %% 'https' into 'gen_tcp' and 'ssl' for erlang module function calls %% later. -spec start_link(http | https, string(), non_neg_integer(), [ssl:ssl_option()]) -> {ok, pid()} | ignore | {error, term()}. start_link(Transport, Host, Port, SSLOptions) -> NewT = case Transport of http -> gen_tcp; https -> ssl end, h2_connection:start_client_link(NewT, Host, Port, SSLOptions, chatterbox:settings(client)). -spec start(http | https, string(), non_neg_integer(), [ssl:ssl_option()]) -> {ok, pid()} | ignore | {error, term()}. start(Transport, Host, Port, SSLOptions) -> NewT = case Transport of http -> gen_tcp; https -> ssl end, h2_connection:start_client(NewT, Host, Port, SSLOptions, chatterbox:settings(client)). start_ssl_upgrade_link(Host, Port, InitialMessage, SSLOptions) -> h2_connection:start_ssl_upgrade_link(Host, Port, InitialMessage, SSLOptions, chatterbox:settings(client)). -spec stop(pid()) -> ok. stop(Pid) -> h2_connection:stop(Pid). -spec sync_request(CliPid, Headers, Body) -> Result when CliPid :: pid(), Headers :: hpack:headers(), Body :: binary(), Result :: {ok, {hpack:headers(), iodata()}} | {error, error_code() | timeout}. sync_request(CliPid, Headers, Body) -> case send_request(CliPid, Headers, Body) of {ok, StreamId} -> receive {'END_STREAM', StreamId} -> h2_connection:get_response(CliPid, StreamId) after 5000 -> {error, timeout} end; Error -> Error end. -spec send_request(CliPid, Headers, Body) -> Result when CliPid :: pid(), Headers :: hpack:headers(), Body :: binary(), Result :: {ok, stream_id()} | {error, error_code()}. send_request(CliPid, Headers, Body) -> case h2_connection:new_stream(CliPid) of {error, _Code} = Err -> Err; StreamId -> h2_connection:send_headers(CliPid, StreamId, Headers), h2_connection:send_body(CliPid, StreamId, Body), {ok, StreamId} end. send_ping(CliPid) -> h2_connection:send_ping(CliPid). -spec get_response(pid(), stream_id()) -> {ok, {hpack:header(), iodata()}} | not_ready | {error, term()}. get_response(CliPid, StreamId) -> h2_connection:get_response(CliPid, StreamId). ================================================ FILE: src/h2_connection.erl ================================================ -module(h2_connection). -include("http2.hrl"). -behaviour(gen_statem). %% Start/Stop API -export([ start_client/2, start_client/5, start_client_link/2, start_client_link/5, start_ssl_upgrade_link/5, start_server_link/3, become/1, become/2, become/3, stop/1 ]). %% HTTP Operations -export([ send_headers/3, send_headers/4, send_trailers/3, send_trailers/4, send_body/3, send_body/4, send_request/3, send_ping/1, is_push/1, new_stream/1, new_stream/2, send_promise/4, get_response/2, get_peer/1, get_peercert/1, get_streams/1, send_window_update/2, update_settings/2, send_frame/2 ]). %% gen_statem callbacks -export( [ init/1, callback_mode/0, code_change/4, terminate/3 ]). %% gen_statem states -export([ listen/3, handshake/3, connected/3, continuation/3, closing/3 ]). -export([ go_away/2 ]). -record(h2_listening_state, { ssl_options :: [ssl:ssl_option()], listen_socket :: ssl:sslsocket() | inet:socket(), transport :: gen_tcp | ssl, listen_ref :: non_neg_integer(), acceptor_callback = fun chatterbox_sup:start_socket/0 :: fun(), server_settings = #settings{} :: settings() }). -record(continuation_state, { stream_id :: stream_id(), promised_id = undefined :: undefined | stream_id(), frames = queue:new() :: queue:queue(h2_frame:frame()), type :: headers | push_promise | trailers, end_stream = false :: boolean(), end_headers = false :: boolean() }). -record(connection, { type = undefined :: client | server | undefined, ssl_options = [], listen_ref :: non_neg_integer() | undefined, socket = undefined :: sock:socket(), peer_settings = #settings{} :: settings(), self_settings = #settings{} :: settings(), send_window_size = ?DEFAULT_INITIAL_WINDOW_SIZE :: integer(), recv_window_size = ?DEFAULT_INITIAL_WINDOW_SIZE :: integer(), decode_context = hpack:new_context() :: hpack:context(), encode_context = hpack:new_context() :: hpack:context(), settings_sent = queue:new() :: queue:queue(), next_available_stream_id = 2 :: stream_id(), streams :: h2_stream_set:stream_set(), stream_callback_mod = application:get_env(chatterbox, stream_callback_mod, chatterbox_static_stream) :: module(), stream_callback_opts = application:get_env(chatterbox, stream_callback_opts, []) :: list(), buffer = empty :: empty | {binary, binary()} | {frame, h2_frame:header(), binary()}, continuation = undefined :: undefined | #continuation_state{}, flow_control = auto :: auto | manual, pings = #{} :: #{binary() => {pid(), non_neg_integer()}} }). -type connection() :: #connection{}. -type send_option() :: {send_end_stream, boolean()}. -type send_opts() :: [send_option()]. -export_type([send_option/0, send_opts/0]). -ifdef(OTP_RELEASE). -define(ssl_accept(ClientSocket, SSLOptions), ssl:handshake(ClientSocket, SSLOptions)). -else. -define(ssl_accept(ClientSocket, SSLOptions), ssl:ssl_accept(ClientSocket, SSLOptions)). -endif. -spec start_client_link(gen_tcp | ssl, inet:ip_address() | inet:hostname(), inet:port_number(), [ssl:ssl_option()], settings() ) -> {ok, pid()} | ignore | {error, term()}. start_client_link(Transport, Host, Port, SSLOptions, Http2Settings) -> gen_statem:start_link(?MODULE, {client, Transport, Host, Port, SSLOptions, Http2Settings}, []). -spec start_client(gen_tcp | ssl, inet:ip_address() | inet:hostname(), inet:port_number(), [ssl:ssl_option()], settings() ) -> {ok, pid()} | ignore | {error, term()}. start_client(Transport, Host, Port, SSLOptions, Http2Settings) -> gen_statem:start(?MODULE, {client, Transport, Host, Port, SSLOptions, Http2Settings}, []). -spec start_client_link(socket(), settings() ) -> {ok, pid()} | ignore | {error, term()}. start_client_link({Transport, Socket}, Http2Settings) -> gen_statem:start_link(?MODULE, {client, {Transport, Socket}, Http2Settings}, []). -spec start_client(socket(), settings() ) -> {ok, pid()} | ignore | {error, term()}. start_client({Transport, Socket}, Http2Settings) -> gen_statem:start(?MODULE, {client, {Transport, Socket}, Http2Settings}, []). -spec start_ssl_upgrade_link(inet:ip_address() | inet:hostname(), inet:port_number(), binary(), [ssl:ssl_option()], settings() ) -> {ok, pid()} | ignore | {error, term()}. start_ssl_upgrade_link(Host, Port, InitialMessage, SSLOptions, Http2Settings) -> gen_statem:start_link(?MODULE, {client_ssl_upgrade, Host, Port, InitialMessage, SSLOptions, Http2Settings}, []). -spec start_server_link(socket(), [ssl:ssl_option()], settings()) -> {ok, pid()} | ignore | {error, term()}. start_server_link({Transport, ListenSocket}, SSLOptions, Http2Settings) -> gen_statem:start_link(?MODULE, {server, {Transport, ListenSocket}, SSLOptions, Http2Settings}, []). -spec become(socket()) -> no_return(). become(Socket) -> become(Socket, chatterbox:settings(server)). -spec become(socket(), settings()) -> no_return(). become(Socket, Http2Settings) -> become(Socket, Http2Settings, #{}). -spec become(socket(), settings(), maps:map()) -> no_return(). become({Transport, Socket}, Http2Settings, ConnectionSettings) -> ok = sock:setopts({Transport, Socket}, [{packet, raw}, binary]), case start_http2_server(Http2Settings, #connection{ stream_callback_mod = maps:get(stream_callback_mod, ConnectionSettings, application:get_env(chatterbox, stream_callback_mod, chatterbox_static_stream)), stream_callback_opts = maps:get(stream_callback_opts, ConnectionSettings, application:get_env(chatterbox, stream_callback_opts, [])), streams = h2_stream_set:new(server), socket = {Transport, Socket} }) of {_, handshake, NewState} -> gen_statem:enter_loop(?MODULE, [], handshake, NewState); {_, closing, _NewState} -> sock:close({Transport, Socket}), exit(invalid_preface) end. %% Init callback init({client, Transport, Host, Port, SSLOptions, Http2Settings}) -> case Transport:connect(Host, Port, client_options(Transport, SSLOptions)) of {ok, Socket} -> init({client, {Transport, Socket}, Http2Settings}); {error, Reason} -> {stop, Reason} end; init({client, {Transport, Socket}, Http2Settings}) -> ok = sock:setopts({Transport, Socket}, [{packet, raw}, binary, {active, once}]), Transport:send(Socket, <>), InitialState = #connection{ type = client, streams = h2_stream_set:new(client), socket = {Transport, Socket}, next_available_stream_id=1, flow_control=application:get_env(chatterbox, client_flow_control, auto) }, {ok, handshake, send_settings(Http2Settings, InitialState), 4500}; init({client_ssl_upgrade, Host, Port, InitialMessage, SSLOptions, Http2Settings}) -> case gen_tcp:connect(Host, Port, [{active, false}]) of {ok, TCP} -> gen_tcp:send(TCP, InitialMessage), case ssl:connect(TCP, client_options(ssl, SSLOptions)) of {ok, Socket} -> init({client, {ssl, Socket}, Http2Settings}); {error, Reason} -> {stop, Reason} end; {error, Reason} -> {stop, Reason} end; init({server, {Transport, ListenSocket}, SSLOptions, Http2Settings}) -> %% prim_inet:async_accept is dope. It says just hang out here and %% wait for a message that a client has connected. That message %% looks like: %% {inet_async, ListenSocket, Ref, {ok, ClientSocket}} case prim_inet:async_accept(ListenSocket, -1) of {ok, Ref} -> {ok, listen, #h2_listening_state{ ssl_options = SSLOptions, listen_socket = ListenSocket, listen_ref = Ref, transport = Transport, server_settings = Http2Settings }}; %% No timeout here, it's just a listener {error, Reason} -> {stop, Reason} end. callback_mode() -> state_functions. send_frame(Pid, Bin) when is_binary(Bin); is_list(Bin) -> gen_statem:cast(Pid, {send_bin, Bin}); send_frame(Pid, Frame) -> gen_statem:cast(Pid, {send_frame, Frame}). -spec send_headers(pid(), stream_id(), hpack:headers()) -> ok. send_headers(Pid, StreamId, Headers) -> gen_statem:cast(Pid, {send_headers, StreamId, Headers, []}), ok. -spec send_headers(pid(), stream_id(), hpack:headers(), send_opts()) -> ok. send_headers(Pid, StreamId, Headers, Opts) -> gen_statem:cast(Pid, {send_headers, StreamId, Headers, Opts}), ok. -spec send_trailers(pid(), stream_id(), hpack:headers()) -> ok. send_trailers(Pid, StreamId, Trailers) -> gen_statem:cast(Pid, {send_trailers, StreamId, Trailers, []}), ok. -spec send_trailers(pid(), stream_id(), hpack:headers(), send_opts()) -> ok. send_trailers(Pid, StreamId, Trailers, Opts) -> gen_statem:cast(Pid, {send_trailers, StreamId, Trailers, Opts}), ok. -spec send_body(pid(), stream_id(), binary()) -> ok. send_body(Pid, StreamId, Body) -> gen_statem:cast(Pid, {send_body, StreamId, Body, []}), ok. -spec send_body(pid(), stream_id(), binary(), send_opts()) -> ok. send_body(Pid, StreamId, Body, Opts) -> gen_statem:cast(Pid, {send_body, StreamId, Body, Opts}), ok. -spec send_request(pid(), hpack:headers(), binary()) -> ok. send_request(Pid, Headers, Body) -> gen_statem:call(Pid, {send_request, self(), Headers, Body}, infinity), ok. -spec send_ping(pid()) -> ok. send_ping(Pid) -> gen_statem:call(Pid, {send_ping, self()}, infinity), ok. -spec get_peer(pid()) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, term()}. get_peer(Pid) -> gen_statem:call(Pid, get_peer). -spec get_peercert(pid()) -> {ok, binary()} | {error, term()}. get_peercert(Pid) -> gen_statem:call(Pid, get_peercert). -spec is_push(pid()) -> boolean(). is_push(Pid) -> gen_statem:call(Pid, is_push). -spec new_stream(pid()) -> stream_id() | {error, error_code()}. new_stream(Pid) -> new_stream(Pid, self()). -spec new_stream(pid(), pid()) -> stream_id() | {error, error_code()}. new_stream(Pid, NotifyPid) -> gen_statem:call(Pid, {new_stream, NotifyPid}). -spec send_promise(pid(), stream_id(), stream_id(), hpack:headers()) -> ok. send_promise(Pid, StreamId, NewStreamId, Headers) -> gen_statem:cast(Pid, {send_promise, StreamId, NewStreamId, Headers}), ok. -spec get_response(pid(), stream_id()) -> {ok, {hpack:headers(), iodata()}} | not_ready. get_response(Pid, StreamId) -> gen_statem:call(Pid, {get_response, StreamId}). -spec get_streams(pid()) -> h2_stream_set:stream_set(). get_streams(Pid) -> gen_statem:call(Pid, streams). -spec send_window_update(pid(), non_neg_integer()) -> ok. send_window_update(Pid, Size) -> gen_statem:cast(Pid, {send_window_update, Size}). -spec update_settings(pid(), h2_frame_settings:payload()) -> ok. update_settings(Pid, Payload) -> gen_statem:cast(Pid, {update_settings, Payload}). -spec stop(pid()) -> ok. stop(Pid) -> gen_statem:cast(Pid, stop). %% The listen state only exists to wait around for new prim_inet %% connections listen(info, {inet_async, ListenSocket, Ref, {ok, ClientSocket}}, #h2_listening_state{ listen_socket = ListenSocket, listen_ref = Ref, transport = Transport, ssl_options = SSLOptions, acceptor_callback = AcceptorCallback, server_settings = Http2Settings }) -> %If anything crashes in here, at least there's another acceptor ready AcceptorCallback(), inet_db:register_socket(ClientSocket, inet_tcp), Socket = case Transport of gen_tcp -> ClientSocket; ssl -> {ok, AcceptSocket} = ?ssl_accept(ClientSocket, SSLOptions), {ok, <<"h2">>} = ssl:negotiated_protocol(AcceptSocket), AcceptSocket end, start_http2_server( Http2Settings, #connection{ streams = h2_stream_set:new(server), socket={Transport, Socket} }); listen(timeout, _, State) -> go_away(?PROTOCOL_ERROR, State); listen(Type, Msg, State) -> handle_event(Type, Msg, State). -spec handshake(gen_statem:event_type(), {frame, h2_frame:frame()} | term(), connection()) -> {next_state, handshake|connected|closing, connection()}. handshake(timeout, _, State) -> go_away(?PROTOCOL_ERROR, State); handshake(_, {frame, {FH, _Payload}=Frame}, State) -> %% The first frame should be the client settings as per %% RFC-7540#3.5 case FH#frame_header.type of ?SETTINGS -> route_frame(Frame, State); _ -> go_away(?PROTOCOL_ERROR, State) end; handshake(Type, Msg, State) -> handle_event(Type, Msg, State). connected(_, {frame, Frame}, #connection{}=Conn ) -> route_frame(Frame, Conn); connected(Type, Msg, State) -> handle_event(Type, Msg, State). %% The continuation state in entered after receiving a HEADERS frame %% with no ?END_HEADERS flag set, we're locked waiting for contiunation %% frames on the same stream to preserve the decoding context state continuation(_, {frame, {#frame_header{ stream_id=StreamId, type=?CONTINUATION }, _}=Frame}, #connection{ continuation = #continuation_state{ stream_id = StreamId } }=Conn) -> route_frame(Frame, Conn); continuation(Type, Msg, State) -> handle_event(Type, Msg, State). %% The closing state should deal with frames on the wire still, I %% think. But we should just close it up now. closing(_, _Message, #connection{ socket=Socket }=Conn) -> sock:close(Socket), {stop, normal, Conn}; closing(Type, Msg, State) -> handle_event(Type, Msg, State). %% route_frame's job needs to be "now that we've read a frame off the %% wire, do connection based things to it and/or forward it to the %% http2 stream processor (h2_stream:recv_frame) -spec route_frame( h2_frame:frame() | {error, term()}, connection()) -> {next_state, connected | continuation | closing , connection()}. %% Bad Length of frame, exceedes maximum allowed size route_frame({#frame_header{length=L}, _}, #connection{ self_settings=#settings{max_frame_size=MFS} }=Conn) when L > MFS -> go_away(?FRAME_SIZE_ERROR, Conn); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Connection Level Frames %% %% Here we'll handle anything that belongs on stream 0. %% SETTINGS, finally something that's ok on stream 0 %% This is the non-ACK case, where settings have actually arrived route_frame({H, Payload}, #connection{ peer_settings=PS=#settings{ initial_window_size=OldIWS, header_table_size=HTS }, streams=Streams, encode_context=EncodeContext }=Conn) when H#frame_header.type == ?SETTINGS, ?NOT_FLAG((H#frame_header.flags), ?FLAG_ACK) -> %% Need a way of processing settings so I know which ones came in %% on this one payload. case h2_frame_settings:validate(Payload) of ok -> {settings, PList} = Payload, Delta = case proplists:get_value(?SETTINGS_INITIAL_WINDOW_SIZE, PList) of undefined -> 0; NewIWS -> NewIWS - OldIWS end, NewPeerSettings = h2_frame_settings:overlay(PS, Payload), %% We've just got connection settings from a peer. He have a %% couple of jobs to do here w.r.t. flow control %% If Delta != 0, we need to change every stream's %% send_window_size in the state open or %% half_closed_remote. We'll just send the message %% everywhere. It's up to them if they need to do %% anything. UpdatedStreams1 = h2_stream_set:update_all_send_windows(Delta, Streams), UpdatedStreams2 = case proplists:get_value(?SETTINGS_MAX_CONCURRENT_STREAMS, PList) of undefined -> UpdatedStreams1; NewMax -> h2_stream_set:update_my_max_active(NewMax, UpdatedStreams1) end, NewEncodeContext = hpack:new_max_table_size(HTS, EncodeContext), socksend(Conn, h2_frame_settings:ack()), {next_state, connected, Conn#connection{ peer_settings=NewPeerSettings, %% Why aren't we updating send_window_size here? Section 6.9.2 of %% the spec says: "The connection flow-control window can only be %% changed using WINDOW_UPDATE frames.", encode_context=NewEncodeContext, streams=UpdatedStreams2 }}; {error, Code} -> go_away(Code, Conn) end; %% This is the case where we got an ACK, so dequeue settings we're %% waiting to apply route_frame({H, _Payload}, #connection{ settings_sent=SS, streams=Streams, self_settings=#settings{ initial_window_size=OldIWS } }=Conn) when H#frame_header.type == ?SETTINGS, ?IS_FLAG((H#frame_header.flags), ?FLAG_ACK) -> case queue:out(SS) of {{value, {_Ref, NewSettings}}, NewSS} -> UpdatedStreams1 = case NewSettings#settings.initial_window_size of undefined -> ok; NewIWS -> Delta = OldIWS - NewIWS, h2_stream_set:update_all_recv_windows(Delta, Streams) end, UpdatedStreams2 = case NewSettings#settings.max_concurrent_streams of undefined -> UpdatedStreams1; NewMax -> h2_stream_set:update_their_max_active(NewMax, UpdatedStreams1) end, {next_state, connected, Conn#connection{ streams=UpdatedStreams2, settings_sent=NewSS, self_settings=NewSettings %% Same thing here, section 6.9.2 }}; _X -> {next_state, closing, Conn} end; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Stream level frames %% %% receive data frame bigger than connection recv window route_frame({H,_Payload}, Conn) when H#frame_header.type == ?DATA, H#frame_header.length > Conn#connection.recv_window_size -> go_away(?FLOW_CONTROL_ERROR, Conn); route_frame(F={H=#frame_header{ length=L, stream_id=StreamId}, _Payload}, #connection{ recv_window_size=CRWS, streams=Streams }=Conn) when H#frame_header.type == ?DATA -> Stream = h2_stream_set:get(StreamId, Streams), case h2_stream_set:type(Stream) of active -> case { h2_stream_set:recv_window_size(Stream) < L, Conn#connection.flow_control, L > 0 } of {true, _, _} -> rst_stream(Stream, ?FLOW_CONTROL_ERROR, Conn); %% If flow control is set to auto, and L > 0, send %% window updates back to the peer. If L == 0, we're %% not allowed to send window_updates of size 0, so we %% hit the next clause {false, auto, true} -> %% Make window size great again h2_frame_window_update:send(Conn#connection.socket, L, StreamId), send_window_update(self(), L), recv_data(Stream, F), {next_state, connected, Conn}; %% Either %% {false, auto, true} or %% {false, manual, _DoesntMatter} _Tried -> recv_data(Stream, F), {next_state, connected, Conn#connection{ recv_window_size=CRWS-L, streams=h2_stream_set:upsert( h2_stream_set:decrement_recv_window(L, Stream), Streams) }} end; _ -> go_away(?PROTOCOL_ERROR, Conn) end; route_frame({#frame_header{type=?HEADERS}=FH, _Payload}, #connection{}=Conn) when Conn#connection.type == server, FH#frame_header.stream_id rem 2 == 0 -> go_away(?PROTOCOL_ERROR, Conn); route_frame({#frame_header{type=?HEADERS}=FH, _Payload}=Frame, #connection{}=Conn) -> StreamId = FH#frame_header.stream_id, Streams = Conn#connection.streams, %% Four things could be happening here. %% If we're a server, these are either request headers or request %% trailers %% If we're a client, these are either headers in response to a %% client request, or headers in response to a push promise Stream = h2_stream_set:get(StreamId, Streams), {ContinuationType, NewConn} = case {h2_stream_set:type(Stream), Conn#connection.type} of {idle, server} -> case h2_stream_set:new_stream( StreamId, self(), Conn#connection.stream_callback_mod, Conn#connection.stream_callback_opts, Conn#connection.socket, (Conn#connection.peer_settings)#settings.initial_window_size, (Conn#connection.self_settings)#settings.initial_window_size, Streams) of {error, ErrorCode, NewStream} -> rst_stream(NewStream, ErrorCode, Conn), {none, Conn}; NewStreams -> {headers, Conn#connection{streams=NewStreams}} end; {active, server} -> {trailers, Conn}; _ -> {headers, Conn} end, case ContinuationType of none -> {next_state, connected, NewConn}; _ -> ContinuationState = #continuation_state{ type = ContinuationType, frames = queue:from_list([Frame]), end_stream = ?IS_FLAG((FH#frame_header.flags), ?FLAG_END_STREAM), end_headers = ?IS_FLAG((FH#frame_header.flags), ?FLAG_END_HEADERS), stream_id = StreamId }, %% maybe_hpack/2 uses this #continuation_state to figure %% out what to do, which might include hpack maybe_hpack(ContinuationState, NewConn) end; route_frame(F={H=#frame_header{ stream_id=_StreamId, type=?CONTINUATION }, _Payload}, #connection{ continuation = #continuation_state{ frames = CFQ } = Cont }=Conn) -> maybe_hpack(Cont#continuation_state{ frames=queue:in(F, CFQ), end_headers=?IS_FLAG((H#frame_header.flags), ?FLAG_END_HEADERS) }, Conn); route_frame({H, _Payload}, #connection{}=Conn) when H#frame_header.type == ?PRIORITY, H#frame_header.stream_id == 0 -> go_away(?PROTOCOL_ERROR, Conn); route_frame({H, _Payload}, #connection{} = Conn) when H#frame_header.type == ?PRIORITY -> {next_state, connected, Conn}; route_frame( {#frame_header{ stream_id=StreamId, type=?RST_STREAM }, _Payload}, #connection{} = Conn) -> %% TODO: anything with this? %% EC = h2_frame_rst_stream:error_code(Payload), Streams = Conn#connection.streams, Stream = h2_stream_set:get(StreamId, Streams), case h2_stream_set:type(Stream) of idle -> go_away(?PROTOCOL_ERROR, Conn); _Stream -> %% TODO: RST_STREAM support {next_state, connected, Conn} end; route_frame({H=#frame_header{}, _P}, #connection{} =Conn) when H#frame_header.type == ?PUSH_PROMISE, Conn#connection.type == server -> go_away(?PROTOCOL_ERROR, Conn); route_frame({H=#frame_header{ stream_id=StreamId }, Payload}=Frame, #connection{}=Conn) when H#frame_header.type == ?PUSH_PROMISE, Conn#connection.type == client -> PSID = h2_frame_push_promise:promised_stream_id(Payload), Streams = Conn#connection.streams, Old = h2_stream_set:get(StreamId, Streams), NotifyPid = h2_stream_set:notify_pid(Old), %% TODO: Case statement here about execeding the number of %% pushed. Honestly I think it's a bigger problem with the %% h2_stream_set, but we can punt it for now. The idea is that %% reserved(local) and reserved(remote) aren't technically %% 'active', but they're being counted that way right now. Again, %% that only matters if Server Push is enabled. NewStreams = h2_stream_set:new_stream( PSID, NotifyPid, Conn#connection.stream_callback_mod, Conn#connection.stream_callback_opts, Conn#connection.socket, (Conn#connection.peer_settings)#settings.initial_window_size, (Conn#connection.self_settings)#settings.initial_window_size, Streams), Continuation = #continuation_state{ stream_id=StreamId, type=push_promise, frames = queue:in(Frame, queue:new()), end_headers=?IS_FLAG((H#frame_header.flags), ?FLAG_END_HEADERS), promised_id=PSID }, maybe_hpack(Continuation, Conn#connection{ streams = NewStreams }); %% PING %% If not stream 0, then connection error route_frame({H, _Payload}, #connection{} = Conn) when H#frame_header.type == ?PING, H#frame_header.stream_id =/= 0 -> go_away(?PROTOCOL_ERROR, Conn); %% If length != 8, FRAME_SIZE_ERROR %% TODO: I think this case is already covered in h2_frame now route_frame({H, _Payload}, #connection{}=Conn) when H#frame_header.type == ?PING, H#frame_header.length =/= 8 -> go_away(?FRAME_SIZE_ERROR, Conn); %% If PING && !ACK, must ACK route_frame({H, Ping}, #connection{}=Conn) when H#frame_header.type == ?PING, ?NOT_FLAG((H#frame_header.flags), ?FLAG_ACK) -> Ack = h2_frame_ping:ack(Ping), socksend(Conn, h2_frame:to_binary(Ack)), {next_state, connected, Conn}; route_frame({H, Payload}, #connection{pings = Pings}=Conn) when H#frame_header.type == ?PING, ?IS_FLAG((H#frame_header.flags), ?FLAG_ACK) -> case maps:get(h2_frame_ping:to_binary(Payload), Pings, undefined) of undefined -> ok; {NotifyPid, _} -> NotifyPid ! {'PONG', self()} end, NextPings = maps:remove(Payload, Pings), {next_state, connected, Conn#connection{pings = NextPings}}; route_frame({H=#frame_header{stream_id=0}, _Payload}, #connection{}=Conn) when H#frame_header.type == ?GOAWAY -> go_away(?NO_ERROR, Conn); %% Window Update route_frame( {#frame_header{ stream_id=0, type=?WINDOW_UPDATE }, Payload}, #connection{ send_window_size=SWS }=Conn) -> WSI = h2_frame_window_update:size_increment(Payload), NewSendWindow = SWS+WSI, case NewSendWindow > 2147483647 of true -> go_away(?FLOW_CONTROL_ERROR, Conn); false -> %% TODO: Priority Sort! Right now, it's just sorting on %% lowest stream_id first Streams = h2_stream_set:sort(Conn#connection.streams), {RemainingSendWindow, UpdatedStreams} = h2_stream_set:send_what_we_can( all, NewSendWindow, (Conn#connection.peer_settings)#settings.max_frame_size, Streams ), {next_state, connected, Conn#connection{ send_window_size=RemainingSendWindow, streams=UpdatedStreams }} end; route_frame( {#frame_header{type=?WINDOW_UPDATE}=FH, Payload}, #connection{}=Conn ) -> StreamId = FH#frame_header.stream_id, Streams = Conn#connection.streams, WSI = h2_frame_window_update:size_increment(Payload), Stream = h2_stream_set:get(StreamId, Streams), case h2_stream_set:type(Stream) of idle -> go_away(?PROTOCOL_ERROR, Conn); closed -> rst_stream(Stream, ?STREAM_CLOSED, Conn); active -> SWS = Conn#connection.send_window_size, NewSSWS = h2_stream_set:send_window_size(Stream)+WSI, case NewSSWS > 2147483647 of true -> rst_stream(Stream, ?FLOW_CONTROL_ERROR, Conn); false -> {RemainingSendWindow, NewStreams} = h2_stream_set:send_what_we_can( StreamId, SWS, (Conn#connection.peer_settings)#settings.max_frame_size, h2_stream_set:upsert( h2_stream_set:increment_send_window_size(WSI, Stream), Streams) ), {next_state, connected, Conn#connection{ send_window_size=RemainingSendWindow, streams=NewStreams }} end end; route_frame({#frame_header{type=T}, _}, Conn) when T > ?CONTINUATION -> {next_state, connected, Conn}; route_frame(Frame, #connection{}=Conn) -> error_logger:error_msg("Frame condition not covered by pattern match." "Please open a github issue with this output: ~s", [h2_frame:format(Frame)]), go_away(?PROTOCOL_ERROR, Conn). handle_event(_, {stream_finished, StreamId, Headers, Body}, Conn) -> Stream = h2_stream_set:get(StreamId, Conn#connection.streams), case h2_stream_set:type(Stream) of active -> NotifyPid = h2_stream_set:notify_pid(Stream), Response = case Conn#connection.type of server -> garbage; client -> {Headers, Body} end, {_NewStream, NewStreams} = h2_stream_set:close( Stream, Response, Conn#connection.streams), NewConn = Conn#connection{ streams = NewStreams }, case {Conn#connection.type, is_pid(NotifyPid)} of {client, true} -> NotifyPid ! {'END_STREAM', StreamId}; _ -> ok end, {keep_state, NewConn}; _ -> %% stream finished multiple times {keep_state, Conn} end; handle_event(_, {send_window_update, 0}, Conn) -> {keep_state, Conn}; handle_event(_, {send_window_update, Size}, #connection{ recv_window_size=CRWS, socket=Socket }=Conn) -> ok = h2_frame_window_update:send(Socket, Size, 0), {keep_state, Conn#connection{ recv_window_size=CRWS+Size }}; handle_event(_, {update_settings, Http2Settings}, #connection{}=Conn) -> {keep_state, send_settings(Http2Settings, Conn)}; handle_event(_, {send_headers, StreamId, Headers, Opts}, #connection{ encode_context=EncodeContext, streams = Streams, socket = Socket }=Conn ) -> StreamComplete = proplists:get_value(send_end_stream, Opts, false), Stream = h2_stream_set:get(StreamId, Streams), case h2_stream_set:type(Stream) of active -> {FramesToSend, NewContext} = h2_frame_headers:to_frames(h2_stream_set:stream_id(Stream), Headers, EncodeContext, (Conn#connection.peer_settings)#settings.max_frame_size, StreamComplete ), [sock:send(Socket, h2_frame:to_binary(Frame)) || Frame <- FramesToSend], send_h(Stream, Headers), {keep_state, Conn#connection{ encode_context=NewContext }}; idle -> %% In theory this is a client maybe activating a stream, %% but in practice, we've already activated the stream in %% new_stream/1 {keep_state, Conn}; closed -> {keep_state, Conn} end; handle_event(_, {send_trailers, StreamId, Headers, Opts}, #connection{ encode_context=EncodeContext, streams = Streams, socket = _Socket }=Conn ) -> BodyComplete = proplists:get_value(send_end_stream, Opts, true), Stream = h2_stream_set:get(StreamId, Streams), case h2_stream_set:type(Stream) of active -> {FramesToSend, NewContext} = h2_frame_headers:to_frames(h2_stream_set:stream_id(Stream), Headers, EncodeContext, (Conn#connection.peer_settings)#settings.max_frame_size, true ), NewS = h2_stream_set:update_trailers(FramesToSend, Stream), {NewSWS, NewStreams} = h2_stream_set:send_what_we_can( StreamId, Conn#connection.send_window_size, (Conn#connection.peer_settings)#settings.max_frame_size, h2_stream_set:upsert( h2_stream_set:update_data_queue(h2_stream_set:queued_data(Stream), BodyComplete, NewS), Conn#connection.streams)), send_t(Stream, Headers), {keep_state, Conn#connection{ encode_context=NewContext, send_window_size=NewSWS, streams=NewStreams }}; idle -> %% In theory this is a client maybe activating a stream, %% but in practice, we've already activated the stream in %% new_stream/1 {keep_state, Conn}; closed -> {keep_state, Conn} end; handle_event(_, {send_body, StreamId, Body, Opts}, #connection{}=Conn) -> BodyComplete = proplists:get_value(send_end_stream, Opts, true), Stream = h2_stream_set:get(StreamId, Conn#connection.streams), case h2_stream_set:type(Stream) of active -> OldBody = h2_stream_set:queued_data(Stream), NewBody = case is_binary(OldBody) of true -> <>; false -> Body end, {NewSWS, NewStreams} = h2_stream_set:send_what_we_can( StreamId, Conn#connection.send_window_size, (Conn#connection.peer_settings)#settings.max_frame_size, h2_stream_set:upsert( h2_stream_set:update_data_queue(NewBody, BodyComplete, Stream), Conn#connection.streams)), {keep_state, Conn#connection{ send_window_size=NewSWS, streams=NewStreams }}; idle -> %% Sending DATA frames on an idle stream? It's a %% Connection level protocol error on receipt, but If we %% have no active stream what can we even do? {keep_state, Conn}; closed -> {keep_state, Conn} end; handle_event(_, {send_request, NotifyPid, Headers, Body}, #connection{ streams=Streams, next_available_stream_id=NextId }=Conn) -> case send_request(NextId, NotifyPid, Conn, Streams, Headers, Body) of {ok, GoodStreamSet} -> {keep_state, Conn#connection{ next_available_stream_id=NextId+2, streams=GoodStreamSet }}; {error, _Code} -> {keep_state, Conn} end; handle_event(_, {send_promise, StreamId, NewStreamId, Headers}, #connection{ streams=Streams, encode_context=OldContext }=Conn ) -> NewStream = h2_stream_set:get(NewStreamId, Streams), case h2_stream_set:type(NewStream) of active -> %% TODO: This could be a series of frames, not just one {PromiseFrame, NewContext} = h2_frame_push_promise:to_frame( StreamId, NewStreamId, Headers, OldContext ), %% Send the PP Frame Binary = h2_frame:to_binary(PromiseFrame), socksend(Conn, Binary), %% Get the promised stream rolling h2_stream:send_pp(h2_stream_set:stream_pid(NewStream), Headers), {keep_state, Conn#connection{ encode_context=NewContext }}; _ -> {keep_state, Conn} end; handle_event(_, {check_settings_ack, {Ref, NewSettings}}, #connection{ settings_sent=SS }=Conn) -> case queue:out(SS) of {{value, {Ref, NewSettings}}, _} -> %% This is still here! go_away(?SETTINGS_TIMEOUT, Conn); _ -> %% YAY! {keep_state, Conn} end; handle_event(_, {send_bin, Binary}, #connection{} = Conn) -> socksend(Conn, Binary), {keep_state, Conn}; handle_event(_, {send_frame, Frame}, #connection{} =Conn) -> Binary = h2_frame:to_binary(Frame), socksend(Conn, Binary), {keep_state, Conn}; handle_event(stop, _StateName, #connection{}=Conn) -> go_away(0, Conn); handle_event({call, From}, streams, #connection{ streams=Streams }=Conn) -> {keep_state, Conn, [{reply, From, Streams}]}; handle_event({call, From}, {get_response, StreamId}, #connection{}=Conn) -> Stream = h2_stream_set:get(StreamId, Conn#connection.streams), {Reply, NewStreams} = case h2_stream_set:type(Stream) of closed -> {_, NewStreams0} = h2_stream_set:close( Stream, garbage, Conn#connection.streams), {{ok, h2_stream_set:response(Stream)}, NewStreams0}; active -> {not_ready, Conn#connection.streams} end, {keep_state, Conn#connection{streams=NewStreams}, [{reply, From, Reply}]}; handle_event({call, From}, {new_stream, NotifyPid}, #connection{ streams=Streams, next_available_stream_id=NextId }=Conn) -> {Reply, NewStreams} = case h2_stream_set:new_stream( NextId, NotifyPid, Conn#connection.stream_callback_mod, Conn#connection.stream_callback_opts, Conn#connection.socket, Conn#connection.peer_settings#settings.initial_window_size, Conn#connection.self_settings#settings.initial_window_size, Streams) of {error, Code, _NewStream} -> %% TODO: probably want to have events like this available for metrics %% tried to create new_stream but there are too many {{error, Code}, Streams}; GoodStreamSet -> {NextId, GoodStreamSet} end, {keep_state, Conn#connection{ next_available_stream_id=NextId+2, streams=NewStreams }, [{reply, From, Reply}]}; handle_event({call, From}, is_push, #connection{ peer_settings=#settings{enable_push=Push} }=Conn) -> IsPush = case Push of 1 -> true; _ -> false end, {keep_state, Conn, [{reply, From, IsPush}]}; handle_event({call, From}, get_peer, #connection{ socket=Socket }=Conn) -> case sock:peername(Socket) of {error, _}=Error -> {keep_state, Conn, [{reply, From, Error}]}; {ok, _AddrPort}=OK -> {keep_state, Conn, [{reply, From, OK}]} end; handle_event({call, From}, get_peercert, #connection{ socket=Socket }=Conn) -> case sock:peercert(Socket) of {error, _}=Error -> {keep_state, Conn, [{reply, From, Error}]}; {ok, _Cert}=OK -> {keep_state, Conn, [{reply, From, OK}]} end; handle_event({call, From}, {send_request, NotifyPid, Headers, Body}, #connection{ streams=Streams, next_available_stream_id=NextId }=Conn) -> case send_request(NextId, NotifyPid, Conn, Streams, Headers, Body) of {ok, GoodStreamSet} -> {keep_state, Conn#connection{ next_available_stream_id=NextId+2, streams=GoodStreamSet }, [{reply, From, ok}]}; {error, Code} -> {keep_state, Conn, [{reply, From, {error, Code}}]} end; handle_event({call, From}, {send_ping, NotifyPid}, #connection{pings = Pings} = Conn) -> PingValue = crypto:strong_rand_bytes(8), Frame = h2_frame_ping:new(PingValue), Headers = #frame_header{stream_id = 0, flags = 16#0}, Binary = h2_frame:to_binary({Headers, Frame}), case socksend(Conn, Binary) of ok -> NextPings = maps:put(PingValue, {NotifyPid, erlang:monotonic_time(milli_seconds)}, Pings), NextConn = Conn#connection{pings = NextPings}, {keep_state, NextConn, [{reply, From, ok}]}; {error, _Reason} = Err -> {keep_state, Conn, [{reply, From, Err}]} end; %% Socket Messages %% {tcp, Socket, Data} handle_event(info, {tcp, Socket, Data}, #connection{ socket={gen_tcp,Socket} }=Conn) -> handle_socket_data(Data, Conn); %% {ssl, Socket, Data} handle_event(info, {ssl, Socket, Data}, #connection{ socket={ssl,Socket} }=Conn) -> handle_socket_data(Data, Conn); %% {tcp_passive, Socket} handle_event(info, {tcp_passive, Socket}, #connection{ socket={gen_tcp, Socket} }=Conn) -> handle_socket_passive(Conn); %% {tcp_closed, Socket} handle_event(info, {tcp_closed, Socket}, #connection{ socket={gen_tcp, Socket} }=Conn) -> handle_socket_closed(Conn); %% {ssl_closed, Socket} handle_event(info, {ssl_closed, Socket}, #connection{ socket={ssl, Socket} }=Conn) -> handle_socket_closed(Conn); %% {tcp_error, Socket, Reason} handle_event(info, {tcp_error, Socket, Reason}, #connection{ socket={gen_tcp,Socket} }=Conn) -> handle_socket_error(Reason, Conn); %% {ssl_error, Socket, Reason} handle_event(info, {ssl_error, Socket, Reason}, #connection{ socket={ssl,Socket} }=Conn) -> handle_socket_error(Reason, Conn); handle_event(info, {_,R}, #connection{}=Conn) -> handle_socket_error(R, Conn); handle_event(_, _, Conn) -> go_away(?PROTOCOL_ERROR, Conn). code_change(_OldVsn, StateName, Conn, _Extra) -> {ok, StateName, Conn}. terminate(normal, _StateName, _Conn) -> ok; terminate(_Reason, _StateName, _Conn=#connection{}) -> ok; terminate(_Reason, _StateName, _State) -> ok. -spec go_away(error_code(), connection()) -> {next_state, closing, connection()}. go_away(ErrorCode, #connection{ next_available_stream_id=NAS }=Conn) -> GoAway = h2_frame_goaway:new(NAS, ErrorCode), GoAwayBin = h2_frame:to_binary({#frame_header{ stream_id=0 }, GoAway}), socksend(Conn, GoAwayBin), %% TODO: why is this sending a string? gen_statem:cast(self(), io_lib:format("GO_AWAY: ErrorCode ~p", [ErrorCode])), {next_state, closing, Conn}. %% rst_stream/3 looks for a running process for the stream. If it %% finds one, it delegates sending the rst_stream frame to it, but if %% it doesn't, it seems like a waste to spawn one just to kill it %% after sending that frame, so we send it from here. -spec rst_stream( h2_stream_set:stream(), error_code(), connection() ) -> {next_state, connected, connection()}. rst_stream(Stream, ErrorCode, Conn) -> case h2_stream_set:type(Stream) of active -> %% Can this ever be undefined? Pid = h2_stream_set:stream_pid(Stream), %% h2_stream's rst_stream will take care of letting us know %% this stream is closed and will send us a message to close the %% stream somewhere else h2_stream:rst_stream(Pid, ErrorCode), {next_state, connected, Conn}; _ -> StreamId = h2_stream_set:stream_id(Stream), RstStream = h2_frame_rst_stream:new(ErrorCode), RstStreamBin = h2_frame:to_binary( {#frame_header{ stream_id=StreamId }, RstStream}), sock:send(Conn#connection.socket, RstStreamBin), {next_state, connected, Conn} end. -spec send_settings(settings(), connection()) -> connection(). send_settings(SettingsToSend, #connection{ self_settings=CurrentSettings, settings_sent=SS }=Conn) -> Ref = make_ref(), Bin = h2_frame_settings:send(CurrentSettings, SettingsToSend), socksend(Conn, Bin), send_ack_timeout({Ref,SettingsToSend}), Conn#connection{ settings_sent=queue:in({Ref, SettingsToSend}, SS) }. -spec send_ack_timeout({reference(), settings()}) -> pid(). send_ack_timeout(SS) -> Self = self(), SendAck = fun() -> timer:sleep(5000), gen_statem:cast(Self, {check_settings_ack,SS}) end, spawn_link(SendAck). %% private socket handling active_once(Socket) -> sock:setopts(Socket, [{active, once}]). client_options(Transport, SSLOptions) -> ClientSocketOptions = [ binary, {packet, raw}, {active, false} ], case Transport of ssl -> [{alpn_advertised_protocols, [<<"h2">>]}|ClientSocketOptions ++ SSLOptions]; gen_tcp -> ClientSocketOptions end. start_http2_server( Http2Settings, #connection{ socket=Socket }=Conn) -> case accept_preface(Socket) of ok -> ok = active_once(Socket), NewState = Conn#connection{ type=server, next_available_stream_id=2, flow_control=application:get_env(chatterbox, server_flow_control, auto) }, {next_state, handshake, send_settings(Http2Settings, NewState) }; {error, invalid_preface} -> {next_state, closing, Conn} end. %% We're going to iterate through the preface string until we're done %% or hit a mismatch accept_preface(Socket) -> accept_preface(Socket, <>). accept_preface(_Socket, <<>>) -> ok; accept_preface(Socket, <>) -> case sock:recv(Socket, 1, 5000) of {ok, <>} -> accept_preface(Socket, Rem); _E -> sock:close(Socket), {error, invalid_preface} end. %% Incoming data is a series of frames. With a passive socket we can just: %% 1. read(9) %% 2. turn that 9 into an http2 frame header %% 3. use that header's length field L %% 4. read(L), now we have a frame %% 5. do something with it %% 6. goto 1 %% Things will be different with an {active, true} socket, and also %% different again with an {active, once} socket %% with {active, true}, we'd have to maintain some kind of input queue %% because it will be very likely that Data is not neatly just a frame %% with {active, once}, we'd probably be in a situation where Data %% starts with a frame header. But it's possible that we're here with %% a partial frame left over from the last active stream %% We're going to go with the {active, once} approach, because it %% won't block the gen_server on Transport:read(L), but it will wake %% up and do something every time Data comes in. handle_socket_data(<<>>, #connection{ socket=Socket }=Conn) -> active_once(Socket), {keep_state, Conn}; handle_socket_data(Data, #connection{ socket=Socket, buffer=Buffer }=Conn) -> More = case sock:recv(Socket, 0, 1) of %% fail fast {ok, Rest} -> Rest; %% It's not really an error, it's what we want {error, timeout} -> <<>>; _ -> <<>> end, %% What is buffer? %% empty - nothing, yay %% {frame, h2_frame:header(), binary()} - Frame Header processed, Payload not big enough %% {binary, binary()} - If we're here, it must mean that Bin was too small to even be a header ToParse = case Buffer of empty -> <>; {frame, FHeader, BufferBin} -> {FHeader, <>}; {binary, BufferBin} -> <> end, %% Now that the buffer has been merged, it's best to make sure any %% further state references don't have one NewConn = Conn#connection{buffer=empty}, case h2_frame:recv(ToParse) of %% We got a full frame, ship it off to the FSM {ok, Frame, Rem} -> gen_statem:cast(self(), {frame, Frame}), handle_socket_data(Rem, NewConn); %% Not enough bytes left to make a header :( {not_enough_header, Bin} -> %% This is a situation where more bytes should come soon, %% so let's switch back to active, once active_once(Socket), {keep_state, NewConn#connection{buffer={binary, Bin}}}; %% Not enough bytes to make a payload {not_enough_payload, Header, Bin} -> %% This too active_once(Socket), {keep_state, NewConn#connection{buffer={frame, Header, Bin}}}; {error, 0, Code, _Rem} -> %% Remaining Bytes don't matter, we're closing up shop. go_away(Code, NewConn); {error, StreamId, Code, Rem} -> Stream = h2_stream_set:get(StreamId, Conn#connection.streams), rst_stream(Stream, Code, NewConn), handle_socket_data(Rem, NewConn) end. handle_socket_passive(Conn) -> {keep_state, Conn}. handle_socket_closed(Conn) -> {stop, normal, Conn}. handle_socket_error(Reason, Conn) -> {stop, Reason, Conn}. socksend(#connection{ socket=Socket }, Data) -> case sock:send(Socket, Data) of ok -> ok; {error, Reason} -> {error, Reason} end. %% maybe_hpack will decode headers if it can, or tell the connection %% to wait for CONTINUATION frames if it can't. -spec maybe_hpack(#continuation_state{}, connection()) -> {next_state, atom(), connection()}. %% If there's an END_HEADERS flag, we have a complete headers binary %% to decode, let's do this! maybe_hpack(Continuation, Conn) when Continuation#continuation_state.end_headers -> Stream = h2_stream_set:get( Continuation#continuation_state.stream_id, Conn#connection.streams ), HeadersBin = h2_frame_headers:from_frames( queue:to_list(Continuation#continuation_state.frames) ), case hpack:decode(HeadersBin, Conn#connection.decode_context) of {error, compression_error} -> go_away(?COMPRESSION_ERROR, Conn); {ok, {Headers, NewDecodeContext}} -> case {Continuation#continuation_state.type, Continuation#continuation_state.end_stream} of {push_promise, _} -> Promised = h2_stream_set:get( Continuation#continuation_state.promised_id, Conn#connection.streams ), recv_pp(Promised, Headers); {trailers, false} -> rst_stream(Stream, ?PROTOCOL_ERROR, Conn); _ -> %% headers or trailers! recv_h(Stream, Conn, Headers) end, case Continuation#continuation_state.end_stream of true -> recv_es(Stream, Conn); false -> ok end, {next_state, connected, Conn#connection{ decode_context=NewDecodeContext, continuation=undefined }} end; %% If not, we have to wait for all the CONTINUATIONS to roll in. maybe_hpack(Continuation, Conn) -> {next_state, continuation, Conn#connection{ continuation = Continuation }}. %% Stream API: These will be moved -spec recv_h( Stream :: h2_stream_set:stream(), Conn :: connection(), Headers :: hpack:headers()) -> ok. recv_h(Stream, Conn, Headers) -> case h2_stream_set:type(Stream) of active -> %% If the stream is active, let the process deal with it. Pid = h2_stream_set:pid(Stream), h2_stream:send_event(Pid, {recv_h, Headers}); closed -> %% If the stream is closed, there's no running FSM rst_stream(Stream, ?STREAM_CLOSED, Conn); idle -> %% If we're calling this function, we've already activated %% a stream FSM (probably). On the off chance we didn't, %% we'll throw this rst_stream(Stream, ?STREAM_CLOSED, Conn) end. -spec send_h( h2_stream_set:stream(), hpack:headers()) -> ok. send_h(Stream, Headers) -> case h2_stream_set:pid(Stream) of undefined -> %% TODO: Should this be some kind of error? ok; Pid -> h2_stream:send_event(Pid, {send_h, Headers}) end. -spec send_t( h2_stream_set:stream(), hpack:headers()) -> ok. send_t(Stream, Trailers) -> case h2_stream_set:pid(Stream) of undefined -> %% TODO: Should this be some kind of error? ok; Pid -> h2_stream:send_event(Pid, {send_t, Trailers}) end. -spec recv_es(Stream :: h2_stream_set:stream(), Conn :: connection()) -> ok | {rst_stream, error_code()}. recv_es(Stream, Conn) -> case h2_stream_set:type(Stream) of active -> Pid = h2_stream_set:pid(Stream), h2_stream:send_event(Pid, recv_es); closed -> rst_stream(Stream, ?STREAM_CLOSED, Conn); idle -> rst_stream(Stream, ?STREAM_CLOSED, Conn) end. -spec recv_pp(h2_stream_set:stream(), hpack:headers()) -> ok. recv_pp(Stream, Headers) -> case h2_stream_set:pid(Stream) of undefined -> %% Should this be an error? ok; Pid -> h2_stream:send_event(Pid, {recv_pp, Headers}) end. -spec recv_data(h2_stream_set:stream(), h2_frame:frame()) -> ok. recv_data(Stream, Frame) -> case h2_stream_set:pid(Stream) of undefined -> %% Again, error? These aren't errors now because the code %% isn't set up to handle errors when these are called %% anyway. ok; Pid -> h2_stream:send_event(Pid, {recv_data, Frame}) end. send_request(NextId, NotifyPid, Conn, Streams, Headers, Body) -> case h2_stream_set:new_stream( NextId, NotifyPid, Conn#connection.stream_callback_mod, Conn#connection.stream_callback_opts, Conn#connection.socket, Conn#connection.peer_settings#settings.initial_window_size, Conn#connection.self_settings#settings.initial_window_size, Streams) of {error, Code, _NewStream} -> %% error creating new stream {error, Code}; GoodStreamSet -> send_headers(self(), NextId, Headers), send_body(self(), NextId, Body), {ok, GoodStreamSet} end. ================================================ FILE: src/h2_frame.erl ================================================ -module(h2_frame). -include("http2.hrl"). -export([ recv/1, read/1, read/2, read_binary_frame_header/1, read_binary_payload/2, from_binary/1, format_header/1, format_payload/1, format/1, to_binary/1, header_to_binary/1 ]). -type payload() :: h2_frame_data:payload() | h2_frame_headers:payload() | h2_frame_priority:payload() | h2_frame_rst_stream:payload() | h2_frame_settings:payload() | h2_frame_push_promise:payload() | h2_frame_ping:payload() | h2_frame_goaway:payload() | h2_frame_window_update:payload() | h2_frame_continuation:payload(). -type header() :: #frame_header{}. -type frame() :: {header(), payload()}. -export_type([frame/0, header/0, payload/0]). %% Each frame type should be able to be read off a binary stream. If %% the header is good, then it'll know how many more bytes to take off %% the stream for the payload. If there are more bytes left over, it's %% the next header, so we should return a tuple that contains the %% remainder as well. -callback read_binary(Bin::binary(), Header::header()) -> {ok, payload(), Remainder::binary()} | {error, stream_id(), error_code(), binary()}. %% For io:formatting -callback format(payload()) -> iodata(). %% convert payload to binary -callback to_binary(payload()) -> iodata(). %% TODO: some kind of callback for sending frames %-callback send(port(), payload()) -> ok | {error, term()}. -spec recv(binary() | {header(), binary()}) -> {ok, frame(), binary()} | {not_enough_header, binary()} | {not_enough_payload, header(), binary()} | {error, stream_id(), error_code(), binary()}. recv(Bin) when is_binary(Bin), byte_size(Bin) < 9 -> {not_enough_header, Bin}; recv(Bin) when is_binary(Bin) -> {Header, PayloadBin} = read_binary_frame_header(Bin), recv({Header, PayloadBin}); recv({Header, PayloadBin}) when byte_size(PayloadBin) < Header#frame_header.length -> {not_enough_payload, Header, PayloadBin}; recv({Header, PayloadBin}) -> case read_binary_payload(PayloadBin, Header) of {ok, Payload, Rem} -> {ok, {Header, Payload}, Rem}; Error -> Error end. -spec read(socket()) -> frame(). read(Socket) -> read(Socket, infinity). -spec read(socket(), timeout()) -> frame() | {error, closed|inet:posix()}. read(Socket, Timeout) -> case read_header(Socket, Timeout) of {error, Reason} -> {error, Reason}; FrameHeader -> {ok, Payload} = read_payload(Socket, FrameHeader, Timeout), {FrameHeader, Payload} end. %% Hi, it's been a while. We need to massage the API here into %% something that can handle an unknown number of bytes in a binary %% containing an unknown quantity of frames -spec from_binary(binary()) -> [frame()]. from_binary(Bin) -> from_binary(Bin, []). from_binary(<<>>, Acc) -> Acc; from_binary(Bin, Acc) -> {Header, PayloadBin} = read_binary_frame_header(Bin), {ok, Payload, Rem} = read_binary_payload(PayloadBin, Header), from_binary(Rem, [{Header, Payload}|Acc]). -spec format_header(header()) -> iodata(). format_header(#frame_header{ length = Length, type = Type, flags = Flags, stream_id = StreamId }) -> io_lib:format("[Frame Header: L:~p, T:~p, F:~p, StrId:~p]", [Length, ?FT(Type), Flags, StreamId]). -spec read_header(socket(), timeout()) -> header() | {error, closed|inet:posix()}. read_header({Transport, Socket}, Timeout) -> case Transport:recv(Socket, 9, Timeout) of {ok, HeaderBytes} -> {Header, <<>>} = read_binary_frame_header(HeaderBytes), Header; E -> E end. -spec read_binary_frame_header(binary()) -> {header(), binary()}. read_binary_frame_header(<>) -> Header = #frame_header{ length = Length, type = Type, flags = Flags, stream_id = StreamId }, {Header, Rem}. -spec read_payload(socket(), header(), timeout()) -> {ok, payload()} | {error, closed|inet:posix()}. read_payload(_, Header=#frame_header{length=0}, _Timeout) -> {ok, FramePayload, <<>>} = read_binary_payload(<<>>, Header), {ok, FramePayload}; read_payload({Transport, Socket}, Header=#frame_header{length=L}, Timeout) -> case Transport:recv(Socket, L, Timeout) of {ok, DataBin} -> {ok, FramePayload, <<>>} = read_binary_payload(DataBin, Header), {ok, FramePayload}; E -> E end. -spec read_binary_payload(binary(), header()) -> {ok, payload(), binary()} | {error, error_code()} | {error, stream_id(), error_code(), binary()}. read_binary_payload(Bin, Header = #frame_header{type=?DATA}) -> h2_frame_data:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?HEADERS}) -> h2_frame_headers:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?PRIORITY}) -> h2_frame_priority:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?RST_STREAM}) -> h2_frame_rst_stream:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?SETTINGS}) -> h2_frame_settings:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?PUSH_PROMISE}) -> h2_frame_push_promise:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?PING}) -> h2_frame_ping:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?GOAWAY}) -> h2_frame_goaway:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?WINDOW_UPDATE}) -> h2_frame_window_update:read_binary(Bin, Header); read_binary_payload(Bin, Header = #frame_header{type=?CONTINUATION}) -> h2_frame_continuation:read_binary(Bin, Header); read_binary_payload(Bin, Header) -> read_unsupported_frame_binary(Bin, Header). read_unsupported_frame_binary(Bin, #frame_header{length=0}) -> {ok, <<>>, Bin}; read_unsupported_frame_binary(Bin, #frame_header{length=L}) -> <> = Bin, {ok, PayloadBin, Rem}. -spec format_payload(frame()) -> iodata(). format_payload({#frame_header{type=?DATA}, P}) -> h2_frame_data:format(P); format_payload({#frame_header{type=?HEADERS}, P}) -> h2_frame_headers:format(P); format_payload({#frame_header{type=?PRIORITY}, P}) -> h2_frame_priority:format(P); format_payload({#frame_header{type=?RST_STREAM}, P}) -> h2_frame_rst_stream:format(P); format_payload({#frame_header{type=?SETTINGS}, P}) -> h2_frame_settings:format(P); format_payload({#frame_header{type=?PUSH_PROMISE}, P}) -> h2_frame_push_promise:format(P); format_payload({#frame_header{type=?PING}, P}) -> h2_frame_ping:format(P); format_payload({#frame_header{type=?GOAWAY}, P}) -> h2_frame_goaway:format(P); format_payload({#frame_header{type=?WINDOW_UPDATE}, P}) -> h2_frame_window_update:format(P); format_payload({#frame_header{type=?CONTINUATION}, P}) -> h2_frame_continuation:format(P); format_payload({_, _P}) -> "Unsupported Frame". -spec format(frame()) -> iodata(). format(error) -> "error"; format({error, E}) -> io_lib:format("error : ~p",[E]); format({Header, Payload}) -> lists:flatten(io_lib:format("~s | ~s", [format_header(Header), format_payload({Header, Payload})])); format(<<>>) -> "". -spec to_binary(frame()) -> iodata(). to_binary({Header, Payload}) -> {Type, PayloadBin} = payload_to_binary(Payload), NewHeader = Header#frame_header{ length = iodata_size(PayloadBin), type = Type }, HeaderBin = header_to_binary(NewHeader), [HeaderBin, PayloadBin]. -spec header_to_binary(header()) -> iodata(). header_to_binary(#frame_header{ length=L, type=T, flags=F, stream_id=StreamId }) -> <>. -spec payload_to_binary(payload()) -> {frame_type(), iodata()}. payload_to_binary(P) -> Type = payload_type(P), Bin = case Type of ?DATA -> h2_frame_data:to_binary(P); ?HEADERS -> h2_frame_headers:to_binary(P); ?PRIORITY -> h2_frame_priority:to_binary(P); ?RST_STREAM -> h2_frame_rst_stream:to_binary(P); ?SETTINGS -> h2_frame_settings:to_binary(P); ?PUSH_PROMISE -> h2_frame_push_promise:to_binary(P); ?PING -> h2_frame_ping:to_binary(P); ?GOAWAY -> h2_frame_goaway:to_binary(P); ?WINDOW_UPDATE -> h2_frame_window_update:to_binary(P); ?CONTINUATION -> h2_frame_continuation:to_binary(P) end, {Type, Bin}. iodata_size(L) when is_list(L) -> lists:foldl(fun(X, Acc) -> Acc + iodata_size(X) end, 0, L); iodata_size(B) when is_binary(B) -> byte_size(B). %% Breaking my own abstraction here, but only for a performance %% optimization. This function assumes that records are built with the %% record name as the first element in the tuple, and if that changes, %% or if payloads are no longer records, then this will stop working. -spec payload_type(payload()) -> frame_type(). payload_type(P) when element(1, P) =:= data -> ?DATA; payload_type(P) when element(1, P) =:= headers -> ?HEADERS; payload_type(P) when element(1, P) =:= priority -> ?PRIORITY; payload_type(P) when element(1, P) =:= rst_stream -> ?RST_STREAM; payload_type(P) when element(1, P) =:= settings -> ?SETTINGS; payload_type(P) when element(1, P) =:= push_promise -> ?PUSH_PROMISE; payload_type(P) when element(1, P) =:= ping -> ?PING; payload_type(P) when element(1, P) =:= goaway -> ?GOAWAY; payload_type(P) when element(1, P) =:= window_update -> ?WINDOW_UPDATE; payload_type(P) when element(1, P) =:= continuation -> ?CONTINUATION. ================================================ FILE: src/h2_frame_continuation.erl ================================================ -module(h2_frame_continuation). -include("http2.hrl"). -behaviour(h2_frame). -export( [ block_fragment/1, format/1, new/1, read_binary/2, to_binary/1 ]). -record(continuation, { block_fragment :: binary() }). -type payload() :: #continuation{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec block_fragment(payload()) -> binary(). block_fragment(#continuation{block_fragment=BF}) -> BF. -spec new(binary()) -> payload(). new(Bin) -> #continuation{ block_fragment=Bin }. -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(_, #frame_header{ stream_id=0 }) -> {error, 0, ?PROTOCOL_ERROR, <<>>}; read_binary(Bin, #frame_header{length=Length}) -> <> = Bin, Payload = #continuation{ block_fragment=Data }, {ok, Payload, Rem}. -spec format(payload()) -> iodata(). format(Payload) -> io_lib:format("[Continuation: ~p ]", [Payload]). -spec to_binary(payload()) -> iodata(). to_binary(#continuation{block_fragment=BF}) -> BF. ================================================ FILE: src/h2_frame_data.erl ================================================ -module(h2_frame_data). -include("http2.hrl"). -behaviour(h2_frame). -export([ format/1, read_binary/2, to_frames/3, to_binary/1, data/1, new/1 ]). -record(data, { data :: iodata() }). -type payload() :: #data{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec data(payload()) -> iodata(). data(#data{data=D}) -> D. -spec format(payload()) -> iodata(). format(Payload) -> BinToShow = case size(Payload) > 7 of false -> Payload#data.data; true -> <> = Payload#data.data, Start end, io_lib:format("[Data: {data: ~p ...}]", [BinToShow]). -spec new(binary()) -> payload(). new(Data) -> #data{data=Data}. -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(_, #frame_header{stream_id=0}) -> {error, 0, ?PROTOCOL_ERROR, <<>>}; read_binary(Bin, _H=#frame_header{length=0}) -> {ok, #data{data= <<>>}, Bin}; read_binary(Bin, H=#frame_header{length=L}) -> <> = Bin, case h2_padding:read_possibly_padded_payload(PayloadBin, H) of {error, Code} -> {error, Code}; Data -> {ok, #data{data=Data}, Rem} end. -spec to_frames(stream_id(), iodata(), settings()) -> [h2_frame:frame()]. to_frames(StreamId, IOList, Settings) when is_list(IOList) -> to_frames(StreamId, iolist_to_binary(IOList), Settings); to_frames(StreamId, Data, S=#settings{max_frame_size=MFS}) -> L = byte_size(Data), case L >= MFS of false -> [{#frame_header{ length=L, type=?DATA, flags=?FLAG_END_STREAM, stream_id=StreamId }, #data{data=Data}}]; true -> <> = Data, [{#frame_header{ length=MFS, type=?DATA, stream_id=StreamId }, #data{data=ToSend}} | to_frames(StreamId, Rest, S)] end. -spec to_binary(payload()) -> iodata(). to_binary(#data{data=D}) -> D. ================================================ FILE: src/h2_frame_goaway.erl ================================================ -module(h2_frame_goaway). -include("http2.hrl"). -behaviour(h2_frame). -export( [ error_code/1, format/1, new/2, read_binary/2, to_binary/1 ]). -record(goaway, { last_stream_id :: stream_id(), error_code :: error_code(), additional_debug_data = <<>> :: binary() }). -type payload() :: #goaway{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec error_code(payload()) -> error_code(). error_code(#goaway{error_code=EC}) -> EC. -spec format(payload()) -> iodata(). format(Payload) -> io_lib:format("[GOAWAY: ~p]", [Payload]). -spec new(stream_id(), error_code()) -> payload(). new(StreamId, ErrorCode) -> #goaway{ last_stream_id = StreamId, error_code = ErrorCode }. -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(Bin, #frame_header{ length=L, stream_id=0 }) -> <> = Bin, <<_R:1,LastStream:31,ErrorCode:32,Extra/bits>> = Data, Payload = #goaway{ last_stream_id = LastStream, error_code = ErrorCode, additional_debug_data = Extra }, {ok, Payload, Rem}; read_binary(_, _) -> {error, 0, ?PROTOCOL_ERROR, <<>>}. -spec to_binary(payload()) -> iodata(). to_binary(#goaway{ last_stream_id=LSID, error_code=EC, additional_debug_data=ADD }) -> [<<0:1,LSID:31,EC:32>>,ADD]. ================================================ FILE: src/h2_frame_headers.erl ================================================ -module(h2_frame_headers). -include("http2.hrl"). -behaviour(h2_frame). -export( [ format/1, from_frames/1, new/1, new/2, read_binary/2, to_frames/5, to_binary/1 ]). -record(headers, { priority = undefined :: h2_frame_priority:payload() | undefined, block_fragment :: binary() }). -type payload() :: #headers{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec format(payload()) -> iodata(). format(Payload) -> io_lib:format("[Headers: ~p]", [Payload]). -spec new(binary()) -> payload(). new(BlockFragment) -> #headers{block_fragment=BlockFragment}. -spec new(h2_frame_priority:payload(), binary()) -> payload(). new(Priority, BlockFragment) -> #headers{ priority=Priority, block_fragment=BlockFragment }. -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(_, #frame_header{ stream_id=0 }) -> {error, 0, ?PROTOCOL_ERROR, <<>>}; read_binary(Bin, H = #frame_header{length=L}) -> <> = Bin, case h2_padding:read_possibly_padded_payload(PayloadBin, H) of {error, Code} -> {error, 0, Code, Rem}; Data -> {Priority, PSID, HeaderFragment} = case is_priority(H) of true -> {P, PRem} = h2_frame_priority:read_priority(Data), PStream = h2_frame_priority:stream_id(P), {P, PStream, PRem}; false -> {undefined, undefined, Data} end, case PSID =:= H#frame_header.stream_id of true -> {error, PSID, ?PROTOCOL_ERROR, Rem}; false -> Payload = #headers{ priority=Priority, block_fragment=HeaderFragment }, {ok, Payload, Rem} end end. is_priority(#frame_header{flags=F}) when ?IS_FLAG(F, ?FLAG_PRIORITY) -> true; is_priority(_) -> false. -spec to_frames(StreamId :: stream_id(), Headers :: hpack:headers(), EncodeContext :: hpack:context(), MaxFrameSize :: pos_integer(), EndStream :: boolean()) -> {[h2_frame:frame()], hpack:context()}. to_frames(StreamId, Headers, EncodeContext, MaxFrameSize, EndStream) -> {ok, {HeadersBin, NewContext}} = hpack:encode(Headers, EncodeContext), %% Break HeadersBin into chunks Chunks = split(HeadersBin, MaxFrameSize), Frames = build_frames(StreamId, Chunks, EndStream), {Frames, NewContext}. -spec to_binary(payload()) -> iodata(). to_binary(#headers{ priority=P, block_fragment=BF }) -> case P of undefined -> BF; _ -> [h2_frame_priority:to_binary(P), BF] end. -spec from_frames([h2_frame:frame()]) -> binary(). from_frames([{#frame_header{type=?HEADERS},#headers{block_fragment=BF}}|Continuations])-> from_frames(Continuations, BF); from_frames([{#frame_header{type=?PUSH_PROMISE},PP}|Continuations])-> BF = h2_frame_push_promise:block_fragment(PP), from_frames(Continuations, BF). -spec from_frames([h2_frame:frame()], binary()) -> binary(). from_frames([], Acc) -> Acc; from_frames([{#frame_header{type=?CONTINUATION},Cont}|Continuations], Acc) -> BF = h2_frame_continuation:block_fragment(Cont), from_frames(Continuations, <>). -spec split(Binary::binary(), MaxFrameSize::pos_integer()) -> [binary()]. split(Binary, MaxFrameSize) -> split(Binary, MaxFrameSize, []). -spec split(Binary::binary(), MaxFrameSize::pos_integer(), [binary()]) -> [binary()]. split(Binary, MaxFrameSize, Acc) when byte_size(Binary) =< MaxFrameSize -> lists:reverse([Binary|Acc]); split(Binary, MaxFrameSize, Acc) -> <> = Binary, split(Remaining, MaxFrameSize, [NextFrame|Acc]). %% Now build frames. %% The first will be a HEADERS frame, followed by CONTINUATION %% If EndStream, that flag needs to be set on the first frame %% ?FLAG_END_HEADERS needs to be set on the last. %% If there's only one, it needs to be set on both. -spec build_frames(StreamId :: stream_id(), Chunks::[binary()], EndStream::boolean()) -> [h2_frame:frame()]. build_frames(StreamId, [FirstChunk|Rest], EndStream) -> Flag = case EndStream of true -> ?FLAG_END_STREAM; false -> 0 end, HeadersFrame = { #frame_header{ type=?HEADERS, flags=Flag, length=byte_size(FirstChunk), stream_id=StreamId}, #headers{ block_fragment=FirstChunk}}, [{LastFrameHeader, LastFrameBody}|Frames] = build_frames_(StreamId, Rest, [HeadersFrame]), NewLastFrame = { LastFrameHeader#frame_header{ flags=LastFrameHeader#frame_header.flags bor ?FLAG_END_HEADERS }, LastFrameBody}, lists:reverse([NewLastFrame|Frames]). -spec build_frames_(StreamId::stream_id(), Chunks::[binary()], Acc::[h2_frame:frame()])-> [h2_frame:frame()]. build_frames_(_StreamId, [], Acc) -> Acc; build_frames_(StreamId, [NextChunk|Rest], Acc) -> NextFrame = { #frame_header{ stream_id=StreamId, type=?CONTINUATION, flags=0, length=byte_size(NextChunk) }, h2_frame_continuation:new(NextChunk) }, build_frames_(StreamId, Rest, [NextFrame|Acc]). ================================================ FILE: src/h2_frame_ping.erl ================================================ -module(h2_frame_ping). -include("http2.hrl"). -behaviour(h2_frame). -export( [ format/1, read_binary/2, to_binary/1, ack/1, new/1 ]). -record(ping, { opaque_data :: binary() }). -type payload() :: #ping{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec format(payload()) -> iodata(). format(Payload) -> io_lib:format("[Ping: ~p]", [Payload]). -spec new(binary()) -> payload(). new(Bin) -> #ping{opaque_data=Bin}. -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(_, #frame_header{ length=L }) when L =/= 8-> {error, 0, ?FRAME_SIZE_ERROR, <<>>}; read_binary(<>, #frame_header{ length=8, stream_id=0 } ) -> Payload = #ping{ opaque_data = Data }, {ok, Payload, Rem}; read_binary(_, _) -> {error, 0, ?PROTOCOL_ERROR, <<>>}. -spec to_binary(payload()) -> iodata(). to_binary(#ping{opaque_data=D}) -> D. -spec ack(payload()) -> {h2_frame:header(), payload()}. ack(Ping) -> {#frame_header{ length = 8, type = ?PING, flags = ?FLAG_ACK, stream_id = 0 }, Ping}. ================================================ FILE: src/h2_frame_priority.erl ================================================ -module(h2_frame_priority). -include("http2.hrl"). -behaviour(h2_frame). -export( [ format/1, new/3, stream_id/1, read_binary/2, read_priority/1, to_binary/1 ]). -record(priority, { exclusive = 0 :: 0 | 1, stream_id = 0 :: stream_id(), weight = 0 :: non_neg_integer() }). -type payload() :: #priority{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec format(payload()) -> iodata(). format(Payload) -> io_lib:format("[Priority: ~p]", [Payload]). -spec new(0|1, stream_id(), non_neg_integer()) -> payload(). new(Exclusive, StreamId, Weight) -> #priority{ exclusive=Exclusive, stream_id=StreamId, weight=Weight }. -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(_, #frame_header{ stream_id=0 }) -> {error, 0, ?PROTOCOL_ERROR, <<>>}; read_binary(_, #frame_header{ length=L }) when L =/= 5 -> {error, 0, ?FRAME_SIZE_ERROR, <<>>}; read_binary(Bin, #frame_header{ length=5 }=H) -> {Payload, Rem} = read_priority(Bin), PriorityStream = Payload#priority.stream_id, case H#frame_header.stream_id of 0 -> {error, 0, ?PROTOCOL_ERROR, Rem}; PriorityStream -> {error, H#frame_header.stream_id, ?PROTOCOL_ERROR, Rem}; _ -> {ok, Payload, Rem} end. -spec read_priority(binary()) -> {payload(), binary()}. read_priority(Binary) -> <> = Binary, Payload = #priority{ exclusive = Exclusive, stream_id = StreamId, weight = Weight }, {Payload, Rem}. -spec stream_id(payload()) -> stream_id(). stream_id(#priority{stream_id=S}) -> S. -spec to_binary(payload()) -> iodata(). to_binary(#priority{ exclusive=E, stream_id=StreamId, weight=W }) -> <>. ================================================ FILE: src/h2_frame_push_promise.erl ================================================ -module(h2_frame_push_promise). -include("http2.hrl"). -behaviour(h2_frame). -export( [ block_fragment/1, format/1, new/2, promised_stream_id/1, read_binary/2, to_binary/1, to_frame/4 ]). -record(push_promise, { promised_stream_id :: stream_id(), block_fragment :: binary() }). -type payload() :: #push_promise{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec block_fragment(payload()) -> binary(). block_fragment(#push_promise{block_fragment=BF}) -> BF. -spec promised_stream_id(payload()) -> stream_id(). promised_stream_id(#push_promise{promised_stream_id=PSID}) -> PSID. -spec format(payload()) -> iodata(). format(Payload) -> io_lib:format("[Headers: ~p]", [Payload]). -spec new(stream_id(), binary()) -> payload(). new(StreamId, Bin) -> #push_promise{ promised_stream_id=StreamId, block_fragment=Bin }. -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(_, #frame_header{ stream_id=0 }) -> {error, 0, ?PROTOCOL_ERROR, <<>>}; read_binary(Bin, H=#frame_header{length=L}) -> <> = Bin, Data = h2_padding:read_possibly_padded_payload(PayloadBin, H), <<_R:1,Stream:31,BlockFragment/bits>> = Data, Payload = #push_promise{ promised_stream_id=Stream, block_fragment=BlockFragment }, {ok, Payload, Rem}. -spec to_frame(pos_integer(), pos_integer(), hpack:headers(), hpack:context()) -> {{h2_frame:header(), payload()}, hpack:context()}. %% Maybe break this up into continuations like the data frame to_frame(StreamId, PStreamId, Headers, EncodeContext) -> {ok, {HeadersToSend, NewContext}} = hpack:encode(Headers, EncodeContext), L = byte_size(HeadersToSend), {{#frame_header{ length=L, type=?PUSH_PROMISE, flags=?FLAG_END_HEADERS, stream_id=StreamId }, #push_promise{ promised_stream_id=PStreamId, block_fragment=HeadersToSend }}, NewContext}. -spec to_binary(payload()) -> iodata(). to_binary(#push_promise{ promised_stream_id=PSID, block_fragment=BF }) -> %% TODO: allow for padding as per HTTP/2 SPEC <<0:1,PSID:31,BF/binary>>. ================================================ FILE: src/h2_frame_rst_stream.erl ================================================ -module(h2_frame_rst_stream). -include("http2.hrl"). -behaviour(h2_frame). -export([ new/1, error_code/1, format/1, read_binary/2, to_binary/1 ]). -record(rst_stream, { error_code :: error_code() }). -type payload() :: #rst_stream{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec new(error_code()) -> payload(). new(ErrorCode) -> #rst_stream{ error_code=ErrorCode }. -spec error_code(payload()) -> error_code(). error_code(#rst_stream{error_code=EC}) -> EC. -spec format(payload()) -> iodata(). format(Payload) -> io_lib:format("[RST Stream: ~p]", [Payload]). -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(_, #frame_header{ stream_id=0 }) -> {error, 0, ?PROTOCOL_ERROR, <<>>}; read_binary(_, #frame_header{ length=L }) when L =/= 4 -> {error, 0, ?FRAME_SIZE_ERROR, <<>>}; read_binary(<>, #frame_header{length=4}) -> Payload = #rst_stream{ error_code = ErrorCode }, {ok, Payload, Rem}. -spec to_binary(payload()) -> iodata(). to_binary(#rst_stream{error_code=C}) -> <>. ================================================ FILE: src/h2_frame_settings.erl ================================================ -module(h2_frame_settings). -include("http2.hrl"). -behaviour(h2_frame). -export( [ format/1, read_binary/2, send/1, send/2, ack/0, ack/1, to_binary/1, overlay/2, validate/1 ]). %%TODO -type payload() :: #settings{} | {settings, proplist()}. -type frame() :: {h2_frame:header(), payload()}. -type name() :: binary(). -type property() :: {name(), any()}. -type proplist() :: [property()]. -export_type([payload/0, name/0, property/0, proplist/0, frame/0]). -spec format(payload() | binary() | {settings, [proplists:property()]} ) -> iodata(). format(<<>>) -> "Ack!"; format(#settings{ header_table_size = HTS, enable_push = EP, max_concurrent_streams = MCS, initial_window_size = IWS, max_frame_size = MFS, max_header_list_size = MHLS }) -> lists:flatten( io_lib:format("[Settings: " " header_table_size = ~p," " enable_push = ~p," " max_concurrent_streams = ~p," " initial_window_size = ~p," " max_frame_size = ~p," " max_header_list_size = ~p~n]", [HTS,EP,MCS,IWS,MFS,MHLS])); format({settings, PList}) -> L = lists:map(fun({?SETTINGS_HEADER_TABLE_SIZE,V}) -> {header_table_size,V}; ({?SETTINGS_ENABLE_PUSH,V}) -> {enable_push,V}; ({?SETTINGS_MAX_CONCURRENT_STREAMS,V}) -> {max_concurrent_streams,V}; ({?SETTINGS_INITIAL_WINDOW_SIZE,V}) -> {initial_window_size, V}; ({?SETTINGS_MAX_FRAME_SIZE,V}) -> {max_frame_size,V}; ({?SETTINGS_MAX_HEADER_LIST_SIZE,V}) -> {max_header_list_size,V} end, PList), io_lib:format("~p", [L]). -spec read_binary(binary(), h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(Bin, #frame_header{ length=0, stream_id=0 }) -> {ok, {settings, []}, Bin}; read_binary(Bin, #frame_header{ length=Length, stream_id=0 }) -> <> = Bin, Settings = parse_settings(SettingsBin), {ok, {settings, Settings}, Rem}; read_binary(_, _) -> {error, 0, ?PROTOCOL_ERROR, <<>>}. -spec parse_settings(binary()) -> [proplists:property()]. parse_settings(Bin) -> lists:reverse(parse_settings(Bin, [])). -spec parse_settings(binary(), [proplists:property()]) -> [proplists:property()]. parse_settings(<<0,1,Val:4/binary,T/binary>>, S) -> parse_settings(T, [{?SETTINGS_HEADER_TABLE_SIZE, binary:decode_unsigned(Val)}|S]); parse_settings(<<0,2,Val:4/binary,T/binary>>, S) -> parse_settings(T, [{?SETTINGS_ENABLE_PUSH, binary:decode_unsigned(Val)}|S]); parse_settings(<<0,3,Val:4/binary,T/binary>>, S) -> parse_settings(T, [{?SETTINGS_MAX_CONCURRENT_STREAMS, binary:decode_unsigned(Val)}|S]); parse_settings(<<0,4,Val:4/binary,T/binary>>, S) -> parse_settings(T, [{?SETTINGS_INITIAL_WINDOW_SIZE, binary:decode_unsigned(Val)}|S]); parse_settings(<<0,5,Val:4/binary,T/binary>>, S) -> parse_settings(T, [{?SETTINGS_MAX_FRAME_SIZE, binary:decode_unsigned(Val)}|S]); parse_settings(<<0,6,Val:4/binary,T/binary>>, S)-> parse_settings(T, [{?SETTINGS_MAX_HEADER_LIST_SIZE, binary:decode_unsigned(Val)}|S]); % An endpoint that receives a SETTINGS frame with any unknown or unsupported identifier % MUST ignore that setting parse_settings(<<_:6/binary,T/binary>>, S)-> parse_settings(T, S); parse_settings(<<>>, Settings) -> Settings. -spec overlay(payload(), {settings, [proplists:property()]}) -> payload(). overlay(S, Setting) -> overlay_(S, S, Setting). overlay_(OriginalS, S, {settings, [{?SETTINGS_HEADER_TABLE_SIZE, Val}|PList]}) -> overlay_(OriginalS, S#settings{header_table_size=Val}, {settings, PList}); overlay_(OriginalS, S, {settings, [{?SETTINGS_ENABLE_PUSH, Val}|PList]}) -> overlay_(OriginalS, S#settings{enable_push=Val}, {settings, PList}); overlay_(OriginalS, S, {settings, [{?SETTINGS_MAX_CONCURRENT_STREAMS, Val}|PList]}) -> overlay_(OriginalS, S#settings{max_concurrent_streams=Val}, {settings, PList}); overlay_(OriginalS, S, {settings, [{?SETTINGS_INITIAL_WINDOW_SIZE, Val}|PList]}) -> overlay_(OriginalS, S#settings{initial_window_size=Val}, {settings, PList}); overlay_(OriginalS, S, {settings, [{?SETTINGS_MAX_FRAME_SIZE, Val}|PList]}) -> overlay_(OriginalS, S#settings{max_frame_size=Val}, {settings, PList}); overlay_(OriginalS, S, {settings, [{?SETTINGS_MAX_HEADER_LIST_SIZE, Val}|PList]}) -> overlay_(OriginalS, S#settings{max_header_list_size=Val}, {settings, PList}); overlay_(OriginalS, _S, {settings, [{_UnknownOrUnsupportedKey, _Val}|_]}) -> OriginalS; overlay_(_OriginalS, S, {settings, []}) -> S. -spec send(payload()) -> binary(). send(Settings) -> List = h2_settings:to_proplist(Settings), Payload = make_payload(List), L = size(Payload), Header = <>, <
>. -spec send(payload(), payload()) -> binary(). send(PrevSettings, NewSettings) -> Diff = h2_settings:diff(PrevSettings, NewSettings), Payload = make_payload(Diff), L = size(Payload), Header = <>, <
>. -spec make_payload(proplist()) -> binary(). make_payload(Diff) -> make_payload_(lists:reverse(Diff), <<>>). make_payload_([], BinAcc) -> BinAcc; make_payload_([{_, unlimited}|Tail], BinAcc) -> make_payload_(Tail, BinAcc); make_payload_([{<>, Value}|Tail], BinAcc) -> make_payload_(Tail, <>). -spec ack() -> binary(). ack() -> <<0:24,4:8,1:8,0:1,0:31>>. -spec ack(socket()) -> ok | {error, term()}. ack({Transport,Socket}) -> Transport:send(Socket, <<0:24,4:8,1:8,0:1,0:31>>). -spec to_binary(payload()) -> iodata(). to_binary(#settings{}=Settings) -> [to_binary(S, Settings) || S <- ?SETTING_NAMES]. -spec to_binary(binary(), payload()) -> binary(). to_binary(?SETTINGS_HEADER_TABLE_SIZE, #settings{header_table_size=undefined}) -> <<>>; to_binary(?SETTINGS_HEADER_TABLE_SIZE, #settings{header_table_size=HTS}) -> <<16#1:16,HTS:32>>; to_binary(?SETTINGS_ENABLE_PUSH, #settings{enable_push=undefined}) -> <<>>; to_binary(?SETTINGS_ENABLE_PUSH, #settings{enable_push=EP}) -> <<16#2:16,EP:32>>; to_binary(?SETTINGS_MAX_CONCURRENT_STREAMS, #settings{max_concurrent_streams=undefined}) -> <<>>; to_binary(?SETTINGS_MAX_CONCURRENT_STREAMS, #settings{max_concurrent_streams=MCS}) -> <<16#3:16,MCS:32>>; to_binary(?SETTINGS_INITIAL_WINDOW_SIZE, #settings{initial_window_size=undefined}) -> <<>>; to_binary(?SETTINGS_INITIAL_WINDOW_SIZE, #settings{initial_window_size=IWS}) -> <<16#4:16,IWS:32>>; to_binary(?SETTINGS_MAX_FRAME_SIZE, #settings{max_frame_size=undefined}) -> <<>>; to_binary(?SETTINGS_MAX_FRAME_SIZE, #settings{max_frame_size=MFS}) -> <<16#5:16,MFS:32>>; to_binary(?SETTINGS_MAX_HEADER_LIST_SIZE, #settings{max_header_list_size=undefined}) -> <<>>; to_binary(?SETTINGS_MAX_HEADER_LIST_SIZE, #settings{max_header_list_size=MHLS}) -> <<16#6:16,MHLS:32>>. -spec validate({settings, [proplists:property()]}) -> ok | {error, integer()}. validate({settings, PList}) -> validate_(PList). validate_([]) -> ok; validate_([{?SETTINGS_ENABLE_PUSH, Push}|_T]) when Push > 1; Push < 0 -> {error, ?PROTOCOL_ERROR}; validate_([{?SETTINGS_INITIAL_WINDOW_SIZE, Size}|_T]) when Size >=2147483648 -> {error, ?FLOW_CONTROL_ERROR}; validate_([{?SETTINGS_MAX_FRAME_SIZE, Size}|_T]) when Size < 16384; Size > 16777215 -> {error, ?PROTOCOL_ERROR}; validate_([_H|T]) -> validate_(T). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). make_payload_test() -> Diff = [ {?SETTINGS_MAX_CONCURRENT_STREAMS, 2}, {?SETTINGS_MAX_FRAME_SIZE, 2048} ], Bin = make_payload(Diff), <> = ?SETTINGS_MAX_CONCURRENT_STREAMS, <> = ?SETTINGS_MAX_FRAME_SIZE, ?assertEqual(<>, Bin), ok. validate_test() -> ?assertEqual(ok, validate_([])), ?assertEqual(ok, validate_([{?SETTINGS_ENABLE_PUSH, 0}])), ?assertEqual(ok, validate_([{?SETTINGS_ENABLE_PUSH, 1}])), ?assertEqual({error, ?PROTOCOL_ERROR}, validate_([{?SETTINGS_ENABLE_PUSH, 2}])), ?assertEqual({error, ?PROTOCOL_ERROR}, validate_([{?SETTINGS_ENABLE_PUSH, -1}])), ?assertEqual({error, ?FLOW_CONTROL_ERROR}, validate_([{?SETTINGS_INITIAL_WINDOW_SIZE, 2147483648}])), ?assertEqual(ok, validate_([{?SETTINGS_INITIAL_WINDOW_SIZE, 2147483647}])), ?assertEqual({error, ?PROTOCOL_ERROR}, validate_([{?SETTINGS_MAX_FRAME_SIZE, 16383}])), ?assertEqual(ok, validate_([{?SETTINGS_MAX_FRAME_SIZE, 16384}])), ?assertEqual(ok, validate_([{?SETTINGS_MAX_FRAME_SIZE, 16777215}])), ?assertEqual({error, ?PROTOCOL_ERROR}, validate_([{?SETTINGS_MAX_FRAME_SIZE, 16777216}])), ok. -endif. ================================================ FILE: src/h2_frame_window_update.erl ================================================ -module(h2_frame_window_update). -include("http2.hrl"). -behaviour(h2_frame). -export( [ new/1, format/1, read_binary/2, send/3, size_increment/1, to_binary/1 ]). -record(window_update, { window_size_increment :: non_neg_integer() }). -type payload() :: #window_update{}. -type frame() :: {h2_frame:header(), payload()}. -export_type([payload/0, frame/0]). -spec format(payload()) -> iodata(). format(Payload) -> io_lib:format("[Window Update: ~p]", [Payload]). -spec new(non_neg_integer()) -> payload(). new(Increment) -> #window_update{window_size_increment=Increment}. -spec read_binary(Bin::binary(), Header::h2_frame:header()) -> {ok, payload(), binary()} | {error, stream_id(), error_code(), binary()}. read_binary(_, #frame_header{ length=L }) when L =/= 4 -> {error, 0, ?FRAME_SIZE_ERROR, <<>>}; read_binary(<<_R:1,0:31,Rem/bits>>, FH) -> {error, FH#frame_header.stream_id, ?PROTOCOL_ERROR, Rem}; read_binary(Bin, #frame_header{length=4}) -> <<_R:1,Increment:31,Rem/bits>> = Bin, Payload = #window_update{ window_size_increment=Increment }, {ok, Payload, Rem}; read_binary(_, #frame_header{}=H) -> %TODO: Maybe return some Rem in element(4) once we know what that %is {error, H#frame_header.stream_id, ?FRAME_SIZE_ERROR, <<>>}. -spec send(sock:socket(), non_neg_integer(), stream_id()) -> ok. send(Socket, WindowSizeIncrement, StreamId) -> sock:send(Socket, [ <<4:24,?WINDOW_UPDATE:8,0:8,0:1,StreamId:31>>, <<0:1,WindowSizeIncrement:31>>]). -spec size_increment(payload()) -> non_neg_integer(). size_increment(#window_update{window_size_increment=WSI}) -> WSI. -spec to_binary(payload()) -> iodata(). to_binary(#window_update{ window_size_increment=I }) -> <<0:1,I:31>>. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). read_binary_zero_test() -> ?assertEqual({error, 0, ?PROTOCOL_ERROR, <<>>}, read_binary(<<0:1,0:31>>, #frame_header{stream_id=0,length=4})), ?assertEqual({error, 2, ?PROTOCOL_ERROR, <<>>}, read_binary(<<1:1,0:31>>, #frame_header{stream_id=2,length=4})), ok. -endif. ================================================ FILE: src/h2_padding.erl ================================================ -module(h2_padding). -include("http2.hrl"). -export([ is_padded/1, read_possibly_padded_payload/2 ]). -spec is_padded(h2_frame:header()) -> boolean(). is_padded(#frame_header{flags=Flags}) when ?IS_FLAG(Flags, ?FLAG_PADDED) -> true; is_padded(_) -> false. -spec read_possibly_padded_payload(binary(), h2_frame:header()) -> binary() | {error, error_code()}. read_possibly_padded_payload(Bin, H=#frame_header{flags=F}) when ?IS_FLAG(F, ?FLAG_PADDED) -> read_padded_payload(Bin, H); read_possibly_padded_payload(Bin, Header) -> read_unpadded_payload(Bin, Header). -spec read_padded_payload(binary(), h2_frame:header()) -> binary() | {error, error_code()}. read_padded_payload(<>, #frame_header{length=Length}) -> L = Length - Padding - 1, % Exclude Pad length field (1 byte) case L >= 0 of true -> <> = Bytes, Data; false -> {error, ?PROTOCOL_ERROR} end. -spec read_unpadded_payload(binary(), h2_frame:header()) -> binary(). read_unpadded_payload(Data, _H) -> Data. ================================================ FILE: src/h2_settings.erl ================================================ -module(h2_settings). -include("http2.hrl"). -export([ diff/2, to_proplist/1 ]). -spec diff(settings(), settings()) -> settings_proplist(). diff(OldSettings, NewSettings) -> OldPl = to_proplist(OldSettings), NewPl = to_proplist(NewSettings), diff_(OldPl, NewPl, []). diff_([],[],Acc) -> lists:reverse(Acc); diff_([OldH|OldT],[OldH|NewT],Acc) -> diff_(OldT, NewT, Acc); diff_([_OldH|OldT],[NewH|NewT],Acc) -> diff_(OldT, NewT, [NewH|Acc]). -spec to_proplist(settings()) -> settings_proplist(). to_proplist(Settings) -> [ {?SETTINGS_HEADER_TABLE_SIZE, Settings#settings.header_table_size }, {?SETTINGS_ENABLE_PUSH, Settings#settings.enable_push }, {?SETTINGS_MAX_CONCURRENT_STREAMS, Settings#settings.max_concurrent_streams}, {?SETTINGS_INITIAL_WINDOW_SIZE, Settings#settings.initial_window_size }, {?SETTINGS_MAX_FRAME_SIZE, Settings#settings.max_frame_size }, {?SETTINGS_MAX_HEADER_LIST_SIZE, Settings#settings.max_header_list_size } ]. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff_test() -> Old = #settings{}, New = #settings{ max_frame_size=2048 }, Diff = diff(Old, New), ?assertEqual([{?SETTINGS_MAX_FRAME_SIZE, 2048}], Diff), ok. diff_order_test() -> Old = #settings{}, New = #settings{ max_frame_size = 2048, max_concurrent_streams = 2 }, Diff = diff(Old, New), ?assertEqual( [{?SETTINGS_MAX_CONCURRENT_STREAMS, 2}, {?SETTINGS_MAX_FRAME_SIZE, 2048}], Diff ), ?assertNotEqual( [ {?SETTINGS_MAX_FRAME_SIZE, 2048}, {?SETTINGS_MAX_CONCURRENT_STREAMS, 2} ], Diff ), ok. -endif. ================================================ FILE: src/h2_stream.erl ================================================ -module(h2_stream). -include("http2.hrl"). %% Public API -export([ start_link/5, send_event/2, send_pp/2, send_data/2, stream_id/0, call/2, connection/0, send_window_update/1, send_connection_window_update/1, rst_stream/2, stop/1 ]). %% gen_statem callbacks -behaviour(gen_statem). -export([init/1, callback_mode/0, terminate/3, code_change/4]). %% gen_statem states -export([ idle/3, reserved_local/3, reserved_remote/3, open/3, half_closed_local/3, half_closed_remote/3, closed/3 ]). -type stream_state_name() :: 'idle' | 'open' | 'closed' | 'reserved_local' | 'reserved_remote' | 'half_closed_local' | 'half_closed_remote'. -record(stream_state, { stream_id = undefined :: stream_id(), connection = undefined :: undefined | pid(), socket = undefined :: sock:socket(), state = idle :: stream_state_name(), incoming_frames = queue:new() :: queue:queue(h2_frame:frame()), request_headers = [] :: hpack:headers(), request_body :: iodata() | undefined, request_body_size = 0 :: non_neg_integer(), request_end_stream = false :: boolean(), request_end_headers = false :: boolean(), response_headers = [] :: hpack:headers(), response_trailers = [] :: hpack:headers(), response_body :: iodata() | undefined, response_end_headers = false :: boolean(), response_end_stream = false :: boolean(), next_state = undefined :: undefined | stream_state_name(), promised_stream = undefined :: undefined | state(), callback_state = undefined :: any(), callback_mod = undefined :: module() }). -type state() :: #stream_state{}. -type callback_state() :: any(). -export_type([state/0, callback_state/0]). -callback init( Conn :: pid(), StreamId :: stream_id(), CallbackOptions :: list() ) -> {ok, callback_state()}. -callback on_receive_request_headers( Headers :: hpack:headers(), CallbackState :: callback_state()) -> {ok, NewState :: callback_state()}. -callback on_send_push_promise( Headers :: hpack:headers(), CallbackState :: callback_state()) -> {ok, NewState :: callback_state()}. -callback on_receive_request_data( iodata(), CallbackState :: callback_state())-> {ok, NewState :: callback_state()}. -callback on_request_end_stream( CallbackState :: callback_state()) -> {ok, NewState :: callback_state()}. %% Public API -spec start_link( StreamId :: stream_id(), Connection :: pid(), CallbackModule :: module(), CallbackOptions :: list(), Socket :: sock:socket() ) -> {ok, pid()} | ignore | {error, term()}. start_link(StreamId, Connection, CallbackModule, CallbackOptions, Socket) -> gen_statem:start_link(?MODULE, [StreamId, Connection, CallbackModule, CallbackOptions, Socket], []). send_event(Pid, Event) -> gen_statem:cast(Pid, Event). -spec send_pp(pid(), hpack:headers()) -> ok. send_pp(Pid, Headers) -> gen_statem:cast(Pid, {send_pp, Headers}). -spec send_data(pid(), h2_frame_data:frame()) -> ok | flow_control. send_data(Pid, Frame) -> gen_statem:cast(Pid, {send_data, Frame}). -spec stream_id() -> stream_id(). stream_id() -> gen_statem:call(self(), stream_id). call(Pid, Msg) -> gen_statem:call(Pid, Msg). -spec connection() -> pid(). connection() -> gen_statem:call(self(), connection). -spec send_window_update(non_neg_integer()) -> ok. send_window_update(Size) -> gen_statem:cast(self(), {send_window_update, Size}). -spec send_connection_window_update(non_neg_integer()) -> ok. send_connection_window_update(Size) -> gen_statem:cast(self(), {send_connection_window_update, Size}). rst_stream(Pid, Code) -> gen_statem:call(Pid, {rst_stream, Code}). -spec stop(pid()) -> ok. stop(Pid) -> gen_statem:stop(Pid). init([ StreamId, ConnectionPid, CB, CBOptions, Socket ]) -> %% TODO: Check for CB implementing this behaviour {ok, CallbackState} = CB:init(ConnectionPid, StreamId, [Socket | CBOptions]), {ok, idle, #stream_state{ callback_mod=CB, socket=Socket, stream_id=StreamId, connection=ConnectionPid, callback_state=CallbackState }}. callback_mode() -> state_functions. %% IMPORTANT: If we're in an idle state, we can only send/receive %% HEADERS frames. The diagram in the spec wants you believe that you %% can send or receive PUSH_PROMISES too, but that's a LIE. What you %% can do is send PPs from the open or half_closed_remote state, or %% receive them in the open or half_closed_local state. Then, that %% will create a new stream in the idle state and THAT stream can %% transition to one of the reserved states, but you'll never get a %% PUSH_PROMISE frame with that Stream Id. It's a subtle thing, but it %% drove me crazy until I figured it out %% Server 'RECV H' idle(cast, {recv_h, Headers}, #stream_state{ callback_mod=CB, callback_state=CallbackState }=Stream) -> case is_valid_headers(request, Headers) of ok -> {ok, NewCBState} = CB:on_receive_request_headers(Headers, CallbackState), {next_state, open, Stream#stream_state{ request_headers=Headers, callback_state=NewCBState }}; {error, Code} -> rst_stream_(Code, Stream) end; %% Server 'SEND PP' idle(cast, {send_pp, Headers}, #stream_state{ callback_mod=CB, callback_state=CallbackState }=Stream) -> {ok, NewCBState} = CB:on_send_push_promise(Headers, CallbackState), {next_state, reserved_local, Stream#stream_state{ request_headers=Headers, callback_state=NewCBState }, 0}; %% zero timeout lets us start dealing with reserved local, %% because there is no END_STREAM event %% Client 'RECV PP' idle(cast, {recv_pp, Headers}, #stream_state{ }=Stream) -> {next_state, reserved_remote, Stream#stream_state{ request_headers=Headers }}; %% Client 'SEND H' idle(cast, {send_h, Headers}, #stream_state{ }=Stream) -> {next_state, open, Stream#stream_state{ request_headers=Headers }}; idle(Type, Event, State) -> handle_event(Type, Event, State). reserved_local(timeout, _, #stream_state{ callback_state=CallbackState, callback_mod=CB }=Stream) -> check_content_length(Stream), {ok, NewCBState} = CB:on_request_end_stream(CallbackState), {next_state, reserved_local, Stream#stream_state{ callback_state=NewCBState }}; reserved_local(cast, {send_h, Headers}, #stream_state{ }=Stream) -> {next_state, half_closed_remote, Stream#stream_state{ response_headers=Headers }}; reserved_local(cast, {send_t, Headers}, #stream_state{ }=Stream) -> {next_state, half_closed_remote, Stream#stream_state{ response_trailers=Headers }}; reserved_local(Type, Event, State) -> handle_event(Type, Event, State). reserved_remote(cast, {recv_h, Headers}, #stream_state{ }=Stream) -> {next_state, half_closed_local, Stream#stream_state{ response_headers=Headers }}; reserved_remote(cast, {recv_t, Headers}, #stream_state{ }=Stream) -> {next_state, half_closed_local, Stream#stream_state{ response_headers=Headers }}; reserved_remote(Type, Event, State) -> handle_event(Type, Event, State). open(cast, recv_es, #stream_state{ callback_mod=CB, callback_state=CallbackState }=Stream) -> case check_content_length(Stream) of ok -> {ok, NewCBState} = CB:on_request_end_stream(CallbackState), {next_state, half_closed_remote, Stream#stream_state{ callback_state=NewCBState }}; rst_stream -> {next_state, closed, Stream} end; open(cast, {recv_data, {#frame_header{ flags=Flags, length=L, type=?DATA }, Payload}=F}, #stream_state{ incoming_frames=IFQ, callback_mod=CB, callback_state=CallbackState }=Stream) when ?NOT_FLAG(Flags, ?FLAG_END_STREAM) -> Bin = h2_frame_data:data(Payload), {ok, NewCBState} = CB:on_receive_request_data(Bin, CallbackState), {next_state, open, Stream#stream_state{ %% TODO: We're storing everything in the state. It's fine for %% some cases, but the decision should be left to the user incoming_frames=queue:in(F, IFQ), request_body_size=Stream#stream_state.request_body_size+L, callback_state=NewCBState }}; open(cast, {recv_data, {#frame_header{ flags=Flags, length=L, type=?DATA }, Payload}=F}, #stream_state{ incoming_frames=IFQ, callback_mod=CB, callback_state=CallbackState }=Stream) when ?IS_FLAG(Flags, ?FLAG_END_STREAM) -> Bin = h2_frame_data:data(Payload), {ok, CallbackState1} = CB:on_receive_request_data(Bin, CallbackState), NewStream = Stream#stream_state{ incoming_frames=queue:in(F, IFQ), request_body_size=Stream#stream_state.request_body_size+L, request_end_stream=true, callback_state=CallbackState1 }, case check_content_length(NewStream) of ok -> {ok, NewCBState} = CB:on_request_end_stream(CallbackState1), {next_state, half_closed_remote, NewStream#stream_state{ callback_state=NewCBState }}; rst_stream -> {next_state, closed, NewStream} end; %% Trailers open(cast, {recv_h, Trailers}, #stream_state{}=Stream) -> case is_valid_headers(request, Trailers) of ok -> {next_state, open, Stream#stream_state{ request_headers=Stream#stream_state.request_headers ++ Trailers }}; {error, Code} -> rst_stream_(Code, Stream) end; open(cast, {send_data, {#frame_header{ type=?HEADERS, flags=Flags }, _}=F}, #stream_state{ socket=Socket }=Stream) -> sock:send(Socket, h2_frame:to_binary(F)), NextState = case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of true -> half_closed_local; _ -> open end, {next_state, NextState, Stream}; open(cast, {send_data, {#frame_header{ type=?DATA, flags=Flags }, _}=F}, #stream_state{ socket=Socket }=Stream) -> sock:send(Socket, h2_frame:to_binary(F)), NextState = case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of true -> half_closed_local; _ -> open end, {next_state, NextState, Stream}; open(cast, {send_h, Headers}, #stream_state{}=Stream) -> {next_state, open, Stream#stream_state{ response_headers=Headers }}; open(cast, {send_t, Headers}, #stream_state{}=Stream) -> {next_state, half_closed_local, Stream#stream_state{ response_trailers=Headers }}; open(Type, Event, State) -> handle_event(Type, Event, State). half_closed_remote(cast, {send_h, Headers}, #stream_state{}=Stream) -> {next_state, half_closed_remote, Stream#stream_state{ response_headers=Headers }}; half_closed_remote(cast, {send_t, Headers}, #stream_state{}=Stream) -> {next_state, half_closed_remote, Stream#stream_state{ response_trailers=Headers }}; half_closed_remote(cast, {send_data, { #frame_header{ flags=Flags, type=?DATA },_ }=F}=_Msg, #stream_state{ socket=Socket }=Stream) -> case sock:send(Socket, h2_frame:to_binary(F)) of ok -> case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of true -> {next_state, closed, Stream, 0}; _ -> {next_state, half_closed_remote, Stream} end; {error,_} -> {next_state, closed, Stream, 0} end; half_closed_remote(cast, {send_data, { #frame_header{ flags=Flags, type=?HEADERS },_ }=F}=_Msg, #stream_state{ socket=Socket }=Stream) -> case sock:send(Socket, h2_frame:to_binary(F)) of ok -> case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of true -> {next_state, closed, Stream, 0}; _ -> {next_state, half_closed_remote, Stream} end; {error,_} -> {next_state, closed, Stream, 0} end; half_closed_remote(cast, _, #stream_state{}=Stream) -> rst_stream_(?STREAM_CLOSED, Stream); half_closed_remote(Type, Event, State) -> handle_event(Type, Event, State). %% PUSH_PROMISES can only be received by streams in the open or %% half_closed_local, but will create a new stream in the idle state, %% but that stream may be ready to transition, it'll make sense, I %% hope! half_closed_local(cast, {recv_h, Headers}, #stream_state{}=Stream) -> case is_valid_headers(response, Headers) of ok -> {next_state, half_closed_local, Stream#stream_state{ response_headers=Headers}}; {error, Code} -> rst_stream_(Code, Stream) end; half_closed_local(cast, {recv_data, {#frame_header{ flags=Flags, type=?DATA },_}=F}, #stream_state{ incoming_frames=IFQ } = Stream) -> NewQ = queue:in(F, IFQ), case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of true -> Data = [h2_frame_data:data(Payload) || {#frame_header{type=?DATA}, Payload} <- queue:to_list(NewQ)], {next_state, closed, Stream#stream_state{ incoming_frames=queue:new(), response_body = Data }, 0}; _ -> {next_state, half_closed_local, Stream#stream_state{ incoming_frames=NewQ }} end; half_closed_local(cast, recv_es, #stream_state{ response_body = undefined, incoming_frames = Q } = Stream) -> Data = [h2_frame_data:data(Payload) || {#frame_header{type=?DATA}, Payload} <- queue:to_list(Q)], {next_state, closed, Stream#stream_state{ incoming_frames=queue:new(), response_body = Data }, 0}; half_closed_local(cast, recv_es, #stream_state{ response_body = Data } = Stream) -> {next_state, closed, Stream#stream_state{ incoming_frames=queue:new(), response_body = Data }, 0}; half_closed_local(_, _, #stream_state{}=Stream) -> rst_stream_(?STREAM_CLOSED, Stream); half_closed_local(Type, Event, State) -> handle_event(Type, Event, State). closed(timeout, _, #stream_state{}=Stream) -> gen_statem:cast(Stream#stream_state.connection, {stream_finished, Stream#stream_state.stream_id, Stream#stream_state.response_headers, Stream#stream_state.response_body}), {stop, normal, Stream}; closed(_, _, #stream_state{}=Stream) -> rst_stream_(?STREAM_CLOSED, Stream); closed(Type, Event, State) -> handle_event(Type, Event, State). handle_event(_, {send_window_update, 0}, #stream_state{}=Stream) -> {keep_state, Stream}; handle_event(_, {send_window_update, Size}, #stream_state{ socket=Socket, stream_id=StreamId }=Stream) -> h2_frame_window_update:send(Socket, Size, StreamId), {keep_state, Stream#stream_state{}}; handle_event(_, {send_connection_window_update, Size}, #stream_state{ connection=ConnPid }=State) -> h2_connection:send_window_update(ConnPid, Size), {keep_state, State}; handle_event({call, From}, {rst_stream, ErrorCode}, State=#stream_state{}) -> {keep_state, State, [{reply, From, {ok, rst_stream_(ErrorCode, State)}}]}; handle_event({call, From}, stream_id, State=#stream_state{stream_id=StreamId}) -> {keep_state, State, [{reply, From, StreamId}]}; handle_event({call, From}, connection, State=#stream_state{connection=Conn}) -> {keep_state, State, [{reply, From, Conn}]}; handle_event({call, From}, Event, State=#stream_state{callback_mod=CB, callback_state=CallbackState}) -> {ok, Reply, CallbackState1} = CB:handle_call(Event, CallbackState), {keep_state, State#stream_state{callback_state=CallbackState1}, [{reply, From, Reply}]}; handle_event(cast, Event, State=#stream_state{callback_mod=CB, callback_state=CallbackState}) -> CallbackState1 = CB:handle_info(Event, CallbackState), {keep_state, State#stream_state{callback_state=CallbackState1}}; handle_event(info, Event, State=#stream_state{callback_mod=CB, callback_state=CallbackState}) -> CallbackState1 = CB:handle_info(Event, CallbackState), {keep_state, State#stream_state{callback_state=CallbackState1}}; handle_event(_, _Event, State) -> {keep_state, State}. code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. terminate(normal, _StateName, _State) -> ok; terminate(_Reason, _StateName, _State) -> ok. -spec rst_stream_(error_code(), state()) -> {next_state, closed, state(), timeout()}. rst_stream_(ErrorCode, #stream_state{ socket=Socket, stream_id=StreamId }=Stream ) -> RstStream = h2_frame_rst_stream:new(ErrorCode), RstStreamBin = h2_frame:to_binary( {#frame_header{ stream_id=StreamId }, RstStream}), sock:send(Socket, RstStreamBin), {next_state, closed, Stream, 0}. check_content_length(Stream) -> ContentLength = proplists:get_value(<<"content-length">>, Stream#stream_state.request_headers), case ContentLength of undefined -> ok; _Other -> try binary_to_integer(ContentLength) of Integer -> case Stream#stream_state.request_body_size =:= Integer of true -> ok; false -> rst_stream_(?PROTOCOL_ERROR, Stream), rst_stream end catch _:_ -> rst_stream_(?PROTOCOL_ERROR, Stream), rst_stream end end. %%% Moving header validation into streams %% Function checks if a set of headers is valid. Currently that means: %% %% * The list of acceptable pseudoheaders for requests are: %% :method, :scheme, :authority, :path, %% * The only acceptable pseudoheader for responses is :status %% * All header names are lowercase. %% * All pseudoheaders occur before normal headers. %% * No pseudoheaders are duplicated -spec is_valid_headers( request | response, hpack:headers() ) -> ok | {error, term()}. is_valid_headers(Type, Headers) -> case validate_pseudos(Type, Headers) of true -> ok; false -> {error, ?PROTOCOL_ERROR} end. no_upper_names(Headers) -> lists:all( fun({Name,_}) -> NameStr = binary_to_list(Name), NameStr =:= string:to_lower(NameStr) end, Headers). validate_pseudos(Type, Headers) -> validate_pseudos(Type, Headers, #{}). validate_pseudos(request, [{<<":path">>,_V}|_Tail], #{<<":path">> := true }) -> false; validate_pseudos(request, [{<<":path">>,_V}|Tail], Found) -> validate_pseudos(request, Tail, Found#{<<":path">> => true}); validate_pseudos(request, [{<<":method">>,_V}|_Tail], #{<<":method">> := true }) -> false; validate_pseudos(request, [{<<":method">>,_V}|Tail], Found) -> validate_pseudos(request, Tail, Found#{<<":method">> => true}); validate_pseudos(request, [{<<":scheme">>,_V}|_Tail], #{<<":scheme">> := true }) -> false; validate_pseudos(request, [{<<":scheme">>,_V}|Tail], Found) -> validate_pseudos(request, Tail, Found#{<<":scheme">> => true}); validate_pseudos(request, [{<<":authority">>,_V}|_Tail], #{<<":authority">> := true }) -> false; validate_pseudos(request, [{<<":authority">>,_V}|Tail], Found) -> validate_pseudos(request, Tail, Found#{<<":authority">> => true}); validate_pseudos(response, [{<<":status">>,_V}|_Tail], #{<<":status">> := true }) -> false; validate_pseudos(response, [{<<":status">>,_V}|Tail], Found) -> validate_pseudos(response, Tail, Found#{<<":status">> => true}); validate_pseudos(_, DoneWithPseudos, _Found) -> lists:all( fun({<<$:, _/binary>>, _}) -> false; ({<<"connection">>, _}) -> false; ({<<"te">>, <<"trailers">>}) -> true; ({<<"te">>, _}) -> false; (_) -> true end, DoneWithPseudos) andalso no_upper_names(DoneWithPseudos). ================================================ FILE: src/h2_stream_set.erl ================================================ -module(h2_stream_set). -include("http2.hrl"). %% This module exists to manage a set of all streams for a given %% connection. When a connection starts, a stream set logically %% contains streams from id 1 to 2^31-1. In practicality, storing that %% many idle streams in a collection of any type would be more memory %% intensive. We're going to manage that here in this module, but to %% the outside, it will behave as if they all exist -record( stream_set, { %% Type determines which streams are mine, and which are theirs type :: client | server, %% Streams initiated by this peer mine :: peer_subset(), %% Streams initiated by the other peer theirs :: peer_subset() }). -type stream_set() :: #stream_set{}. -export_type([stream_set/0]). %% The stream_set needs to keep track of two subsets of streams, one %% for the streams that it has initiated, and one for the streams that %% have been initiated on the other side of the connection. It is this %% peer_subset that will try to optimize the set of stream metadata %% that we're storing in memory. For each side of the connection, we %% also need an accurate count of how many are currently active -record( peer_subset, { %% Provided by the connection settings, we can check against this %% every time we try to add a stream to this subset max_active = unlimited :: unlimited | pos_integer(), %% A counter that's an accurate reflection of the number of %% active streams active_count = 0 :: non_neg_integer(), %% lowest_stream_id is the lowest stream id that we're currently %% managing in memory. Any stream with an id lower than this is %% automatically of type closed. lowest_stream_id = 0 :: stream_id(), %% Next available stream id will be the stream id of the next %% stream that can be added to this subset. That means if asked %% for a stream with this id or higher, the stream type returned %% must be idle. Any stream id lower than this that isn't active %% must be of type closed. next_available_stream_id :: stream_id(), %% A bit of a misnomer, active is actually the set of streams %% that are active in MEMORY not the connection. This will %% include *ALL* active streams, and possibly some closed streams %% as well that have yet to be garbage collected. active = [] :: [stream()] }). -type peer_subset() :: #peer_subset{}. %% Streams all have stream_ids. It is the only thing all three types %% have. It *MUST* be the first field in *ALL* *_stream{} records. %% The metadata for an active stream is, unsurprisingly, the most %% complex. -record( active_stream, { id :: stream_id(), % Pid running the http2_stream gen_statem pid :: pid(), % The process to notify with events on this stream notify_pid :: pid() | undefined, % The stream's flow control send window size send_window_size :: non_neg_integer(), % The stream's flow control recv window size recv_window_size :: non_neg_integer(), % Data that is in queue to send on this stream, if flow control % hasn't allowed it to be sent yet queued_data :: undefined | done | binary(), % Has the body been completely received. body_complete = false :: boolean(), trailers = undefined :: [h2_frame:frame()] | undefined }). -type active_stream() :: #active_stream{}. %% The closed_stream record is way more important to a client than a %% server. It's a way of holding on to a response that has been %% received, but not processed by the client yet. -record( closed_stream, { id :: stream_id(), % The pid to notify about events on this stream notify_pid :: pid() | undefined, % The response headers received response_headers :: hpack:headers() | undefined, % The response body response_body :: binary() | undefined, % Can this be thrown away? garbage = false :: boolean() | undefined }). -type closed_stream() :: #closed_stream{}. %% An idle stream record isn't used for much. It's never stored, %% unlike the other two types. It is always generated on the fly when %% asked for a stream >= next_available_stream_id. But, we're able to %% perform a rst_stream operation on it, and we need a stream_id to %% make that happen. -record( idle_stream, { id :: stream_id() }). -type idle_stream() :: #idle_stream{}. %% So a stream can be any of these things. And it will be something %% that you can pass back into several functions here in this module. -type stream() :: active_stream() | closed_stream() | idle_stream(). -export_type([stream/0]). %% Set Operations -export( [ new/1, new_stream/8, get/2, upsert/2, sort/1 ]). %% Accessors -export( [ queued_data/1, update_trailers/2, update_data_queue/3, decrement_recv_window/2, recv_window_size/1, response/1, send_window_size/1, increment_send_window_size/2, pid/1, stream_id/1, stream_pid/1, notify_pid/1, type/1, my_active_count/1, their_active_count/1, my_active_streams/1, their_active_streams/1, my_max_active/1, their_max_active/1 ] ). -export( [ close/3, send_what_we_can/4, update_all_recv_windows/2, update_all_send_windows/2, update_their_max_active/2, update_my_max_active/2 ] ). %% new/1 returns a new stream_set. This is your constructor. -spec new( client | server ) -> stream_set(). new(client) -> #stream_set{ type=client, %% I'm a client, so mine are always odd numbered mine= #peer_subset{ lowest_stream_id=0, next_available_stream_id=1 }, %% And theirs are always even theirs= #peer_subset{ lowest_stream_id=0, next_available_stream_id=2 } }; new(server) -> #stream_set{ type=server, %% I'm a server, so mine are always even mine= #peer_subset{ lowest_stream_id=0, next_available_stream_id=2 }, %% And theirs are always odd. theirs= #peer_subset{ lowest_stream_id=0, next_available_stream_id=1 } }. -spec new_stream( StreamId :: stream_id(), NotifyPid :: pid(), CBMod :: module(), CBOpts :: list(), Socket :: sock:socket(), InitialSendWindow :: integer(), InitialRecvWindow :: integer(), StreamSet :: stream_set()) -> stream_set() | {error, error_code(), closed_stream()}. new_stream( StreamId, NotifyPid, CBMod, CBOpts, Socket, InitialSendWindow, InitialRecvWindow, StreamSet) -> PeerSubset = get_peer_subset(StreamId, StreamSet), case PeerSubset#peer_subset.max_active =/= unlimited andalso PeerSubset#peer_subset.active_count >= PeerSubset#peer_subset.max_active of true -> {error, ?REFUSED_STREAM, #closed_stream{id=StreamId}}; false -> {ok, Pid} = h2_stream:start_link( StreamId, self(), CBMod, CBOpts, Socket ), NewStream = #active_stream{ id = StreamId, pid = Pid, notify_pid=NotifyPid, send_window_size=InitialSendWindow, recv_window_size=InitialRecvWindow }, case upsert(NewStream, StreamSet) of {error, ?REFUSED_STREAM} -> %% This should be very rare, if it ever happens at %% all. The case clause above tests the same %% condition that upsert/2 checks to return this %% result. Still, we need this case statement %% because returning an {error tuple here would be %% catastrophic %% If this did happen, we need to kill this %% process, or it will just hang out there. h2_stream:stop(Pid), {error, ?REFUSED_STREAM, #closed_stream{id=StreamId}}; NewStreamSet -> NewStreamSet end end. -spec get_peer_subset( stream_id(), stream_set()) -> peer_subset(). get_peer_subset(Id, StreamSet) -> case {Id rem 2, StreamSet#stream_set.type} of {0, client} -> StreamSet#stream_set.theirs; {1, client} -> StreamSet#stream_set.mine; {0, server} -> StreamSet#stream_set.mine; {1, server} -> StreamSet#stream_set.theirs end. -spec set_peer_subset( Id :: stream_id(), StreamSet :: stream_set(), NewPeerSubset :: peer_subset() ) -> stream_set(). set_peer_subset(Id, StreamSet, NewPeerSubset) -> case {Id rem 2, StreamSet#stream_set.type} of {0, client} -> StreamSet#stream_set{ theirs=NewPeerSubset }; {1, client} -> StreamSet#stream_set{ mine=NewPeerSubset }; {0, server} -> StreamSet#stream_set{ mine=NewPeerSubset }; {1, server} -> StreamSet#stream_set{ theirs=NewPeerSubset } end. %% get/2 gets a stream. The logic in here basically just chooses which %% subset. -spec get(Id :: stream_id(), Streams :: stream_set()) -> stream(). get(Id, StreamSet) -> get_from_subset(Id, get_peer_subset( Id, StreamSet)). -spec get_from_subset( Id :: stream_id(), PeerSubset :: peer_subset()) -> stream(). get_from_subset(Id, #peer_subset{ lowest_stream_id=Lowest }) when Id < Lowest -> #closed_stream{id=Id}; get_from_subset(Id, #peer_subset{ next_available_stream_id=Next }) when Id >= Next -> #idle_stream{id=Id}; get_from_subset(Id, PeerSubset) -> case lists:keyfind(Id, 2, PeerSubset#peer_subset.active) of false -> #closed_stream{id=Id}; Stream -> Stream end. -spec upsert( Stream :: stream(), StreamSet :: stream_set()) -> stream_set() | {error, error_code()}. %% Can't store idle streams upsert(#idle_stream{}, StreamSet) -> StreamSet; upsert(Stream, StreamSet) -> StreamId = stream_id(Stream), PeerSubset = get_peer_subset(StreamId, StreamSet), case upsert_peer_subset(Stream, PeerSubset) of {error, Code} -> {error, Code}; NewPeerSubset -> set_peer_subset(StreamId, StreamSet, NewPeerSubset) end. -spec upsert_peer_subset( Stream :: closed_stream() | active_stream(), PeerSubset :: peer_subset() ) -> peer_subset() | {error, error_code()}. %% Case 1: We're upserting a closed stream, it contains garbage we %% don't care about and it's in the range of streams we're actively %% tracking We remove it, and move the lowest_active pointer. upsert_peer_subset( #closed_stream{ id=Id, garbage=true }, PeerSubset) when Id >= PeerSubset#peer_subset.lowest_stream_id, Id < PeerSubset#peer_subset.next_available_stream_id -> OldStream = get_from_subset(Id, PeerSubset), OldType = type(OldStream), ActiveDiff = case OldType of closed -> 0; active -> -1 end, NewActive = lists:keydelete(Id, 2, PeerSubset#peer_subset.active), %% NewActive could now have a #closed_stream with no information %% in it as the lowest active stream, so we should drop those. OptimizedNewActive = drop_unneeded_streams(NewActive), case OptimizedNewActive of [] -> PeerSubset#peer_subset{ lowest_stream_id=PeerSubset#peer_subset.next_available_stream_id, active_count=0, active=[] }; [NewLowestStream|_] -> NewLowest = stream_id(NewLowestStream), PeerSubset#peer_subset{ lowest_stream_id=NewLowest, active_count=PeerSubset#peer_subset.active_count+ActiveDiff, active=OptimizedNewActive } end; %% Case 2: Like case 1, but it's not garbage upsert_peer_subset( #closed_stream{ id=Id, garbage=false }=Closed, PeerSubset) when Id >= PeerSubset#peer_subset.lowest_stream_id, Id < PeerSubset#peer_subset.next_available_stream_id -> OldStream = get_from_subset(Id, PeerSubset), OldType = type(OldStream), ActiveDiff = case OldType of closed -> 0; active -> -1 end, NewActive = lists:keyreplace(Id, 2, PeerSubset#peer_subset.active, Closed), PeerSubset#peer_subset{ active_count=PeerSubset#peer_subset.active_count+ActiveDiff, active=NewActive }; %% Case 3: It's closed, but greater than or equal to next available: upsert_peer_subset( #closed_stream{ id=Id } = Closed, PeerSubset) when Id >= PeerSubset#peer_subset.next_available_stream_id -> PeerSubset#peer_subset{ next_available_stream_id=Id+2, active=lists:keystore(Id, 2, PeerSubset#peer_subset.active, Closed) }; %% Case 4: It's active, and in the range we're working with upsert_peer_subset( #active_stream{ id=Id }=Stream, PeerSubset) when Id >= PeerSubset#peer_subset.lowest_stream_id, Id < PeerSubset#peer_subset.next_available_stream_id -> PeerSubset#peer_subset{ active = lists:keystore(Id, 2, PeerSubset#peer_subset.active, Stream) }; %% Case 5: It's active, but it wasn't active before and activating it %% would exceed our concurrent stream limits upsert_peer_subset( #active_stream{}, PeerSubset) when PeerSubset#peer_subset.max_active =/= unlimited, PeerSubset#peer_subset.active_count >= PeerSubset#peer_subset.max_active -> {error, ?REFUSED_STREAM}; %% Case 6: It's active, and greater than the range we're tracking upsert_peer_subset( #active_stream{ id=Id }=Stream, PeerSubset) when Id >= PeerSubset#peer_subset.next_available_stream_id -> PeerSubset#peer_subset{ next_available_stream_id=Id+2, active_count=PeerSubset#peer_subset.active_count+1, active = lists:keystore(Id, 2, PeerSubset#peer_subset.active, Stream) }; %% Catch All %% TODO: remove this match and crash instead? upsert_peer_subset( _Stream, PeerSubset) -> PeerSubset. drop_unneeded_streams(Streams) -> SortedStreams = lists:keysort(2, Streams), lists:dropwhile( fun(#closed_stream{ garbage=true }) -> true; (_) -> false end, SortedStreams). -spec close( Stream :: stream(), Response :: garbage | {hpack:headers(), iodata()}, Streams :: stream_set() ) -> { stream(), stream_set()}. close(Stream, garbage, StreamSet) -> Closed = #closed_stream{ id = stream_id(Stream), garbage=true }, {Closed, upsert(Closed, StreamSet)}; close(Closed=#closed_stream{}, _Response, Streams) -> {Closed, Streams}; close(_Idle=#idle_stream{id=StreamId}, {Headers, Body}, Streams) -> Closed = #closed_stream{ id=StreamId, response_headers=Headers, response_body=Body }, {Closed, upsert(Closed, Streams)}; close(#active_stream{ id=Id, notify_pid=NotifyPid }, {Headers, Body}, Streams) -> Closed = #closed_stream{ id=Id, response_headers=Headers, response_body=Body, notify_pid=NotifyPid }, {Closed, upsert(Closed, Streams)}. %% TODO: Change sort to send peer_initiated first! -spec sort(StreamSet::stream_set()) -> stream_set(). sort(StreamSet) -> StreamSet#stream_set{ theirs = sort_peer_subset(StreamSet#stream_set.theirs), mine = sort_peer_subset(StreamSet#stream_set.mine) }. sort_peer_subset(PeerSubset) -> PeerSubset#peer_subset{ active=lists:keysort(2, PeerSubset#peer_subset.active) }. -spec update_all_recv_windows(Delta :: integer(), Streams:: stream_set()) -> stream_set(). update_all_recv_windows(Delta, Streams) -> Streams#stream_set{ theirs=update_all_recv_windows_subset(Delta, Streams#stream_set.theirs), mine=update_all_recv_windows_subset(Delta, Streams#stream_set.mine) }. update_all_recv_windows_subset(Delta, PeerSubset) -> NewActive = lists:map( fun(#active_stream{}=S) -> S#active_stream{ recv_window_size=S#active_stream.recv_window_size+Delta }; (S) -> S end, PeerSubset#peer_subset.active), PeerSubset#peer_subset{ active=NewActive }. -spec update_all_send_windows(Delta :: integer(), Streams:: stream_set()) -> stream_set(). update_all_send_windows(Delta, Streams) -> Streams#stream_set{ theirs=update_all_send_windows_subset(Delta, Streams#stream_set.theirs), mine=update_all_recv_windows_subset(Delta, Streams#stream_set.mine) }. update_all_send_windows_subset(Delta, PeerSubset) -> NewActive = lists:map( fun(#active_stream{}=S) -> S#active_stream{ send_window_size=S#active_stream.send_window_size+Delta }; (S) -> S end, PeerSubset#peer_subset.active), PeerSubset#peer_subset{ active=NewActive }. -spec update_their_max_active(NewMax :: non_neg_integer() | unlimited, Streams :: stream_set()) -> stream_set(). update_their_max_active(NewMax, #stream_set{ theirs=Theirs }=Streams) -> Streams#stream_set{ theirs=Theirs#peer_subset{max_active=NewMax} }. -spec update_my_max_active(NewMax :: non_neg_integer() | unlimited, Streams :: stream_set()) -> stream_set(). update_my_max_active(NewMax, #stream_set{ mine=Mine }=Streams) -> Streams#stream_set{ mine=Mine#peer_subset{max_active=NewMax} }. -spec send_what_we_can(StreamId :: all | stream_id(), ConnSendWindowSize :: integer(), MaxFrameSize :: non_neg_integer(), Streams :: stream_set()) -> {NewConnSendWindowSize :: integer(), NewStreams :: stream_set()}. send_what_we_can(all, ConnSendWindowSize, MaxFrameSize, Streams) -> {AfterPeerWindowSize, NewPeerInitiated} = c_send_what_we_can( ConnSendWindowSize, MaxFrameSize, Streams#stream_set.theirs#peer_subset.active, []), {AfterAfterWindowSize, NewSelfInitiated} = c_send_what_we_can( AfterPeerWindowSize, MaxFrameSize, Streams#stream_set.mine#peer_subset.active, []), {AfterAfterWindowSize, Streams#stream_set{ theirs=Streams#stream_set.theirs#peer_subset{active=NewPeerInitiated}, mine=Streams#stream_set.mine#peer_subset{active=NewSelfInitiated} } }; send_what_we_can(StreamId, ConnSendWindowSize, MaxFrameSize, Streams) -> {NewConnSendWindowSize, NewStream} = s_send_what_we_can(ConnSendWindowSize, MaxFrameSize, get(StreamId, Streams)), {NewConnSendWindowSize, upsert(NewStream, Streams)}. %% Send at the connection level -spec c_send_what_we_can(ConnSendWindowSize :: integer(), MaxFrameSize :: non_neg_integer(), Streams :: [stream()], Acc :: [stream()] ) -> {integer(), [stream()]}. %% If we hit =< 0, done c_send_what_we_can(ConnSendWindowSize, _MFS, Streams, Acc) when ConnSendWindowSize =< 0 -> {ConnSendWindowSize, lists:reverse(Acc) ++ Streams}; %% If we hit end of streams list, done c_send_what_we_can(SWS, _MFS, [], Acc) -> {SWS, lists:reverse(Acc)}; %% Otherwise, try sending on the working stream c_send_what_we_can(SWS, MFS, [S|Streams], Acc) -> {NewSWS, NewS} = s_send_what_we_can(SWS, MFS, S), c_send_what_we_can(NewSWS, MFS, Streams, [NewS|Acc]). %% Send at the stream level -spec s_send_what_we_can(SWS :: integer(), MFS :: non_neg_integer(), Stream :: stream()) -> {integer(), stream()}. s_send_what_we_can(SWS, _, #active_stream{queued_data=Data, trailers=undefined}=S) when is_atom(Data) -> {SWS, S}; s_send_what_we_can(SWS, _, #active_stream{queued_data=Data, pid=Pid, trailers=Trailers}=S) when is_atom(Data) -> [h2_stream:send_data(Pid, Frame) || Frame <- Trailers], {SWS, S}; s_send_what_we_can(SWS, MFS, #active_stream{}=Stream) -> %% We're coming in here with three numbers we need to look at: %% * Connection send window size %% * Stream send window size %% * Maximum frame size %% If none of them are zero, we have to send something, so we're %% going to figure out what's the biggest number we can send. If %% that's more than we have to send, we'll send everything and put %% an END_STREAM flag on it. Otherwise, we'll send as much as we %% can. Then, based on which number was the limiting factor, we'll %% make another decision %% If it was MAX_FRAME_SIZE, then we recurse into this same %% function, because we're able to send another frame of some %% length. %% If it was connection send window size, we're blocked at the %% connection level and we should break out of this recursion %% If it was stream send_window size, we're blocked on this %% stream, but other streams can still go, so we'll break out of %% this recursion, but not the connection level SSWS = Stream#active_stream.send_window_size, Trailers = Stream#active_stream.trailers, QueueSize = byte_size(Stream#active_stream.queued_data), {MaxToSend, ExitStrategy} = case {MFS =< SWS andalso MFS =< SSWS, SWS < SSWS} of %% If MAX_FRAME_SIZE is the smallest, send one and recurse {true, _} -> {MFS, max_frame_size}; {false, true} -> {SWS, connection}; _ -> {SSWS, stream} end, {Frame, SentBytes, NewS} = case MaxToSend > QueueSize of true -> Flags = case Stream#active_stream.body_complete of true -> ?FLAG_END_STREAM; false -> 0 end, %% We have the power to send everything {{#frame_header{ stream_id=Stream#active_stream.id, flags=Flags, type=?DATA, length=QueueSize }, h2_frame_data:new(Stream#active_stream.queued_data)}, %% Full Body QueueSize, Stream#active_stream{ queued_data=done, send_window_size=SSWS-QueueSize}}; false -> <> = Stream#active_stream.queued_data, {{#frame_header{ stream_id=Stream#active_stream.id, type=?DATA, length=MaxToSend }, h2_frame_data:new(BinToSend)}, MaxToSend, Stream#active_stream{ queued_data=Rest, send_window_size=SSWS-MaxToSend}} end, _Sent = h2_stream:send_data(Stream#active_stream.pid, Frame), case NewS of #active_stream{trailers=undefined} -> ok; #active_stream{pid=Pid, queued_data=done, trailers=Trailers} -> [h2_stream:send_data(Pid, Trailer) || Trailer <- Trailers]; _ -> ok end, case ExitStrategy of max_frame_size -> s_send_what_we_can(SWS - SentBytes, MFS, NewS); stream -> {SWS - SentBytes, NewS}; connection -> {SWS - SentBytes, NewS} end; s_send_what_we_can(SWS, _MFS, NonActiveStream) -> {SWS, NonActiveStream}. %% Record Accessors -spec stream_id( Stream :: stream()) -> stream_id(). stream_id(#idle_stream{id=SID}) -> SID; stream_id(#active_stream{id=SID}) -> SID; stream_id(#closed_stream{id=SID}) -> SID. -spec pid(stream()) -> pid() | undefined. pid(#active_stream{pid=Pid}) -> Pid; pid(_) -> undefined. -spec type(stream()) -> idle | active | closed. type(#idle_stream{}) -> idle; type(#active_stream{}) -> active; type(#closed_stream{}) -> closed. queued_data(#active_stream{queued_data=QD}) -> QD; queued_data(_) -> undefined. update_trailers(Trailers, Stream=#active_stream{}) -> Stream#active_stream{trailers=Trailers}. update_data_queue( NewBody, BodyComplete, #active_stream{} = Stream) -> Stream#active_stream{ queued_data=NewBody, body_complete=BodyComplete }; update_data_queue(_, _, S) -> S. response(#closed_stream{ response_headers=Headers, response_body=Body}) -> Encoding = case lists:keyfind(<<"content-encoding">>, 1, Headers) of false -> identity; {_, Encoding0} -> binary_to_atom(Encoding0, 'utf8') end, {Headers, decode_body(Body, Encoding)}; response(_) -> no_response. decode_body(Body, identity) -> Body; decode_body(Body, gzip) -> zlib:gunzip(Body); decode_body(Body, zip) -> zlib:unzip(Body); decode_body(Body, compress) -> zlib:uncompress(Body); decode_body(Body, deflate) -> Z = zlib:open(), ok = zlib:inflateInit(Z, -15), Decompressed = try zlib:inflate(Z, Body) catch E:V -> {E,V} end, ok = zlib:inflateEnd(Z), ok = zlib:close(Z), iolist_to_binary(Decompressed). recv_window_size(#active_stream{recv_window_size=RWS}) -> RWS; recv_window_size(_) -> undefined. decrement_recv_window( L, #active_stream{recv_window_size=RWS}=Stream ) -> Stream#active_stream{ recv_window_size=RWS-L }; decrement_recv_window(_, S) -> S. send_window_size(#active_stream{send_window_size=SWS}) -> SWS; send_window_size(_) -> undefined. increment_send_window_size( WSI, #active_stream{send_window_size=SWS}=Stream) -> Stream#active_stream{ send_window_size=SWS+WSI }; increment_send_window_size(_WSI, Stream) -> Stream. stream_pid(#active_stream{pid=Pid}) -> Pid; stream_pid(_) -> undefined. %% the false clause is here as an artifact of us using a simple %% lists:keyfind notify_pid(#idle_stream{}) -> undefined; notify_pid(#active_stream{notify_pid=Pid}) -> Pid; notify_pid(#closed_stream{notify_pid=Pid}) -> Pid. %% The number of #active_stream records -spec my_active_count(stream_set()) -> non_neg_integer(). my_active_count(SS) -> SS#stream_set.mine#peer_subset.active_count. %% The number of #active_stream records -spec their_active_count(stream_set()) -> non_neg_integer(). their_active_count(SS) -> SS#stream_set.theirs#peer_subset.active_count. %% The list of #active_streams, and un gc'd #closed_streams -spec my_active_streams(stream_set()) -> [stream()]. my_active_streams(SS) -> SS#stream_set.mine#peer_subset.active. %% The list of #active_streams, and un gc'd #closed_streams -spec their_active_streams(stream_set()) -> [stream()]. their_active_streams(SS) -> SS#stream_set.theirs#peer_subset.active. %% My MCS (max_active) -spec my_max_active(stream_set()) -> non_neg_integer(). my_max_active(SS) -> SS#stream_set.mine#peer_subset.max_active. %% Their MCS (max_active) -spec their_max_active(stream_set()) -> non_neg_integer(). their_max_active(SS) -> SS#stream_set.theirs#peer_subset.max_active. ================================================ FILE: src/sock.erl ================================================ -module(sock). -type transport() :: gen_tcp | ssl. -type socket() :: {gen_tcp, inet:socket()|undefined} | {ssl, ssl:sslsocket()|undefined}. -export_type([ transport/0, socket/0 ]). -export([ send/2, recv/2, recv/3, close/1, peername/1, peercert/1, setopts/2 ]). -spec send( Socket :: socket(), Data :: iodata()) -> ok | {error, closed | inet:posix()}. send({gen_tcp, Socket}, Data) -> gen_tcp:send(Socket, Data); send({ssl, Socket}, Data) -> ssl:send(Socket, Data); send(_, _) -> {error, bad_socket}. -spec recv( Socket :: socket(), Length :: non_neg_integer()) -> {ok, string() | binary() | term()} %% term for HttpSocket | {error, closed | inet:posix()}. recv(Socket, Length) -> recv(Socket, Length, infinity). -spec recv( Socket :: socket(), Length :: non_neg_integer(), Timeout :: timeout()) -> {ok, string() | binary() | term()} %% term for HttpSocket | {error, closed | inet:posix()}. recv({gen_tcp, Socket}, Length, Timeout) -> gen_tcp:recv(Socket, Length, Timeout); recv({ssl, Socket}, Length, Timeout) -> ssl:recv(Socket, Length, Timeout); recv(_, _, _) -> {error, bad_socket}. close({Transport, Socket}) -> Transport:close(Socket). peername({ssl, Socket}) -> ssl:peername(Socket); peername({gen_tcp, Socket}) -> inet:peername(Socket). peercert({ssl, Socket}) -> ssl:peercert(Socket); peercert({gen_tcp, _Socket}) -> {error, unsupported}. setopts({ssl, Socket}, Opts) -> ssl:setopts(Socket, Opts); setopts({gen_tcp, Socket}, Opts) -> inet:setopts(Socket, Opts). ================================================ FILE: test/chatterbox_test_buddy.erl ================================================ -module(chatterbox_test_buddy). -compile([export_all]). -include_lib("common_test/include/ct.hrl"). start(Config) -> application:load(chatterbox), ok = application:ensure_started(ranch), Config2 = ensure_ssl(Config), PreDataSettings = [ {port, 8081}, {ssl, ?config(ssl, Config2)}, {ssl_options, [{certfile, "../../../../config/localhost.crt"}, {keyfile, "../../../../config/localhost.key"}, {honor_cipher_order, false}, {versions, ['tlsv1.2']}, {alpn_preferred_protocols, [<<"h2">>]}]} ], Settings = case ?config(www_root, Config) of undefined -> [{chatterbox_static_stream, [{root_dir, code:priv_dir(chatterbox)}]}|PreDataSettings]; data_dir -> Root = ?config(data_dir, Config), [{chatterbox_static_stream, [{root_dir, Root}]}|PreDataSettings]; WWWRoot -> [{chatterbox_static_stream, [{root_dir, WWWRoot}]}|PreDataSettings] end, application:set_env(chatterbox, stream_callback_mod, proplists:get_value(stream_callback_mod, Config, chatterbox_static_stream)), application:set_env(chatterbox, server_header_table_size, proplists:get_value(header_table_size, Config, 4096)), application:set_env(chatterbox, server_enable_push, proplists:get_value(enable_push, Config, 1)), application:set_env(chatterbox, server_max_concurrent_streams, proplists:get_value(max_concurrent_streams, Config, unlimited)), application:set_env(chatterbox, server_initial_window_size, proplists:get_value(initial_window_size, Config, 65535)), application:set_env(chatterbox, server_max_frame_size, proplists:get_value(max_frame_size, Config, 16384)), application:set_env(chatterbox, server_max_header_list_size, proplists:get_value(max_header_list_size, Config, unlimited)), application:set_env(chatterbox, server_flow_control, proplists:get_value(flow_control, Config, auto)), ct:pal("Settings ~p", [Settings]), [ok = application:set_env(chatterbox, Key, Value) || {Key, Value} <- Settings ], ct:pal("Chatterbox Server Settings: ~p", [chatterbox:settings(server)]), %%% ensure client settings defaults application:set_env(chatterbox, client_header_table_size, 4096), application:set_env(chatterbox, client_enable_push, 1), application:set_env(chatterbox, client_max_concurrent_streams, unlimited), application:set_env(chatterbox, client_initial_window_size, 65535), application:set_env(chatterbox, client_max_frame_size, 16384), application:set_env(chatterbox, client_max_header_list_size, unlimited), application:set_env(chatterbox, client_flow_control, auto), {ok, List} = application:ensure_all_started(chatterbox), ct:pal("Started: ~p", [List]), {ok, _RanchPid} = ranch:start_listener( chatterbox_ranch_protocol, 10, ranch_ssl, [{port, 8081}|proplists:get_value(ssl_options, Settings)], chatterbox_ranch_protocol, []), Config. ssl(SSLBool, Config) -> [{ssl, SSLBool}|Config]. ensure_ssl(Config) -> case ?config(ssl, Config) of undefined -> ssl(true, Config); _SSL -> Config end. stop(_Config) -> ct:pal("chatterbox_test_buddy:stop/1"), application:stop(chatterbox), ranch:stop_listener(chatterbox_ranch_protocol), ok. ================================================ FILE: test/chatterbox_tests.erl ================================================ -module(chatterbox_tests). -include_lib("eunit/include/eunit.hrl"). chatterbox_test_() -> {setup, fun() -> ok end, fun(_) -> ok end, [ {"chatterbox is alive", fun() -> %% format is always: expected, actual % ?assertEqual(howdy, chatterbox:hello()) ok end} ]}. ================================================ FILE: test/client_server_SUITE.erl ================================================ -module(client_server_SUITE). -include("http2.hrl"). -include_lib("eunit/include/eunit.hrl"). -include_lib("common_test/include/ct.hrl"). -compile([export_all]). all() -> [ {group, default_handler}, {group, peer_handler}, {group, double_body_handler}, {group, echo_handler} ]. groups() -> [{default_handler, [complex_request, upgrade_tcp_connection, basic_push]}, {peer_handler, [get_peer_in_handler]}, {double_body_handler, [send_body_opts]}, {echo_handler, [echo_body]} ]. init_per_suite(Config) -> Config. init_per_group(default_handler, Config) -> %% We'll start up a chatterbox server once, with this data_dir. NewConfig = [{www_root, data_dir},{initial_window_size,99999999}|Config], chatterbox_test_buddy:start(NewConfig); init_per_group(double_body_handler, Config) -> NewConfig = [{stream_callback_mod, double_body_handler}, {initial_window_size,99999999}|Config], chatterbox_test_buddy:start(NewConfig), Config; init_per_group(peer_handler, Config) -> NewConfig = [{stream_callback_mod, peer_test_handler}, {initial_window_size,99999999}|Config], chatterbox_test_buddy:start(NewConfig); init_per_group(echo_handler, Config) -> NewConfig = [{stream_callback_mod, echo_handler}, {initial_window_size,64}|Config], chatterbox_test_buddy:start(NewConfig); init_per_group(_, Config) -> Config. init_per_testcase(_, Config) -> Config. end_per_group(_, Config) -> chatterbox_test_buddy:stop(Config), ok. end_per_suite(_Config) -> ok. complex_request(_Config) -> application:set_env(chatterbox, client_initial_window_size, 99999999), {ok, Client} = h2_client:start_link(), RequestHeaders = [ {<<":method">>, <<"GET">>}, {<<":path">>, <<"/index.html">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost:8080">>}, {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip, deflate">>}, {<<"user-agent">>, <<"chattercli/0.0.1 :D">>} ], {ok, {ResponseHeaders, ResponseBody}} = h2_client:sync_request(Client, RequestHeaders, <<>>), ct:pal("Response Headers: ~p", [ResponseHeaders]), ct:pal("Response Body: ~p", [ResponseBody]), ok. upgrade_tcp_connection(_Config) -> {ok, Client} = h2_client:start_ssl_upgrade_link("localhost", 8081, <<>>, []), RequestHeaders = [ {<<":method">>, <<"GET">>}, {<<":path">>, <<"/index.html">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost:8080">>}, {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip, deflate">>}, {<<"user-agent">>, <<"chattercli/0.0.1 :D">>} ], {ok, {ResponseHeaders, ResponseBody}} = h2_client:sync_request(Client, RequestHeaders, <<>>), ct:pal("Response Headers: ~p", [ResponseHeaders]), ct:pal("Response Body: ~p", [ResponseBody]), ok. basic_push(_Config) -> {ok, Client} = h2_client:start_link(), RequestHeaders = [ {<<":method">>, <<"GET">>}, {<<":path">>, <<"/index.html">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost:8080">>}, {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip, deflate">>}, {<<"user-agent">>, <<"chattercli/0.0.1 :D">>} ], {ok, {ResponseHeaders, ResponseBody}} = h2_client:sync_request(Client, RequestHeaders, <<>>), ct:pal("Response Headers: ~p", [ResponseHeaders]), ct:pal("Response Body: ~p", [ResponseBody]), %% Give it time to deliver pushes %% We'll know we're done when we're notified of all the streams ending. wait_for_n_notifications(12), Streams = h2_connection:get_streams(Client), ct:pal("Streams ~p", [Streams]), ?assertEqual(0, (h2_stream_set:my_active_count(Streams))), ?assertEqual(0, (h2_stream_set:their_active_count(Streams))), MyActiveStreams = h2_stream_set:my_active_streams(Streams), ?assertEqual(0, (length(MyActiveStreams))), %% This closed stream should be GC'ed TheirActiveStreams = h2_stream_set:their_active_streams(Streams), ?assertEqual(12, (length(TheirActiveStreams))), [?assertEqual(closed, (h2_stream_set:type(S))) || S <- TheirActiveStreams], ok. wait_for_n_notifications(0) -> ok; wait_for_n_notifications(N) -> receive {'END_STREAM', _} -> wait_for_n_notifications(N-1); _ -> wait_for_n_notifications(N) after 2000 -> ok end. get_peer_in_handler(_Config) -> {ok, Client} = h2_client:start_link(), RequestHeaders = [ {<<":method">>, <<"GET">>}, {<<":path">>, <<"/index.html">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost:8080">>}, {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip, deflate">>}, {<<"user-agent">>, <<"chattercli/0.0.1 :D">>} ], {ok, {ResponseHeaders, ResponseBody}} = h2_client:sync_request(Client, RequestHeaders, <<>>), ct:pal("Response Headers: ~p", [ResponseHeaders]), ct:pal("Response Body: ~p", [ResponseBody]), ok. send_body_opts(_Config) -> {ok, Client} = h2_client:start_link(), RequestHeaders = [ {<<":method">>, <<"GET">>}, {<<":path">>, <<"/index.html">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost:8080">>}, {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip, deflate">>}, {<<"user-agent">>, <<"chattercli/0.0.1 :D">>} ], ExpectedResponseBody = <<"BodyPart1\nBodyPart2">>, {ok, {ResponseHeaders, ResponseBody}} = h2_client:sync_request(Client, RequestHeaders, <<>>), ct:pal("Response Headers: ~p", [ResponseHeaders]), ct:pal("Response Body: ~p", [ResponseBody]), ?assertEqual(ExpectedResponseBody, (iolist_to_binary(ResponseBody))), ok. echo_body(_Config) -> {ok, Client} = http2c:start_link(), RequestHeaders = [ {<<":method">>, <<"POST">>}, {<<":path">>, <<"/">>}, {<<":scheme">>, <<"https">>}, {<<":authority">>, <<"localhost:8080">>}, {<<"accept">>, <<"*/*">>}, {<<"accept-encoding">>, <<"gzip, deflate">>}, {<<"user-agent">>, <<"chattercli/0.0.1 :D">>} ], {ok, {HeadersBin, _EncodeContext}} = hpack:encode(RequestHeaders, hpack:new_context()), HeaderFrame = {#frame_header{ length=byte_size(HeadersBin), type=?HEADERS, flags=?FLAG_END_HEADERS, stream_id=3 }, h2_frame_headers:new(HeadersBin) }, http2c:send_unaltered_frames(Client, [HeaderFrame]), Body = crypto:strong_rand_bytes(128), BodyFrames = h2_frame_data:to_frames(3, Body, #settings{max_frame_size=64}), http2c:send_unaltered_frames(Client, BodyFrames), timer:sleep(300), Frames = http2c:get_frames(Client, 3), DataFrames = lists:filter(fun({#frame_header{type=?DATA}, _}) -> true; (_) -> false end, Frames), ResponseData = lists:map(fun({_, DataP}) -> h2_frame_data:data(DataP) end, DataFrames), io:format("Body: ~p, response: ~p~n", [Body, ResponseData]), ?assertEqual(Body, (iolist_to_binary(ResponseData))), ok. ================================================ FILE: test/client_server_SUITE_data/README.md ================================================ Thanks to reveal-js for the test content. https://github.com/hakimel/reveal.js ================================================ FILE: test/client_server_SUITE_data/bower.json ================================================ { "name": "reveal.js", "version": "3.2.0", "main": [ "js/reveal.js", "css/reveal.css" ], "homepage": "http://lab.hakim.se/reveal-js/", "license": "MIT", "description": "The HTML Presentation Framework", "authors": [ "Hakim El Hattab " ], "dependencies": { "headjs": "~1.0.3" }, "repository": { "type": "git", "url": "git://github.com/hakimel/reveal.js.git" }, "ignore": [ "**/.*", "node_modules", "bower_components", "test" ] } ================================================ FILE: test/client_server_SUITE_data/css/print/paper.css ================================================ /* Default Print Stylesheet Template by Rob Glazebrook of CSSnewbie.com Last Updated: June 4, 2008 Feel free (nay, compelled) to edit, append, and manipulate this file as you see fit. */ @media print { /* SECTION 1: Set default width, margin, float, and background. This prevents elements from extending beyond the edge of the printed page, and prevents unnecessary background images from printing */ html { background: #fff; width: auto; height: auto; overflow: visible; } body { background: #fff; font-size: 20pt; width: auto; height: auto; border: 0; margin: 0 5%; padding: 0; overflow: visible; float: none !important; } /* SECTION 2: Remove any elements not needed in print. This would include navigation, ads, sidebars, etc. */ .nestedarrow, .controls, .fork-reveal, .share-reveal, .state-background, .reveal .progress, .reveal .backgrounds { display: none !important; } /* SECTION 3: Set body font face, size, and color. Consider using a serif font for readability. */ body, p, td, li, div { font-size: 20pt!important; font-family: Georgia, "Times New Roman", Times, serif !important; color: #000; } /* SECTION 4: Set heading font face, sizes, and color. Differentiate your headings from your body text. Perhaps use a large sans-serif for distinction. */ h1,h2,h3,h4,h5,h6 { color: #000!important; height: auto; line-height: normal; font-family: Georgia, "Times New Roman", Times, serif !important; text-shadow: 0 0 0 #000 !important; text-align: left; letter-spacing: normal; } /* Need to reduce the size of the fonts for printing */ h1 { font-size: 28pt !important; } h2 { font-size: 24pt !important; } h3 { font-size: 22pt !important; } h4 { font-size: 22pt !important; font-variant: small-caps; } h5 { font-size: 21pt !important; } h6 { font-size: 20pt !important; font-style: italic; } /* SECTION 5: Make hyperlinks more usable. Ensure links are underlined, and consider appending the URL to the end of the link for usability. */ a:link, a:visited { color: #000 !important; font-weight: bold; text-decoration: underline; } /* .reveal a:link:after, .reveal a:visited:after { content: " (" attr(href) ") "; color: #222 !important; font-size: 90%; } */ /* SECTION 6: more reveal.js specific additions by @skypanther */ ul, ol, div, p { visibility: visible; position: static; width: auto; height: auto; display: block; overflow: visible; margin: 0; text-align: left !important; } .reveal pre, .reveal table { margin-left: 0; margin-right: 0; } .reveal pre code { padding: 20px; border: 1px solid #ddd; } .reveal blockquote { margin: 20px 0; } .reveal .slides { position: static !important; width: auto !important; height: auto !important; left: 0 !important; top: 0 !important; margin-left: 0 !important; margin-top: 0 !important; padding: 0 !important; zoom: 1 !important; overflow: visible !important; display: block !important; text-align: left !important; -webkit-perspective: none; -moz-perspective: none; -ms-perspective: none; perspective: none; -webkit-perspective-origin: 50% 50%; -moz-perspective-origin: 50% 50%; -ms-perspective-origin: 50% 50%; perspective-origin: 50% 50%; } .reveal .slides section { visibility: visible !important; position: static !important; width: auto !important; height: auto !important; display: block !important; overflow: visible !important; left: 0 !important; top: 0 !important; margin-left: 0 !important; margin-top: 0 !important; padding: 60px 20px !important; z-index: auto !important; opacity: 1 !important; page-break-after: always !important; -webkit-transform-style: flat !important; -moz-transform-style: flat !important; -ms-transform-style: flat !important; transform-style: flat !important; -webkit-transform: none !important; -moz-transform: none !important; -ms-transform: none !important; transform: none !important; -webkit-transition: none !important; -moz-transition: none !important; -ms-transition: none !important; transition: none !important; } .reveal .slides section.stack { padding: 0 !important; } .reveal section:last-of-type { page-break-after: avoid !important; } .reveal section .fragment { opacity: 1 !important; visibility: visible !important; -webkit-transform: none !important; -moz-transform: none !important; -ms-transform: none !important; transform: none !important; } .reveal section img { display: block; margin: 15px 0px; background: rgba(255,255,255,1); border: 1px solid #666; box-shadow: none; } .reveal section small { font-size: 0.8em; } } ================================================ FILE: test/client_server_SUITE_data/css/print/pdf.css ================================================ /** * This stylesheet is used to print reveal.js * presentations to PDF. * * https://github.com/hakimel/reveal.js#pdf-export */ * { -webkit-print-color-adjust: exact; } body { margin: 0 auto !important; border: 0; padding: 0; float: none !important; overflow: visible; } html { width: 100%; height: 100%; overflow: visible; } /* Remove any elements not needed in print. */ .nestedarrow, .reveal .controls, .reveal .progress, .reveal .playback, .reveal.overview, .fork-reveal, .share-reveal, .state-background { display: none !important; } h1, h2, h3, h4, h5, h6 { text-shadow: 0 0 0 #000 !important; } .reveal pre code { overflow: hidden !important; font-family: Courier, 'Courier New', monospace !important; } ul, ol, div, p { visibility: visible; position: static; width: auto; height: auto; display: block; overflow: visible; margin: auto; } .reveal { width: auto !important; height: auto !important; overflow: hidden !important; } .reveal .slides { position: static; width: 100%; height: auto; left: auto; top: auto; margin: 0 !important; padding: 0 !important; overflow: visible; display: block; -webkit-perspective: none; -moz-perspective: none; -ms-perspective: none; perspective: none; -webkit-perspective-origin: 50% 50%; /* there isn't a none/auto value but 50-50 is the default */ -moz-perspective-origin: 50% 50%; -ms-perspective-origin: 50% 50%; perspective-origin: 50% 50%; } .reveal .slides section { page-break-after: always !important; visibility: visible !important; position: relative !important; display: block !important; position: relative !important; margin: 0 !important; padding: 0 !important; box-sizing: border-box !important; min-height: 1px; opacity: 1 !important; -webkit-transform-style: flat !important; -moz-transform-style: flat !important; -ms-transform-style: flat !important; transform-style: flat !important; -webkit-transform: none !important; -moz-transform: none !important; -ms-transform: none !important; transform: none !important; } .reveal section.stack { margin: 0 !important; padding: 0 !important; page-break-after: avoid !important; height: auto !important; min-height: auto !important; } .reveal img { box-shadow: none; } .reveal .roll { overflow: visible; line-height: 1em; } /* Slide backgrounds are placed inside of their slide when exporting to PDF */ .reveal section .slide-background { display: block !important; position: absolute; top: 0; left: 0; width: 100%; z-index: -1; } /* All elements should be above the slide-background */ .reveal section>* { position: relative; z-index: 1; } /* Display slide speaker notes when 'showNotes' is enabled */ .reveal .speaker-notes-pdf { display: block; width: 100%; max-height: none; left: auto; top: auto; z-index: 100; } /* Display slide numbers when 'slideNumber' is enabled */ .reveal .slide-number-pdf { display: block; position: absolute; font-size: 14px; } ================================================ FILE: test/client_server_SUITE_data/css/reveal.css ================================================ /*! * reveal.js * http://lab.hakim.se/reveal-js * MIT licensed * * Copyright (C) 2015 Hakim El Hattab, http://hakim.se */ /********************************************* * RESET STYLES *********************************************/ html, body, .reveal div, .reveal span, .reveal applet, .reveal object, .reveal iframe, .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6, .reveal p, .reveal blockquote, .reveal pre, .reveal a, .reveal abbr, .reveal acronym, .reveal address, .reveal big, .reveal cite, .reveal code, .reveal del, .reveal dfn, .reveal em, .reveal img, .reveal ins, .reveal kbd, .reveal q, .reveal s, .reveal samp, .reveal small, .reveal strike, .reveal strong, .reveal sub, .reveal sup, .reveal tt, .reveal var, .reveal b, .reveal u, .reveal center, .reveal dl, .reveal dt, .reveal dd, .reveal ol, .reveal ul, .reveal li, .reveal fieldset, .reveal form, .reveal label, .reveal legend, .reveal table, .reveal caption, .reveal tbody, .reveal tfoot, .reveal thead, .reveal tr, .reveal th, .reveal td, .reveal article, .reveal aside, .reveal canvas, .reveal details, .reveal embed, .reveal figure, .reveal figcaption, .reveal footer, .reveal header, .reveal hgroup, .reveal menu, .reveal nav, .reveal output, .reveal ruby, .reveal section, .reveal summary, .reveal time, .reveal mark, .reveal audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } .reveal article, .reveal aside, .reveal details, .reveal figcaption, .reveal figure, .reveal footer, .reveal header, .reveal hgroup, .reveal menu, .reveal nav, .reveal section { display: block; } /********************************************* * GLOBAL STYLES *********************************************/ html, body { width: 100%; height: 100%; overflow: hidden; } body { position: relative; line-height: 1; background-color: #fff; color: #000; } html:-webkit-full-screen-ancestor { background-color: inherit; } html:-moz-full-screen-ancestor { background-color: inherit; } /********************************************* * VIEW FRAGMENTS *********************************************/ .reveal .slides section .fragment { opacity: 0; visibility: hidden; -webkit-transition: all 0.2s ease; transition: all 0.2s ease; } .reveal .slides section .fragment.visible { opacity: 1; visibility: visible; } .reveal .slides section .fragment.grow { opacity: 1; visibility: visible; } .reveal .slides section .fragment.grow.visible { -webkit-transform: scale(1.3); -ms-transform: scale(1.3); transform: scale(1.3); } .reveal .slides section .fragment.shrink { opacity: 1; visibility: visible; } .reveal .slides section .fragment.shrink.visible { -webkit-transform: scale(0.7); -ms-transform: scale(0.7); transform: scale(0.7); } .reveal .slides section .fragment.zoom-in { -webkit-transform: scale(0.1); -ms-transform: scale(0.1); transform: scale(0.1); } .reveal .slides section .fragment.zoom-in.visible { -webkit-transform: none; -ms-transform: none; transform: none; } .reveal .slides section .fragment.fade-out { opacity: 1; visibility: visible; } .reveal .slides section .fragment.fade-out.visible { opacity: 0; visibility: hidden; } .reveal .slides section .fragment.semi-fade-out { opacity: 1; visibility: visible; } .reveal .slides section .fragment.semi-fade-out.visible { opacity: 0.5; visibility: visible; } .reveal .slides section .fragment.strike { opacity: 1; visibility: visible; } .reveal .slides section .fragment.strike.visible { text-decoration: line-through; } .reveal .slides section .fragment.current-visible { opacity: 0; visibility: hidden; } .reveal .slides section .fragment.current-visible.current-fragment { opacity: 1; visibility: visible; } .reveal .slides section .fragment.highlight-red, .reveal .slides section .fragment.highlight-current-red, .reveal .slides section .fragment.highlight-green, .reveal .slides section .fragment.highlight-current-green, .reveal .slides section .fragment.highlight-blue, .reveal .slides section .fragment.highlight-current-blue { opacity: 1; visibility: visible; } .reveal .slides section .fragment.highlight-red.visible { color: #ff2c2d; } .reveal .slides section .fragment.highlight-green.visible { color: #17ff2e; } .reveal .slides section .fragment.highlight-blue.visible { color: #1b91ff; } .reveal .slides section .fragment.highlight-current-red.current-fragment { color: #ff2c2d; } .reveal .slides section .fragment.highlight-current-green.current-fragment { color: #17ff2e; } .reveal .slides section .fragment.highlight-current-blue.current-fragment { color: #1b91ff; } /********************************************* * DEFAULT ELEMENT STYLES *********************************************/ /* Fixes issue in Chrome where italic fonts did not appear when printing to PDF */ .reveal:after { content: ''; font-style: italic; } .reveal iframe { z-index: 1; } /** Prevents layering issues in certain browser/transition combinations */ .reveal a { position: relative; } .reveal .stretch { max-width: none; max-height: none; } .reveal pre.stretch code { height: 100%; max-height: 100%; box-sizing: border-box; } /********************************************* * CONTROLS *********************************************/ .reveal .controls { display: none; position: fixed; width: 110px; height: 110px; z-index: 30; right: 10px; bottom: 10px; -webkit-user-select: none; } .reveal .controls button { padding: 0; position: absolute; opacity: 0.05; width: 0; height: 0; background-color: transparent; border: 12px solid transparent; -webkit-transform: scale(0.9999); -ms-transform: scale(0.9999); transform: scale(0.9999); -webkit-transition: all 0.2s ease; transition: all 0.2s ease; -webkit-appearance: none; -webkit-tap-highlight-color: transparent; } .reveal .controls .enabled { opacity: 0.7; cursor: pointer; } .reveal .controls .enabled:active { margin-top: 1px; } .reveal .controls .navigate-left { top: 42px; border-right-width: 22px; border-right-color: #000; } .reveal .controls .navigate-left.fragmented { opacity: 0.3; } .reveal .controls .navigate-right { left: 74px; top: 42px; border-left-width: 22px; border-left-color: #000; } .reveal .controls .navigate-right.fragmented { opacity: 0.3; } .reveal .controls .navigate-up { left: 42px; border-bottom-width: 22px; border-bottom-color: #000; } .reveal .controls .navigate-up.fragmented { opacity: 0.3; } .reveal .controls .navigate-down { left: 42px; top: 74px; border-top-width: 22px; border-top-color: #000; } .reveal .controls .navigate-down.fragmented { opacity: 0.3; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { position: fixed; display: none; height: 3px; width: 100%; bottom: 0; left: 0; z-index: 10; background-color: rgba(0, 0, 0, 0.2); } .reveal .progress:after { content: ''; display: block; position: absolute; height: 20px; width: 100%; top: -20px; } .reveal .progress span { display: block; height: 100%; width: 0px; background-color: #000; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } /********************************************* * SLIDE NUMBER *********************************************/ .reveal .slide-number { position: fixed; display: block; right: 8px; bottom: 8px; z-index: 31; font-family: Helvetica, sans-serif; font-size: 12px; line-height: 1; color: #fff; background-color: rgba(0, 0, 0, 0.4); padding: 5px; } .reveal .slide-number-delimiter { margin: 0 3px; } /********************************************* * SLIDES *********************************************/ .reveal { position: relative; width: 100%; height: 100%; overflow: hidden; -ms-touch-action: none; touch-action: none; } .reveal .slides { position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; margin: auto; overflow: visible; z-index: 1; text-align: center; -webkit-perspective: 600px; perspective: 600px; -webkit-perspective-origin: 50% 40%; perspective-origin: 50% 40%; } .reveal .slides > section { -ms-perspective: 600px; } .reveal .slides > section, .reveal .slides > section > section { display: none; position: absolute; width: 100%; padding: 20px 0px; z-index: 10; -webkit-transform-style: preserve-3d; transform-style: preserve-3d; -webkit-transition: -webkit-transform-origin 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), -webkit-transform 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), visibility 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), opacity 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: -ms-transform-origin 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), transform 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), visibility 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), opacity 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: transform-origin 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), transform 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), visibility 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985), opacity 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } /* Global transition speed settings */ .reveal[data-transition-speed="fast"] .slides section { -webkit-transition-duration: 400ms; transition-duration: 400ms; } .reveal[data-transition-speed="slow"] .slides section { -webkit-transition-duration: 1200ms; transition-duration: 1200ms; } /* Slide-specific transition speed overrides */ .reveal .slides section[data-transition-speed="fast"] { -webkit-transition-duration: 400ms; transition-duration: 400ms; } .reveal .slides section[data-transition-speed="slow"] { -webkit-transition-duration: 1200ms; transition-duration: 1200ms; } .reveal .slides > section.stack { padding-top: 0; padding-bottom: 0; } .reveal .slides > section.present, .reveal .slides > section > section.present { display: block; z-index: 11; opacity: 1; } .reveal.center, .reveal.center .slides, .reveal.center .slides section { min-height: 0 !important; } /* Don't allow interaction with invisible slides */ .reveal .slides > section.future, .reveal .slides > section > section.future, .reveal .slides > section.past, .reveal .slides > section > section.past { pointer-events: none; } .reveal.overview .slides > section, .reveal.overview .slides > section > section { pointer-events: auto; } .reveal .slides > section.past, .reveal .slides > section.future, .reveal .slides > section > section.past, .reveal .slides > section > section.future { opacity: 0; } /********************************************* * Mixins for readability of transitions *********************************************/ /********************************************* * SLIDE TRANSITION * Aliased 'linear' for backwards compatibility *********************************************/ .reveal.slide section { -webkit-backface-visibility: hidden; backface-visibility: hidden; } .reveal .slides > section[data-transition=slide].past, .reveal .slides > section[data-transition~=slide-out].past, .reveal.slide .slides > section:not([data-transition]).past { -webkit-transform: translate(-150%, 0); -ms-transform: translate(-150%, 0); transform: translate(-150%, 0); } .reveal .slides > section[data-transition=slide].future, .reveal .slides > section[data-transition~=slide-in].future, .reveal.slide .slides > section:not([data-transition]).future { -webkit-transform: translate(150%, 0); -ms-transform: translate(150%, 0); transform: translate(150%, 0); } .reveal .slides > section > section[data-transition=slide].past, .reveal .slides > section > section[data-transition~=slide-out].past, .reveal.slide .slides > section > section:not([data-transition]).past { -webkit-transform: translate(0, -150%); -ms-transform: translate(0, -150%); transform: translate(0, -150%); } .reveal .slides > section > section[data-transition=slide].future, .reveal .slides > section > section[data-transition~=slide-in].future, .reveal.slide .slides > section > section:not([data-transition]).future { -webkit-transform: translate(0, 150%); -ms-transform: translate(0, 150%); transform: translate(0, 150%); } .reveal.linear section { -webkit-backface-visibility: hidden; backface-visibility: hidden; } .reveal .slides > section[data-transition=linear].past, .reveal .slides > section[data-transition~=linear-out].past, .reveal.linear .slides > section:not([data-transition]).past { -webkit-transform: translate(-150%, 0); -ms-transform: translate(-150%, 0); transform: translate(-150%, 0); } .reveal .slides > section[data-transition=linear].future, .reveal .slides > section[data-transition~=linear-in].future, .reveal.linear .slides > section:not([data-transition]).future { -webkit-transform: translate(150%, 0); -ms-transform: translate(150%, 0); transform: translate(150%, 0); } .reveal .slides > section > section[data-transition=linear].past, .reveal .slides > section > section[data-transition~=linear-out].past, .reveal.linear .slides > section > section:not([data-transition]).past { -webkit-transform: translate(0, -150%); -ms-transform: translate(0, -150%); transform: translate(0, -150%); } .reveal .slides > section > section[data-transition=linear].future, .reveal .slides > section > section[data-transition~=linear-in].future, .reveal.linear .slides > section > section:not([data-transition]).future { -webkit-transform: translate(0, 150%); -ms-transform: translate(0, 150%); transform: translate(0, 150%); } /********************************************* * CONVEX TRANSITION * Aliased 'default' for backwards compatibility *********************************************/ .reveal .slides > section[data-transition=default].past, .reveal .slides > section[data-transition~=default-out].past, .reveal.default .slides > section:not([data-transition]).past { -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); } .reveal .slides > section[data-transition=default].future, .reveal .slides > section[data-transition~=default-in].future, .reveal.default .slides > section:not([data-transition]).future { -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); } .reveal .slides > section > section[data-transition=default].past, .reveal .slides > section > section[data-transition~=default-out].past, .reveal.default .slides > section > section:not([data-transition]).past { -webkit-transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0); transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0); } .reveal .slides > section > section[data-transition=default].future, .reveal .slides > section > section[data-transition~=default-in].future, .reveal.default .slides > section > section:not([data-transition]).future { -webkit-transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0); transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0); } .reveal .slides > section[data-transition=convex].past, .reveal .slides > section[data-transition~=convex-out].past, .reveal.convex .slides > section:not([data-transition]).past { -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); } .reveal .slides > section[data-transition=convex].future, .reveal .slides > section[data-transition~=convex-in].future, .reveal.convex .slides > section:not([data-transition]).future { -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); } .reveal .slides > section > section[data-transition=convex].past, .reveal .slides > section > section[data-transition~=convex-out].past, .reveal.convex .slides > section > section:not([data-transition]).past { -webkit-transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0); transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0); } .reveal .slides > section > section[data-transition=convex].future, .reveal .slides > section > section[data-transition~=convex-in].future, .reveal.convex .slides > section > section:not([data-transition]).future { -webkit-transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0); transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0); } /********************************************* * CONCAVE TRANSITION *********************************************/ .reveal .slides > section[data-transition=concave].past, .reveal .slides > section[data-transition~=concave-out].past, .reveal.concave .slides > section:not([data-transition]).past { -webkit-transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); } .reveal .slides > section[data-transition=concave].future, .reveal .slides > section[data-transition~=concave-in].future, .reveal.concave .slides > section:not([data-transition]).future { -webkit-transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); } .reveal .slides > section > section[data-transition=concave].past, .reveal .slides > section > section[data-transition~=concave-out].past, .reveal.concave .slides > section > section:not([data-transition]).past { -webkit-transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0); transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0); } .reveal .slides > section > section[data-transition=concave].future, .reveal .slides > section > section[data-transition~=concave-in].future, .reveal.concave .slides > section > section:not([data-transition]).future { -webkit-transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0); transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0); } /********************************************* * ZOOM TRANSITION *********************************************/ .reveal .slides section[data-transition=zoom], .reveal.zoom .slides section:not([data-transition]) { -webkit-transition-timing-function: ease; transition-timing-function: ease; } .reveal .slides > section[data-transition=zoom].past, .reveal .slides > section[data-transition~=zoom-out].past, .reveal.zoom .slides > section:not([data-transition]).past { visibility: hidden; -webkit-transform: scale(16); -ms-transform: scale(16); transform: scale(16); } .reveal .slides > section[data-transition=zoom].future, .reveal .slides > section[data-transition~=zoom-in].future, .reveal.zoom .slides > section:not([data-transition]).future { visibility: hidden; -webkit-transform: scale(0.2); -ms-transform: scale(0.2); transform: scale(0.2); } .reveal .slides > section > section[data-transition=zoom].past, .reveal .slides > section > section[data-transition~=zoom-out].past, .reveal.zoom .slides > section > section:not([data-transition]).past { -webkit-transform: translate(0, -150%); -ms-transform: translate(0, -150%); transform: translate(0, -150%); } .reveal .slides > section > section[data-transition=zoom].future, .reveal .slides > section > section[data-transition~=zoom-in].future, .reveal.zoom .slides > section > section:not([data-transition]).future { -webkit-transform: translate(0, 150%); -ms-transform: translate(0, 150%); transform: translate(0, 150%); } /********************************************* * CUBE TRANSITION *********************************************/ .reveal.cube .slides { -webkit-perspective: 1300px; perspective: 1300px; } .reveal.cube .slides section { padding: 30px; min-height: 700px; -webkit-backface-visibility: hidden; backface-visibility: hidden; box-sizing: border-box; } .reveal.center.cube .slides section { min-height: 0; } .reveal.cube .slides section:not(.stack):before { content: ''; position: absolute; display: block; width: 100%; height: 100%; left: 0; top: 0; background: rgba(0, 0, 0, 0.1); border-radius: 4px; -webkit-transform: translateZ(-20px); transform: translateZ(-20px); } .reveal.cube .slides section:not(.stack):after { content: ''; position: absolute; display: block; width: 90%; height: 30px; left: 5%; bottom: 0; background: none; z-index: 1; border-radius: 4px; box-shadow: 0px 95px 25px rgba(0, 0, 0, 0.2); -webkit-transform: translateZ(-90px) rotateX(65deg); transform: translateZ(-90px) rotateX(65deg); } .reveal.cube .slides > section.stack { padding: 0; background: none; } .reveal.cube .slides > section.past { -webkit-transform-origin: 100% 0%; -ms-transform-origin: 100% 0%; transform-origin: 100% 0%; -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg); transform: translate3d(-100%, 0, 0) rotateY(-90deg); } .reveal.cube .slides > section.future { -webkit-transform-origin: 0% 0%; -ms-transform-origin: 0% 0%; transform-origin: 0% 0%; -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg); transform: translate3d(100%, 0, 0) rotateY(90deg); } .reveal.cube .slides > section > section.past { -webkit-transform-origin: 0% 100%; -ms-transform-origin: 0% 100%; transform-origin: 0% 100%; -webkit-transform: translate3d(0, -100%, 0) rotateX(90deg); transform: translate3d(0, -100%, 0) rotateX(90deg); } .reveal.cube .slides > section > section.future { -webkit-transform-origin: 0% 0%; -ms-transform-origin: 0% 0%; transform-origin: 0% 0%; -webkit-transform: translate3d(0, 100%, 0) rotateX(-90deg); transform: translate3d(0, 100%, 0) rotateX(-90deg); } /********************************************* * PAGE TRANSITION *********************************************/ .reveal.page .slides { -webkit-perspective-origin: 0% 50%; perspective-origin: 0% 50%; -webkit-perspective: 3000px; perspective: 3000px; } .reveal.page .slides section { padding: 30px; min-height: 700px; box-sizing: border-box; } .reveal.page .slides section.past { z-index: 12; } .reveal.page .slides section:not(.stack):before { content: ''; position: absolute; display: block; width: 100%; height: 100%; left: 0; top: 0; background: rgba(0, 0, 0, 0.1); -webkit-transform: translateZ(-20px); transform: translateZ(-20px); } .reveal.page .slides section:not(.stack):after { content: ''; position: absolute; display: block; width: 90%; height: 30px; left: 5%; bottom: 0; background: none; z-index: 1; border-radius: 4px; box-shadow: 0px 95px 25px rgba(0, 0, 0, 0.2); -webkit-transform: translateZ(-90px) rotateX(65deg); } .reveal.page .slides > section.stack { padding: 0; background: none; } .reveal.page .slides > section.past { -webkit-transform-origin: 0% 0%; -ms-transform-origin: 0% 0%; transform-origin: 0% 0%; -webkit-transform: translate3d(-40%, 0, 0) rotateY(-80deg); transform: translate3d(-40%, 0, 0) rotateY(-80deg); } .reveal.page .slides > section.future { -webkit-transform-origin: 100% 0%; -ms-transform-origin: 100% 0%; transform-origin: 100% 0%; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } .reveal.page .slides > section > section.past { -webkit-transform-origin: 0% 0%; -ms-transform-origin: 0% 0%; transform-origin: 0% 0%; -webkit-transform: translate3d(0, -40%, 0) rotateX(80deg); transform: translate3d(0, -40%, 0) rotateX(80deg); } .reveal.page .slides > section > section.future { -webkit-transform-origin: 0% 100%; -ms-transform-origin: 0% 100%; transform-origin: 0% 100%; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } /********************************************* * FADE TRANSITION *********************************************/ .reveal .slides section[data-transition=fade], .reveal.fade .slides section:not([data-transition]), .reveal.fade .slides > section > section:not([data-transition]) { -webkit-transform: none; -ms-transform: none; transform: none; -webkit-transition: opacity 0.5s; transition: opacity 0.5s; } .reveal.fade.overview .slides section, .reveal.fade.overview .slides > section > section { -webkit-transition: none; transition: none; } /********************************************* * NO TRANSITION *********************************************/ .reveal .slides section[data-transition=none], .reveal.none .slides section:not([data-transition]) { -webkit-transform: none; -ms-transform: none; transform: none; -webkit-transition: none; transition: none; } /********************************************* * PAUSED MODE *********************************************/ .reveal .pause-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: black; visibility: hidden; opacity: 0; z-index: 100; -webkit-transition: all 1s ease; transition: all 1s ease; } .reveal.paused .pause-overlay { visibility: visible; opacity: 1; } /********************************************* * FALLBACK *********************************************/ .no-transforms { overflow-y: auto; } .no-transforms .reveal .slides { position: relative; width: 80%; height: auto !important; top: 0; left: 50%; margin: 0; text-align: center; } .no-transforms .reveal .controls, .no-transforms .reveal .progress { display: none !important; } .no-transforms .reveal .slides section { display: block !important; opacity: 1 !important; position: relative !important; height: auto; min-height: 0; top: 0; left: -50%; margin: 70px 0; -webkit-transform: none; -ms-transform: none; transform: none; } .no-transforms .reveal .slides section section { left: 0; } .reveal .no-transition, .reveal .no-transition * { -webkit-transition: none !important; transition: none !important; } /********************************************* * PER-SLIDE BACKGROUNDS *********************************************/ .reveal .backgrounds { position: absolute; width: 100%; height: 100%; top: 0; left: 0; -webkit-perspective: 600px; perspective: 600px; } .reveal .slide-background { display: none; position: absolute; width: 100%; height: 100%; opacity: 0; visibility: hidden; background-color: transparent; background-position: 50% 50%; background-repeat: no-repeat; background-size: cover; -webkit-transition: all 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: all 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } .reveal .slide-background.stack { display: block; } .reveal .slide-background.present { opacity: 1; visibility: visible; } .print-pdf .reveal .slide-background { opacity: 1 !important; visibility: visible !important; } /* Video backgrounds */ .reveal .slide-background video { position: absolute; width: 100%; height: 100%; max-width: none; max-height: none; top: 0; left: 0; } /* Immediate transition style */ .reveal[data-background-transition=none] > .backgrounds .slide-background, .reveal > .backgrounds .slide-background[data-background-transition=none] { -webkit-transition: none; transition: none; } /* Slide */ .reveal[data-background-transition=slide] > .backgrounds .slide-background, .reveal > .backgrounds .slide-background[data-background-transition=slide] { opacity: 1; -webkit-backface-visibility: hidden; backface-visibility: hidden; } .reveal[data-background-transition=slide] > .backgrounds .slide-background.past, .reveal > .backgrounds .slide-background.past[data-background-transition=slide] { -webkit-transform: translate(-100%, 0); -ms-transform: translate(-100%, 0); transform: translate(-100%, 0); } .reveal[data-background-transition=slide] > .backgrounds .slide-background.future, .reveal > .backgrounds .slide-background.future[data-background-transition=slide] { -webkit-transform: translate(100%, 0); -ms-transform: translate(100%, 0); transform: translate(100%, 0); } .reveal[data-background-transition=slide] > .backgrounds .slide-background > .slide-background.past, .reveal > .backgrounds .slide-background > .slide-background.past[data-background-transition=slide] { -webkit-transform: translate(0, -100%); -ms-transform: translate(0, -100%); transform: translate(0, -100%); } .reveal[data-background-transition=slide] > .backgrounds .slide-background > .slide-background.future, .reveal > .backgrounds .slide-background > .slide-background.future[data-background-transition=slide] { -webkit-transform: translate(0, 100%); -ms-transform: translate(0, 100%); transform: translate(0, 100%); } /* Convex */ .reveal[data-background-transition=convex] > .backgrounds .slide-background.past, .reveal > .backgrounds .slide-background.past[data-background-transition=convex] { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); } .reveal[data-background-transition=convex] > .backgrounds .slide-background.future, .reveal > .backgrounds .slide-background.future[data-background-transition=convex] { opacity: 0; -webkit-transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); } .reveal[data-background-transition=convex] > .backgrounds .slide-background > .slide-background.past, .reveal > .backgrounds .slide-background > .slide-background.past[data-background-transition=convex] { opacity: 0; -webkit-transform: translate3d(0, -100%, 0) rotateX(90deg) translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0) rotateX(90deg) translate3d(0, -100%, 0); } .reveal[data-background-transition=convex] > .backgrounds .slide-background > .slide-background.future, .reveal > .backgrounds .slide-background > .slide-background.future[data-background-transition=convex] { opacity: 0; -webkit-transform: translate3d(0, 100%, 0) rotateX(-90deg) translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0) rotateX(-90deg) translate3d(0, 100%, 0); } /* Concave */ .reveal[data-background-transition=concave] > .backgrounds .slide-background.past, .reveal > .backgrounds .slide-background.past[data-background-transition=concave] { opacity: 0; -webkit-transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); } .reveal[data-background-transition=concave] > .backgrounds .slide-background.future, .reveal > .backgrounds .slide-background.future[data-background-transition=concave] { opacity: 0; -webkit-transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); } .reveal[data-background-transition=concave] > .backgrounds .slide-background > .slide-background.past, .reveal > .backgrounds .slide-background > .slide-background.past[data-background-transition=concave] { opacity: 0; -webkit-transform: translate3d(0, -100%, 0) rotateX(-90deg) translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0) rotateX(-90deg) translate3d(0, -100%, 0); } .reveal[data-background-transition=concave] > .backgrounds .slide-background > .slide-background.future, .reveal > .backgrounds .slide-background > .slide-background.future[data-background-transition=concave] { opacity: 0; -webkit-transform: translate3d(0, 100%, 0) rotateX(90deg) translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0) rotateX(90deg) translate3d(0, 100%, 0); } /* Zoom */ .reveal[data-background-transition=zoom] > .backgrounds .slide-background, .reveal > .backgrounds .slide-background[data-background-transition=zoom] { -webkit-transition-timing-function: ease; transition-timing-function: ease; } .reveal[data-background-transition=zoom] > .backgrounds .slide-background.past, .reveal > .backgrounds .slide-background.past[data-background-transition=zoom] { opacity: 0; visibility: hidden; -webkit-transform: scale(16); -ms-transform: scale(16); transform: scale(16); } .reveal[data-background-transition=zoom] > .backgrounds .slide-background.future, .reveal > .backgrounds .slide-background.future[data-background-transition=zoom] { opacity: 0; visibility: hidden; -webkit-transform: scale(0.2); -ms-transform: scale(0.2); transform: scale(0.2); } .reveal[data-background-transition=zoom] > .backgrounds .slide-background > .slide-background.past, .reveal > .backgrounds .slide-background > .slide-background.past[data-background-transition=zoom] { opacity: 0; visibility: hidden; -webkit-transform: scale(16); -ms-transform: scale(16); transform: scale(16); } .reveal[data-background-transition=zoom] > .backgrounds .slide-background > .slide-background.future, .reveal > .backgrounds .slide-background > .slide-background.future[data-background-transition=zoom] { opacity: 0; visibility: hidden; -webkit-transform: scale(0.2); -ms-transform: scale(0.2); transform: scale(0.2); } /* Global transition speed settings */ .reveal[data-transition-speed="fast"] > .backgrounds .slide-background { -webkit-transition-duration: 400ms; transition-duration: 400ms; } .reveal[data-transition-speed="slow"] > .backgrounds .slide-background { -webkit-transition-duration: 1200ms; transition-duration: 1200ms; } /********************************************* * OVERVIEW *********************************************/ .reveal.overview { -webkit-perspective-origin: 50% 50%; perspective-origin: 50% 50%; -webkit-perspective: 700px; perspective: 700px; } .reveal.overview .slides section { height: 700px; opacity: 1 !important; overflow: hidden; visibility: visible !important; cursor: pointer; box-sizing: border-box; } .reveal.overview .slides section:hover, .reveal.overview .slides section.present { outline: 10px solid rgba(150, 150, 150, 0.4); outline-offset: 10px; } .reveal.overview .slides section .fragment { opacity: 1; -webkit-transition: none; transition: none; } .reveal.overview .slides section:after, .reveal.overview .slides section:before { display: none !important; } .reveal.overview .slides > section.stack { padding: 0; top: 0 !important; background: none; outline: none; overflow: visible; } .reveal.overview .backgrounds { -webkit-perspective: inherit; perspective: inherit; } .reveal.overview .backgrounds .slide-background { opacity: 1; visibility: visible; outline: 10px solid rgba(150, 150, 150, 0.1); outline-offset: 10px; } .reveal.overview .slides section, .reveal.overview-deactivating .slides section { -webkit-transition: none; transition: none; } .reveal.overview .backgrounds .slide-background, .reveal.overview-deactivating .backgrounds .slide-background { -webkit-transition: none; transition: none; } .reveal.overview-animated .slides { -webkit-transition: -webkit-transform 0.4s ease; transition: transform 0.4s ease; } /********************************************* * RTL SUPPORT *********************************************/ .reveal.rtl .slides, .reveal.rtl .slides h1, .reveal.rtl .slides h2, .reveal.rtl .slides h3, .reveal.rtl .slides h4, .reveal.rtl .slides h5, .reveal.rtl .slides h6 { direction: rtl; font-family: sans-serif; } .reveal.rtl pre, .reveal.rtl code { direction: ltr; } .reveal.rtl ol, .reveal.rtl ul { text-align: right; } .reveal.rtl .progress span { float: right; } /********************************************* * PARALLAX BACKGROUND *********************************************/ .reveal.has-parallax-background .backgrounds { -webkit-transition: all 0.8s ease; transition: all 0.8s ease; } /* Global transition speed settings */ .reveal.has-parallax-background[data-transition-speed="fast"] .backgrounds { -webkit-transition-duration: 400ms; transition-duration: 400ms; } .reveal.has-parallax-background[data-transition-speed="slow"] .backgrounds { -webkit-transition-duration: 1200ms; transition-duration: 1200ms; } /********************************************* * LINK PREVIEW OVERLAY *********************************************/ .reveal .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; background: rgba(0, 0, 0, 0.9); opacity: 0; visibility: hidden; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; } .reveal .overlay.visible { opacity: 1; visibility: visible; } .reveal .overlay .spinner { position: absolute; display: block; top: 50%; left: 50%; width: 32px; height: 32px; margin: -16px 0 0 -16px; z-index: 10; background-image: url(data:image/gif;base64,R0lGODlhIAAgAPMAAJmZmf%2F%2F%2F6%2Bvr8nJybW1tcDAwOjo6Nvb26ioqKOjo7Ozs%2FLy8vz8%2FAAAAAAAAAAAACH%2FC05FVFNDQVBFMi4wAwEAAAAh%2FhpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh%2BQQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ%2FV%2FnmOM82XiHRLYKhKP1oZmADdEAAAh%2BQQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY%2FCZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB%2BA4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6%2BHo7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq%2BB6QDtuetcaBPnW6%2BO7wDHpIiK9SaVK5GgV543tzjgGcghAgAh%2BQQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK%2B%2BG%2Bw48edZPK%2BM6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE%2BG%2BcD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm%2BFNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk%2BaV%2BoJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0%2FVNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc%2BXiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30%2FiI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE%2FjiuL04RGEBgwWhShRgQExHBAAh%2BQQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR%2BipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY%2BYip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd%2BMFCN6HAAIKgNggY0KtEBAAh%2BQQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1%2BvsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d%2BjYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg%2BygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0%2Bbm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h%2BKr0SJ8MFihpNbx%2B4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX%2BBP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA%3D%3D); visibility: visible; opacity: 0.6; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; } .reveal .overlay header { position: absolute; left: 0; top: 0; width: 100%; height: 40px; z-index: 2; border-bottom: 1px solid #222; } .reveal .overlay header a { display: inline-block; width: 40px; height: 40px; padding: 0 10px; float: right; opacity: 0.6; box-sizing: border-box; } .reveal .overlay header a:hover { opacity: 1; } .reveal .overlay header a .icon { display: inline-block; width: 20px; height: 20px; background-position: 50% 50%; background-size: 100%; background-repeat: no-repeat; } .reveal .overlay header a.close .icon { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABkklEQVRYR8WX4VHDMAxG6wnoJrABZQPYBCaBTWAD2g1gE5gg6OOsXuxIlr40d81dfrSJ9V4c2VLK7spHuTJ/5wpM07QXuXc5X0opX2tEJcadjHuV80li/FgxTIEK/5QBCICBD6xEhSMGHgQPgBgLiYVAB1dpSqKDawxTohFw4JSEA3clzgIBPCURwE2JucBR7rhPJJv5OpJwDX+SfDjgx1wACQeJG1aChP9K/IMmdZ8DtESV1WyP3Bt4MwM6sj4NMxMYiqUWHQu4KYA/SYkIjOsm3BXYWMKFDwU2khjCQ4ELJUJ4SmClRArOCmSXGuKma0fYD5CbzHxFpCSGAhfAVSSUGDUk2BWZaff2g6GE15BsBQ9nwmpIGDiyHQddwNTMKkbZaf9fajXQca1EX44puJZUsnY0ObGmITE3GVLCbEhQUjGVt146j6oasWN+49Vph2w1pZ5EansNZqKBm1txbU57iRRcZ86RWMDdWtBJUHBHwoQPi1GV+JCbntmvok7iTX4/Up9mgyTc/FJYDTcndgH/AA5A/CHsyEkVAAAAAElFTkSuQmCC); } .reveal .overlay header a.external .icon { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAcElEQVRYR+2WSQoAIQwEzf8f7XiOMkUQxUPlGkM3hVmiQfQR9GYnH1SsAQlI4DiBqkCMoNb9y2e90IAEJPAcgdznU9+engMaeJ7Azh5Y1U67gAho4DqBqmB1buAf0MB1AlVBek83ZPkmJMGc1wAR+AAqod/B97TRpQAAAABJRU5ErkJggg==); } .reveal .overlay .viewport { position: absolute; top: 40px; right: 0; bottom: 0; left: 0; } .reveal .overlay.overlay-preview .viewport iframe { width: 100%; height: 100%; max-width: 100%; max-height: 100%; border: 0; opacity: 0; visibility: hidden; -webkit-transition: all 0.3s ease; transition: all 0.3s ease; } .reveal .overlay.overlay-preview.loaded .viewport iframe { opacity: 1; visibility: visible; } .reveal .overlay.overlay-preview.loaded .spinner { opacity: 0; visibility: hidden; -webkit-transform: scale(0.2); -ms-transform: scale(0.2); transform: scale(0.2); } .reveal .overlay.overlay-help .viewport { overflow: auto; color: #fff; } .reveal .overlay.overlay-help .viewport .viewport-inner { width: 600px; margin: 0 auto; padding: 60px; text-align: center; letter-spacing: normal; } .reveal .overlay.overlay-help .viewport .viewport-inner .title { font-size: 20px; } .reveal .overlay.overlay-help .viewport .viewport-inner table { border: 1px solid #fff; border-collapse: collapse; font-size: 14px; } .reveal .overlay.overlay-help .viewport .viewport-inner table th, .reveal .overlay.overlay-help .viewport .viewport-inner table td { width: 200px; padding: 10px; border: 1px solid #fff; vertical-align: middle; } .reveal .overlay.overlay-help .viewport .viewport-inner table th { padding-top: 20px; padding-bottom: 20px; } /********************************************* * PLAYBACK COMPONENT *********************************************/ .reveal .playback { position: fixed; left: 15px; bottom: 20px; z-index: 30; cursor: pointer; -webkit-transition: all 400ms ease; transition: all 400ms ease; } .reveal.overview .playback { opacity: 0; visibility: hidden; } /********************************************* * ROLLING LINKS *********************************************/ .reveal .roll { display: inline-block; line-height: 1.2; overflow: hidden; vertical-align: top; -webkit-perspective: 400px; perspective: 400px; -webkit-perspective-origin: 50% 50%; perspective-origin: 50% 50%; } .reveal .roll:hover { background: none; text-shadow: none; } .reveal .roll span { display: block; position: relative; padding: 0 2px; pointer-events: none; -webkit-transition: all 400ms ease; transition: all 400ms ease; -webkit-transform-origin: 50% 0%; -ms-transform-origin: 50% 0%; transform-origin: 50% 0%; -webkit-transform-style: preserve-3d; transform-style: preserve-3d; -webkit-backface-visibility: hidden; backface-visibility: hidden; } .reveal .roll:hover span { background: rgba(0, 0, 0, 0.5); -webkit-transform: translate3d(0px, 0px, -45px) rotateX(90deg); transform: translate3d(0px, 0px, -45px) rotateX(90deg); } .reveal .roll span:after { content: attr(data-title); display: block; position: absolute; left: 0; top: 0; padding: 0 2px; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-transform-origin: 50% 0%; -ms-transform-origin: 50% 0%; transform-origin: 50% 0%; -webkit-transform: translate3d(0px, 110%, 0px) rotateX(-90deg); transform: translate3d(0px, 110%, 0px) rotateX(-90deg); } /********************************************* * SPEAKER NOTES *********************************************/ .reveal aside.notes { display: none; } .reveal .speaker-notes { display: none; position: absolute; width: 70%; max-height: 15%; left: 15%; bottom: 26px; padding: 10px; z-index: 1; font-size: 18px; line-height: 1.4; color: #fff; background-color: rgba(0, 0, 0, 0.5); overflow: auto; box-sizing: border-box; text-align: left; font-family: Helvetica, sans-serif; -webkit-overflow-scrolling: touch; } .reveal .speaker-notes.visible:not(:empty) { display: block; } @media screen and (max-width: 1024px) { .reveal .speaker-notes { font-size: 14px; } } @media screen and (max-width: 600px) { .reveal .speaker-notes { width: 90%; left: 5%; } } /********************************************* * ZOOM PLUGIN *********************************************/ .zoomed .reveal *, .zoomed .reveal *:before, .zoomed .reveal *:after { -webkit-backface-visibility: visible !important; backface-visibility: visible !important; } .zoomed .reveal .progress, .zoomed .reveal .controls { opacity: 0; } .zoomed .reveal .roll span { background: none; } .zoomed .reveal .roll span:after { visibility: hidden; } ================================================ FILE: test/client_server_SUITE_data/css/reveal.scss ================================================ /*! * reveal.js * http://lab.hakim.se/reveal-js * MIT licensed * * Copyright (C) 2015 Hakim El Hattab, http://hakim.se */ /********************************************* * RESET STYLES *********************************************/ html, body, .reveal div, .reveal span, .reveal applet, .reveal object, .reveal iframe, .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6, .reveal p, .reveal blockquote, .reveal pre, .reveal a, .reveal abbr, .reveal acronym, .reveal address, .reveal big, .reveal cite, .reveal code, .reveal del, .reveal dfn, .reveal em, .reveal img, .reveal ins, .reveal kbd, .reveal q, .reveal s, .reveal samp, .reveal small, .reveal strike, .reveal strong, .reveal sub, .reveal sup, .reveal tt, .reveal var, .reveal b, .reveal u, .reveal center, .reveal dl, .reveal dt, .reveal dd, .reveal ol, .reveal ul, .reveal li, .reveal fieldset, .reveal form, .reveal label, .reveal legend, .reveal table, .reveal caption, .reveal tbody, .reveal tfoot, .reveal thead, .reveal tr, .reveal th, .reveal td, .reveal article, .reveal aside, .reveal canvas, .reveal details, .reveal embed, .reveal figure, .reveal figcaption, .reveal footer, .reveal header, .reveal hgroup, .reveal menu, .reveal nav, .reveal output, .reveal ruby, .reveal section, .reveal summary, .reveal time, .reveal mark, .reveal audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } .reveal article, .reveal aside, .reveal details, .reveal figcaption, .reveal figure, .reveal footer, .reveal header, .reveal hgroup, .reveal menu, .reveal nav, .reveal section { display: block; } /********************************************* * GLOBAL STYLES *********************************************/ html, body { width: 100%; height: 100%; overflow: hidden; } body { position: relative; line-height: 1; background-color: #fff; color: #000; } // Ensures that the main background color matches the // theme in fullscreen mode html:-webkit-full-screen-ancestor { background-color: inherit; } html:-moz-full-screen-ancestor { background-color: inherit; } /********************************************* * VIEW FRAGMENTS *********************************************/ .reveal .slides section .fragment { opacity: 0; visibility: hidden; transition: all .2s ease; &.visible { opacity: 1; visibility: visible; } } .reveal .slides section .fragment.grow { opacity: 1; visibility: visible; &.visible { transform: scale( 1.3 ); } } .reveal .slides section .fragment.shrink { opacity: 1; visibility: visible; &.visible { transform: scale( 0.7 ); } } .reveal .slides section .fragment.zoom-in { transform: scale( 0.1 ); &.visible { transform: none; } } .reveal .slides section .fragment.fade-out { opacity: 1; visibility: visible; &.visible { opacity: 0; visibility: hidden; } } .reveal .slides section .fragment.semi-fade-out { opacity: 1; visibility: visible; &.visible { opacity: 0.5; visibility: visible; } } .reveal .slides section .fragment.strike { opacity: 1; visibility: visible; &.visible { text-decoration: line-through; } } .reveal .slides section .fragment.current-visible { opacity: 0; visibility: hidden; &.current-fragment { opacity: 1; visibility: visible; } } .reveal .slides section .fragment.highlight-red, .reveal .slides section .fragment.highlight-current-red, .reveal .slides section .fragment.highlight-green, .reveal .slides section .fragment.highlight-current-green, .reveal .slides section .fragment.highlight-blue, .reveal .slides section .fragment.highlight-current-blue { opacity: 1; visibility: visible; } .reveal .slides section .fragment.highlight-red.visible { color: #ff2c2d } .reveal .slides section .fragment.highlight-green.visible { color: #17ff2e; } .reveal .slides section .fragment.highlight-blue.visible { color: #1b91ff; } .reveal .slides section .fragment.highlight-current-red.current-fragment { color: #ff2c2d } .reveal .slides section .fragment.highlight-current-green.current-fragment { color: #17ff2e; } .reveal .slides section .fragment.highlight-current-blue.current-fragment { color: #1b91ff; } /********************************************* * DEFAULT ELEMENT STYLES *********************************************/ /* Fixes issue in Chrome where italic fonts did not appear when printing to PDF */ .reveal:after { content: ''; font-style: italic; } .reveal iframe { z-index: 1; } /** Prevents layering issues in certain browser/transition combinations */ .reveal a { position: relative; } .reveal .stretch { max-width: none; max-height: none; } .reveal pre.stretch code { height: 100%; max-height: 100%; box-sizing: border-box; } /********************************************* * CONTROLS *********************************************/ .reveal .controls { display: none; position: fixed; width: 110px; height: 110px; z-index: 30; right: 10px; bottom: 10px; -webkit-user-select: none; } .reveal .controls button { padding: 0; position: absolute; opacity: 0.05; width: 0; height: 0; background-color: transparent; border: 12px solid transparent; transform: scale(.9999); transition: all 0.2s ease; -webkit-appearance: none; -webkit-tap-highlight-color: rgba( 0, 0, 0, 0 ); } .reveal .controls .enabled { opacity: 0.7; cursor: pointer; } .reveal .controls .enabled:active { margin-top: 1px; } .reveal .controls .navigate-left { top: 42px; border-right-width: 22px; border-right-color: #000; } .reveal .controls .navigate-left.fragmented { opacity: 0.3; } .reveal .controls .navigate-right { left: 74px; top: 42px; border-left-width: 22px; border-left-color: #000; } .reveal .controls .navigate-right.fragmented { opacity: 0.3; } .reveal .controls .navigate-up { left: 42px; border-bottom-width: 22px; border-bottom-color: #000; } .reveal .controls .navigate-up.fragmented { opacity: 0.3; } .reveal .controls .navigate-down { left: 42px; top: 74px; border-top-width: 22px; border-top-color: #000; } .reveal .controls .navigate-down.fragmented { opacity: 0.3; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { position: fixed; display: none; height: 3px; width: 100%; bottom: 0; left: 0; z-index: 10; background-color: rgba( 0, 0, 0, 0.2 ); } .reveal .progress:after { content: ''; display: block; position: absolute; height: 20px; width: 100%; top: -20px; } .reveal .progress span { display: block; height: 100%; width: 0px; background-color: #000; transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); } /********************************************* * SLIDE NUMBER *********************************************/ .reveal .slide-number { position: fixed; display: block; right: 8px; bottom: 8px; z-index: 31; font-family: Helvetica, sans-serif; font-size: 12px; line-height: 1; color: #fff; background-color: rgba( 0, 0, 0, 0.4 ); padding: 5px; } .reveal .slide-number-delimiter { margin: 0 3px; } /********************************************* * SLIDES *********************************************/ .reveal { position: relative; width: 100%; height: 100%; overflow: hidden; touch-action: none; } .reveal .slides { position: absolute; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; margin: auto; overflow: visible; z-index: 1; text-align: center; perspective: 600px; perspective-origin: 50% 40%; } .reveal .slides>section { -ms-perspective: 600px; } .reveal .slides>section, .reveal .slides>section>section { display: none; position: absolute; width: 100%; padding: 20px 0px; z-index: 10; transform-style: preserve-3d; transition: transform-origin 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985), transform 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985), visibility 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985), opacity 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); } /* Global transition speed settings */ .reveal[data-transition-speed="fast"] .slides section { transition-duration: 400ms; } .reveal[data-transition-speed="slow"] .slides section { transition-duration: 1200ms; } /* Slide-specific transition speed overrides */ .reveal .slides section[data-transition-speed="fast"] { transition-duration: 400ms; } .reveal .slides section[data-transition-speed="slow"] { transition-duration: 1200ms; } .reveal .slides>section.stack { padding-top: 0; padding-bottom: 0; } .reveal .slides>section.present, .reveal .slides>section>section.present { display: block; z-index: 11; opacity: 1; } .reveal.center, .reveal.center .slides, .reveal.center .slides section { min-height: 0 !important; } /* Don't allow interaction with invisible slides */ .reveal .slides>section.future, .reveal .slides>section>section.future, .reveal .slides>section.past, .reveal .slides>section>section.past { pointer-events: none; } .reveal.overview .slides>section, .reveal.overview .slides>section>section { pointer-events: auto; } .reveal .slides>section.past, .reveal .slides>section.future, .reveal .slides>section>section.past, .reveal .slides>section>section.future { opacity: 0; } /********************************************* * Mixins for readability of transitions *********************************************/ @mixin transition-global($style) { .reveal .slides section[data-transition=#{$style}], .reveal.#{$style} .slides section:not([data-transition]) { @content; } } @mixin transition-horizontal-past($style) { .reveal .slides>section[data-transition=#{$style}].past, .reveal .slides>section[data-transition~=#{$style}-out].past, .reveal.#{$style} .slides>section:not([data-transition]).past { @content; } } @mixin transition-horizontal-future($style) { .reveal .slides>section[data-transition=#{$style}].future, .reveal .slides>section[data-transition~=#{$style}-in].future, .reveal.#{$style} .slides>section:not([data-transition]).future { @content; } } @mixin transition-vertical-past($style) { .reveal .slides>section>section[data-transition=#{$style}].past, .reveal .slides>section>section[data-transition~=#{$style}-out].past, .reveal.#{$style} .slides>section>section:not([data-transition]).past { @content; } } @mixin transition-vertical-future($style) { .reveal .slides>section>section[data-transition=#{$style}].future, .reveal .slides>section>section[data-transition~=#{$style}-in].future, .reveal.#{$style} .slides>section>section:not([data-transition]).future { @content; } } /********************************************* * SLIDE TRANSITION * Aliased 'linear' for backwards compatibility *********************************************/ @each $stylename in slide, linear { .reveal.#{$stylename} section { backface-visibility: hidden; } @include transition-horizontal-past(#{$stylename}) { transform: translate(-150%, 0); } @include transition-horizontal-future(#{$stylename}) { transform: translate(150%, 0); } @include transition-vertical-past(#{$stylename}) { transform: translate(0, -150%); } @include transition-vertical-future(#{$stylename}) { transform: translate(0, 150%); } } /********************************************* * CONVEX TRANSITION * Aliased 'default' for backwards compatibility *********************************************/ @each $stylename in default, convex { @include transition-horizontal-past(#{$stylename}) { transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); } @include transition-horizontal-future(#{$stylename}) { transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); } @include transition-vertical-past(#{$stylename}) { transform: translate3d(0, -300px, 0) rotateX(70deg) translate3d(0, -300px, 0); } @include transition-vertical-future(#{$stylename}) { transform: translate3d(0, 300px, 0) rotateX(-70deg) translate3d(0, 300px, 0); } } /********************************************* * CONCAVE TRANSITION *********************************************/ @include transition-horizontal-past(concave) { transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); } @include transition-horizontal-future(concave) { transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); } @include transition-vertical-past(concave) { transform: translate3d(0, -80%, 0) rotateX(-70deg) translate3d(0, -80%, 0); } @include transition-vertical-future(concave) { transform: translate3d(0, 80%, 0) rotateX(70deg) translate3d(0, 80%, 0); } /********************************************* * ZOOM TRANSITION *********************************************/ @include transition-global(zoom) { transition-timing-function: ease; } @include transition-horizontal-past(zoom) { visibility: hidden; transform: scale(16); } @include transition-horizontal-future(zoom) { visibility: hidden; transform: scale(0.2); } @include transition-vertical-past(zoom) { transform: translate(0, -150%); } @include transition-vertical-future(zoom) { transform: translate(0, 150%); } /********************************************* * CUBE TRANSITION *********************************************/ .reveal.cube .slides { perspective: 1300px; } .reveal.cube .slides section { padding: 30px; min-height: 700px; backface-visibility: hidden; box-sizing: border-box; } .reveal.center.cube .slides section { min-height: 0; } .reveal.cube .slides section:not(.stack):before { content: ''; position: absolute; display: block; width: 100%; height: 100%; left: 0; top: 0; background: rgba(0,0,0,0.1); border-radius: 4px; transform: translateZ( -20px ); } .reveal.cube .slides section:not(.stack):after { content: ''; position: absolute; display: block; width: 90%; height: 30px; left: 5%; bottom: 0; background: none; z-index: 1; border-radius: 4px; box-shadow: 0px 95px 25px rgba(0,0,0,0.2); transform: translateZ(-90px) rotateX( 65deg ); } .reveal.cube .slides>section.stack { padding: 0; background: none; } .reveal.cube .slides>section.past { transform-origin: 100% 0%; transform: translate3d(-100%, 0, 0) rotateY(-90deg); } .reveal.cube .slides>section.future { transform-origin: 0% 0%; transform: translate3d(100%, 0, 0) rotateY(90deg); } .reveal.cube .slides>section>section.past { transform-origin: 0% 100%; transform: translate3d(0, -100%, 0) rotateX(90deg); } .reveal.cube .slides>section>section.future { transform-origin: 0% 0%; transform: translate3d(0, 100%, 0) rotateX(-90deg); } /********************************************* * PAGE TRANSITION *********************************************/ .reveal.page .slides { perspective-origin: 0% 50%; perspective: 3000px; } .reveal.page .slides section { padding: 30px; min-height: 700px; box-sizing: border-box; } .reveal.page .slides section.past { z-index: 12; } .reveal.page .slides section:not(.stack):before { content: ''; position: absolute; display: block; width: 100%; height: 100%; left: 0; top: 0; background: rgba(0,0,0,0.1); transform: translateZ( -20px ); } .reveal.page .slides section:not(.stack):after { content: ''; position: absolute; display: block; width: 90%; height: 30px; left: 5%; bottom: 0; background: none; z-index: 1; border-radius: 4px; box-shadow: 0px 95px 25px rgba(0,0,0,0.2); -webkit-transform: translateZ(-90px) rotateX( 65deg ); } .reveal.page .slides>section.stack { padding: 0; background: none; } .reveal.page .slides>section.past { transform-origin: 0% 0%; transform: translate3d(-40%, 0, 0) rotateY(-80deg); } .reveal.page .slides>section.future { transform-origin: 100% 0%; transform: translate3d(0, 0, 0); } .reveal.page .slides>section>section.past { transform-origin: 0% 0%; transform: translate3d(0, -40%, 0) rotateX(80deg); } .reveal.page .slides>section>section.future { transform-origin: 0% 100%; transform: translate3d(0, 0, 0); } /********************************************* * FADE TRANSITION *********************************************/ .reveal .slides section[data-transition=fade], .reveal.fade .slides section:not([data-transition]), .reveal.fade .slides>section>section:not([data-transition]) { transform: none; transition: opacity 0.5s; } .reveal.fade.overview .slides section, .reveal.fade.overview .slides>section>section { transition: none; } /********************************************* * NO TRANSITION *********************************************/ @include transition-global(none) { transform: none; transition: none; } /********************************************* * PAUSED MODE *********************************************/ .reveal .pause-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: black; visibility: hidden; opacity: 0; z-index: 100; transition: all 1s ease; } .reveal.paused .pause-overlay { visibility: visible; opacity: 1; } /********************************************* * FALLBACK *********************************************/ .no-transforms { overflow-y: auto; } .no-transforms .reveal .slides { position: relative; width: 80%; height: auto !important; top: 0; left: 50%; margin: 0; text-align: center; } .no-transforms .reveal .controls, .no-transforms .reveal .progress { display: none !important; } .no-transforms .reveal .slides section { display: block !important; opacity: 1 !important; position: relative !important; height: auto; min-height: 0; top: 0; left: -50%; margin: 70px 0; transform: none; } .no-transforms .reveal .slides section section { left: 0; } .reveal .no-transition, .reveal .no-transition * { transition: none !important; } /********************************************* * PER-SLIDE BACKGROUNDS *********************************************/ .reveal .backgrounds { position: absolute; width: 100%; height: 100%; top: 0; left: 0; perspective: 600px; } .reveal .slide-background { display: none; position: absolute; width: 100%; height: 100%; opacity: 0; visibility: hidden; background-color: rgba( 0, 0, 0, 0 ); background-position: 50% 50%; background-repeat: no-repeat; background-size: cover; transition: all 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); } .reveal .slide-background.stack { display: block; } .reveal .slide-background.present { opacity: 1; visibility: visible; } .print-pdf .reveal .slide-background { opacity: 1 !important; visibility: visible !important; } /* Video backgrounds */ .reveal .slide-background video { position: absolute; width: 100%; height: 100%; max-width: none; max-height: none; top: 0; left: 0; } /* Immediate transition style */ .reveal[data-background-transition=none]>.backgrounds .slide-background, .reveal>.backgrounds .slide-background[data-background-transition=none] { transition: none; } /* Slide */ .reveal[data-background-transition=slide]>.backgrounds .slide-background, .reveal>.backgrounds .slide-background[data-background-transition=slide] { opacity: 1; backface-visibility: hidden; } .reveal[data-background-transition=slide]>.backgrounds .slide-background.past, .reveal>.backgrounds .slide-background.past[data-background-transition=slide] { transform: translate(-100%, 0); } .reveal[data-background-transition=slide]>.backgrounds .slide-background.future, .reveal>.backgrounds .slide-background.future[data-background-transition=slide] { transform: translate(100%, 0); } .reveal[data-background-transition=slide]>.backgrounds .slide-background>.slide-background.past, .reveal>.backgrounds .slide-background>.slide-background.past[data-background-transition=slide] { transform: translate(0, -100%); } .reveal[data-background-transition=slide]>.backgrounds .slide-background>.slide-background.future, .reveal>.backgrounds .slide-background>.slide-background.future[data-background-transition=slide] { transform: translate(0, 100%); } /* Convex */ .reveal[data-background-transition=convex]>.backgrounds .slide-background.past, .reveal>.backgrounds .slide-background.past[data-background-transition=convex] { opacity: 0; transform: translate3d(-100%, 0, 0) rotateY(-90deg) translate3d(-100%, 0, 0); } .reveal[data-background-transition=convex]>.backgrounds .slide-background.future, .reveal>.backgrounds .slide-background.future[data-background-transition=convex] { opacity: 0; transform: translate3d(100%, 0, 0) rotateY(90deg) translate3d(100%, 0, 0); } .reveal[data-background-transition=convex]>.backgrounds .slide-background>.slide-background.past, .reveal>.backgrounds .slide-background>.slide-background.past[data-background-transition=convex] { opacity: 0; transform: translate3d(0, -100%, 0) rotateX(90deg) translate3d(0, -100%, 0); } .reveal[data-background-transition=convex]>.backgrounds .slide-background>.slide-background.future, .reveal>.backgrounds .slide-background>.slide-background.future[data-background-transition=convex] { opacity: 0; transform: translate3d(0, 100%, 0) rotateX(-90deg) translate3d(0, 100%, 0); } /* Concave */ .reveal[data-background-transition=concave]>.backgrounds .slide-background.past, .reveal>.backgrounds .slide-background.past[data-background-transition=concave] { opacity: 0; transform: translate3d(-100%, 0, 0) rotateY(90deg) translate3d(-100%, 0, 0); } .reveal[data-background-transition=concave]>.backgrounds .slide-background.future, .reveal>.backgrounds .slide-background.future[data-background-transition=concave] { opacity: 0; transform: translate3d(100%, 0, 0) rotateY(-90deg) translate3d(100%, 0, 0); } .reveal[data-background-transition=concave]>.backgrounds .slide-background>.slide-background.past, .reveal>.backgrounds .slide-background>.slide-background.past[data-background-transition=concave] { opacity: 0; transform: translate3d(0, -100%, 0) rotateX(-90deg) translate3d(0, -100%, 0); } .reveal[data-background-transition=concave]>.backgrounds .slide-background>.slide-background.future, .reveal>.backgrounds .slide-background>.slide-background.future[data-background-transition=concave] { opacity: 0; transform: translate3d(0, 100%, 0) rotateX(90deg) translate3d(0, 100%, 0); } /* Zoom */ .reveal[data-background-transition=zoom]>.backgrounds .slide-background, .reveal>.backgrounds .slide-background[data-background-transition=zoom] { transition-timing-function: ease; } .reveal[data-background-transition=zoom]>.backgrounds .slide-background.past, .reveal>.backgrounds .slide-background.past[data-background-transition=zoom] { opacity: 0; visibility: hidden; transform: scale(16); } .reveal[data-background-transition=zoom]>.backgrounds .slide-background.future, .reveal>.backgrounds .slide-background.future[data-background-transition=zoom] { opacity: 0; visibility: hidden; transform: scale(0.2); } .reveal[data-background-transition=zoom]>.backgrounds .slide-background>.slide-background.past, .reveal>.backgrounds .slide-background>.slide-background.past[data-background-transition=zoom] { opacity: 0; visibility: hidden; transform: scale(16); } .reveal[data-background-transition=zoom]>.backgrounds .slide-background>.slide-background.future, .reveal>.backgrounds .slide-background>.slide-background.future[data-background-transition=zoom] { opacity: 0; visibility: hidden; transform: scale(0.2); } /* Global transition speed settings */ .reveal[data-transition-speed="fast"]>.backgrounds .slide-background { transition-duration: 400ms; } .reveal[data-transition-speed="slow"]>.backgrounds .slide-background { transition-duration: 1200ms; } /********************************************* * OVERVIEW *********************************************/ .reveal.overview { perspective-origin: 50% 50%; perspective: 700px; .slides section { height: 700px; opacity: 1 !important; overflow: hidden; visibility: visible !important; cursor: pointer; box-sizing: border-box; } .slides section:hover, .slides section.present { outline: 10px solid rgba(150,150,150,0.4); outline-offset: 10px; } .slides section .fragment { opacity: 1; transition: none; } .slides section:after, .slides section:before { display: none !important; } .slides>section.stack { padding: 0; top: 0 !important; background: none; outline: none; overflow: visible; } .backgrounds { perspective: inherit; } .backgrounds .slide-background { opacity: 1; visibility: visible; // This can't be applied to the slide itself in Safari outline: 10px solid rgba(150,150,150,0.1); outline-offset: 10px; } } // Disable transitions transitions while we're activating // or deactivating the overview mode. .reveal.overview .slides section, .reveal.overview-deactivating .slides section { transition: none; } .reveal.overview .backgrounds .slide-background, .reveal.overview-deactivating .backgrounds .slide-background { transition: none; } .reveal.overview-animated .slides { transition: transform 0.4s ease; } /********************************************* * RTL SUPPORT *********************************************/ .reveal.rtl .slides, .reveal.rtl .slides h1, .reveal.rtl .slides h2, .reveal.rtl .slides h3, .reveal.rtl .slides h4, .reveal.rtl .slides h5, .reveal.rtl .slides h6 { direction: rtl; font-family: sans-serif; } .reveal.rtl pre, .reveal.rtl code { direction: ltr; } .reveal.rtl ol, .reveal.rtl ul { text-align: right; } .reveal.rtl .progress span { float: right } /********************************************* * PARALLAX BACKGROUND *********************************************/ .reveal.has-parallax-background .backgrounds { transition: all 0.8s ease; } /* Global transition speed settings */ .reveal.has-parallax-background[data-transition-speed="fast"] .backgrounds { transition-duration: 400ms; } .reveal.has-parallax-background[data-transition-speed="slow"] .backgrounds { transition-duration: 1200ms; } /********************************************* * LINK PREVIEW OVERLAY *********************************************/ .reveal .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; background: rgba( 0, 0, 0, 0.9 ); opacity: 0; visibility: hidden; transition: all 0.3s ease; } .reveal .overlay.visible { opacity: 1; visibility: visible; } .reveal .overlay .spinner { position: absolute; display: block; top: 50%; left: 50%; width: 32px; height: 32px; margin: -16px 0 0 -16px; z-index: 10; background-image: url(data:image/gif;base64,R0lGODlhIAAgAPMAAJmZmf%2F%2F%2F6%2Bvr8nJybW1tcDAwOjo6Nvb26ioqKOjo7Ozs%2FLy8vz8%2FAAAAAAAAAAAACH%2FC05FVFNDQVBFMi4wAwEAAAAh%2FhpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh%2BQQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ%2FV%2FnmOM82XiHRLYKhKP1oZmADdEAAAh%2BQQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY%2FCZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB%2BA4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6%2BHo7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq%2BB6QDtuetcaBPnW6%2BO7wDHpIiK9SaVK5GgV543tzjgGcghAgAh%2BQQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK%2B%2BG%2Bw48edZPK%2BM6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE%2BG%2BcD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm%2BFNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk%2BaV%2BoJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0%2FVNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc%2BXiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30%2FiI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE%2FjiuL04RGEBgwWhShRgQExHBAAh%2BQQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR%2BipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq%2BE71SRQeyqUToLA7VxF0JDyIQh%2FMVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY%2BYip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd%2BMFCN6HAAIKgNggY0KtEBAAh%2BQQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1%2BvsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d%2BjYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg%2BygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0%2Bbm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h%2BKr0SJ8MFihpNbx%2B4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX%2BBP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA%3D%3D); visibility: visible; opacity: 0.6; transition: all 0.3s ease; } .reveal .overlay header { position: absolute; left: 0; top: 0; width: 100%; height: 40px; z-index: 2; border-bottom: 1px solid #222; } .reveal .overlay header a { display: inline-block; width: 40px; height: 40px; padding: 0 10px; float: right; opacity: 0.6; box-sizing: border-box; } .reveal .overlay header a:hover { opacity: 1; } .reveal .overlay header a .icon { display: inline-block; width: 20px; height: 20px; background-position: 50% 50%; background-size: 100%; background-repeat: no-repeat; } .reveal .overlay header a.close .icon { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABkklEQVRYR8WX4VHDMAxG6wnoJrABZQPYBCaBTWAD2g1gE5gg6OOsXuxIlr40d81dfrSJ9V4c2VLK7spHuTJ/5wpM07QXuXc5X0opX2tEJcadjHuV80li/FgxTIEK/5QBCICBD6xEhSMGHgQPgBgLiYVAB1dpSqKDawxTohFw4JSEA3clzgIBPCURwE2JucBR7rhPJJv5OpJwDX+SfDjgx1wACQeJG1aChP9K/IMmdZ8DtESV1WyP3Bt4MwM6sj4NMxMYiqUWHQu4KYA/SYkIjOsm3BXYWMKFDwU2khjCQ4ELJUJ4SmClRArOCmSXGuKma0fYD5CbzHxFpCSGAhfAVSSUGDUk2BWZaff2g6GE15BsBQ9nwmpIGDiyHQddwNTMKkbZaf9fajXQca1EX44puJZUsnY0ObGmITE3GVLCbEhQUjGVt146j6oasWN+49Vph2w1pZ5EansNZqKBm1txbU57iRRcZ86RWMDdWtBJUHBHwoQPi1GV+JCbntmvok7iTX4/Up9mgyTc/FJYDTcndgH/AA5A/CHsyEkVAAAAAElFTkSuQmCC); } .reveal .overlay header a.external .icon { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAcElEQVRYR+2WSQoAIQwEzf8f7XiOMkUQxUPlGkM3hVmiQfQR9GYnH1SsAQlI4DiBqkCMoNb9y2e90IAEJPAcgdznU9+engMaeJ7Azh5Y1U67gAho4DqBqmB1buAf0MB1AlVBek83ZPkmJMGc1wAR+AAqod/B97TRpQAAAABJRU5ErkJggg==); } .reveal .overlay .viewport { position: absolute; top: 40px; right: 0; bottom: 0; left: 0; } .reveal .overlay.overlay-preview .viewport iframe { width: 100%; height: 100%; max-width: 100%; max-height: 100%; border: 0; opacity: 0; visibility: hidden; transition: all 0.3s ease; } .reveal .overlay.overlay-preview.loaded .viewport iframe { opacity: 1; visibility: visible; } .reveal .overlay.overlay-preview.loaded .spinner { opacity: 0; visibility: hidden; transform: scale(0.2); } .reveal .overlay.overlay-help .viewport { overflow: auto; color: #fff; } .reveal .overlay.overlay-help .viewport .viewport-inner { width: 600px; margin: 0 auto; padding: 60px; text-align: center; letter-spacing: normal; } .reveal .overlay.overlay-help .viewport .viewport-inner .title { font-size: 20px; } .reveal .overlay.overlay-help .viewport .viewport-inner table { border: 1px solid #fff; border-collapse: collapse; font-size: 14px; } .reveal .overlay.overlay-help .viewport .viewport-inner table th, .reveal .overlay.overlay-help .viewport .viewport-inner table td { width: 200px; padding: 10px; border: 1px solid #fff; vertical-align: middle; } .reveal .overlay.overlay-help .viewport .viewport-inner table th { padding-top: 20px; padding-bottom: 20px; } /********************************************* * PLAYBACK COMPONENT *********************************************/ .reveal .playback { position: fixed; left: 15px; bottom: 20px; z-index: 30; cursor: pointer; transition: all 400ms ease; } .reveal.overview .playback { opacity: 0; visibility: hidden; } /********************************************* * ROLLING LINKS *********************************************/ .reveal .roll { display: inline-block; line-height: 1.2; overflow: hidden; vertical-align: top; perspective: 400px; perspective-origin: 50% 50%; } .reveal .roll:hover { background: none; text-shadow: none; } .reveal .roll span { display: block; position: relative; padding: 0 2px; pointer-events: none; transition: all 400ms ease; transform-origin: 50% 0%; transform-style: preserve-3d; backface-visibility: hidden; } .reveal .roll:hover span { background: rgba(0,0,0,0.5); transform: translate3d( 0px, 0px, -45px ) rotateX( 90deg ); } .reveal .roll span:after { content: attr(data-title); display: block; position: absolute; left: 0; top: 0; padding: 0 2px; backface-visibility: hidden; transform-origin: 50% 0%; transform: translate3d( 0px, 110%, 0px ) rotateX( -90deg ); } /********************************************* * SPEAKER NOTES *********************************************/ // Hide on-page notes .reveal aside.notes { display: none; } // An interface element that can optionally be used to show the // speaker notes to all viewers, on top of the presentation .reveal .speaker-notes { display: none; position: absolute; width: 70%; max-height: 15%; left: 15%; bottom: 26px; padding: 10px; z-index: 1; font-size: 18px; line-height: 1.4; color: #fff; background-color: rgba(0,0,0,0.5); overflow: auto; box-sizing: border-box; text-align: left; font-family: Helvetica, sans-serif; -webkit-overflow-scrolling: touch; } .reveal .speaker-notes.visible:not(:empty) { display: block; } @media screen and (max-width: 1024px) { .reveal .speaker-notes { font-size: 14px; } } @media screen and (max-width: 600px) { .reveal .speaker-notes { width: 90%; left: 5%; } } /********************************************* * ZOOM PLUGIN *********************************************/ .zoomed .reveal *, .zoomed .reveal *:before, .zoomed .reveal *:after { backface-visibility: visible !important; } .zoomed .reveal .progress, .zoomed .reveal .controls { opacity: 0; } .zoomed .reveal .roll span { background: none; } .zoomed .reveal .roll span:after { visibility: hidden; } ================================================ FILE: test/client_server_SUITE_data/css/theme/README.md ================================================ ## Dependencies Themes are written using Sass to keep things modular and reduce the need for repeated selectors across files. Make sure that you have the reveal.js development environment including the Grunt dependencies installed before proceeding: https://github.com/hakimel/reveal.js#full-setup ## Creating a Theme To create your own theme, start by duplicating a ```.scss``` file in [/css/theme/source](https://github.com/hakimel/reveal.js/blob/master/css/theme/source). It will be automatically compiled by Grunt from Sass to CSS (see the [Gruntfile](https://github.com/hakimel/reveal.js/blob/master/Gruntfile.js)) when you run `grunt css-themes`. Each theme file does four things in the following order: 1. **Include [/css/theme/template/mixins.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/mixins.scss)** Shared utility functions. 2. **Include [/css/theme/template/settings.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/settings.scss)** Declares a set of custom variables that the template file (step 4) expects. Can be overridden in step 3. 3. **Override** This is where you override the default theme. Either by specifying variables (see [settings.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/settings.scss) for reference) or by adding any selectors and styles you please. 4. **Include [/css/theme/template/theme.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/theme.scss)** The template theme file which will generate final CSS output based on the currently defined variables. ================================================ FILE: test/client_server_SUITE_data/css/theme/beige.css ================================================ /** * Beige theme for reveal.js. * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ @import url(../../lib/font/league-gothic/league-gothic.css); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); /********************************************* * GLOBAL STYLES *********************************************/ body { background: #f7f2d3; background: -moz-radial-gradient(center, circle cover, white 0%, #f7f2d3 100%); background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, white), color-stop(100%, #f7f2d3)); background: -webkit-radial-gradient(center, circle cover, white 0%, #f7f2d3 100%); background: -o-radial-gradient(center, circle cover, white 0%, #f7f2d3 100%); background: -ms-radial-gradient(center, circle cover, white 0%, #f7f2d3 100%); background: radial-gradient(center, circle cover, white 0%, #f7f2d3 100%); background-color: #f7f3de; } .reveal { font-family: "Lato", sans-serif; font-size: 36px; font-weight: normal; color: #333; } ::selection { color: #fff; background: rgba(79, 64, 28, 0.99); text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #333; font-family: "League Gothic", Impact, sans-serif; font-weight: normal; line-height: 1.2; letter-spacing: normal; text-transform: uppercase; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0, 0, 0, 0.1), 0 0 5px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.3), 0 3px 5px rgba(0, 0, 0, 0.2), 0 5px 10px rgba(0, 0, 0, 0.25), 0 20px 20px rgba(0, 0, 0, 0.15); } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #8b743d; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #c0a86e; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #564826; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #333; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #8b743d; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #8b743d; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #8b743d; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #8b743d; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #8b743d; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #c0a86e; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #c0a86e; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #c0a86e; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #c0a86e; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #8b743d; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/black.css ================================================ /** * Black theme for reveal.js. This is the opposite of the 'white' theme. * * Copyright (C) 2015 Hakim El Hattab, http://hakim.se */ @import url(../../lib/font/source-sans-pro/source-sans-pro.css); section.has-light-background, section.has-light-background h1, section.has-light-background h2, section.has-light-background h3, section.has-light-background h4, section.has-light-background h5, section.has-light-background h6 { color: #222; } /********************************************* * GLOBAL STYLES *********************************************/ body { background: #222; background-color: #222; } .reveal { font-family: "Source Sans Pro", Helvetica, sans-serif; font-size: 38px; font-weight: normal; color: #fff; } ::selection { color: #fff; background: #bee4fd; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #fff; font-family: "Source Sans Pro", Helvetica, sans-serif; font-weight: 600; line-height: 1.2; letter-spacing: normal; text-transform: uppercase; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 2.5em; } .reveal h2 { font-size: 1.6em; } .reveal h3 { font-size: 1.3em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: none; } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #42affa; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #8dcffc; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #068de9; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #42affa; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #42affa; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #42affa; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #42affa; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #42affa; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #8dcffc; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #8dcffc; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #8dcffc; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #8dcffc; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #42affa; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/blood.css ================================================ /** * Blood theme for reveal.js * Author: Walther http://github.com/Walther * * Designed to be used with highlight.js theme * "monokai_sublime.css" available from * https://github.com/isagalaev/highlight.js/ * * For other themes, change $codeBackground accordingly. * */ @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,700,300italic,700italic); /********************************************* * GLOBAL STYLES *********************************************/ body { background: #222; background-color: #222; } .reveal { font-family: Ubuntu, "sans-serif"; font-size: 36px; font-weight: normal; color: #eee; } ::selection { color: #fff; background: #a23; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #eee; font-family: Ubuntu, "sans-serif"; font-weight: normal; line-height: 1.2; letter-spacing: normal; text-transform: uppercase; text-shadow: 2px 2px 2px #222; word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0, 0, 0, 0.1), 0 0 5px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.3), 0 3px 5px rgba(0, 0, 0, 0.2), 0 5px 10px rgba(0, 0, 0, 0.25), 0 20px 20px rgba(0, 0, 0, 0.15); } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #a23; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #dd5566; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #6a1520; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #eee; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #a23; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #a23; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #a23; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #a23; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #a23; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #dd5566; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #dd5566; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #dd5566; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #dd5566; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #a23; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } .reveal p { font-weight: 300; text-shadow: 1px 1px #222; } .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { font-weight: 700; } .reveal p code { background-color: #23241f; display: inline-block; border-radius: 7px; } .reveal small code { vertical-align: baseline; } ================================================ FILE: test/client_server_SUITE_data/css/theme/league.css ================================================ /** * League theme for reveal.js. * * This was the default theme pre-3.0.0. * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ @import url(../../lib/font/league-gothic/league-gothic.css); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); /********************************************* * GLOBAL STYLES *********************************************/ body { background: #1c1e20; background: -moz-radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%); background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, #555a5f), color-stop(100%, #1c1e20)); background: -webkit-radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%); background: -o-radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%); background: -ms-radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%); background: radial-gradient(center, circle cover, #555a5f 0%, #1c1e20 100%); background-color: #2b2b2b; } .reveal { font-family: "Lato", sans-serif; font-size: 36px; font-weight: normal; color: #eee; } ::selection { color: #fff; background: #FF5E99; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #eee; font-family: "League Gothic", Impact, sans-serif; font-weight: normal; line-height: 1.2; letter-spacing: normal; text-transform: uppercase; text-shadow: 0px 0px 6px rgba(0, 0, 0, 0.2); word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0, 0, 0, 0.1), 0 0 5px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.3), 0 3px 5px rgba(0, 0, 0, 0.2), 0 5px 10px rgba(0, 0, 0, 0.25), 0 20px 20px rgba(0, 0, 0, 0.15); } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #13DAEC; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #71e9f4; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #0d99a5; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #eee; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #13DAEC; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #13DAEC; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #13DAEC; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #13DAEC; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #13DAEC; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #71e9f4; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #71e9f4; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #71e9f4; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #71e9f4; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #13DAEC; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/moon.css ================================================ /** * Solarized Dark theme for reveal.js. * Author: Achim Staebler */ @import url(../../lib/font/league-gothic/league-gothic.css); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); /** * Solarized colors by Ethan Schoonover */ html * { color-profile: sRGB; rendering-intent: auto; } /********************************************* * GLOBAL STYLES *********************************************/ body { background: #002b36; background-color: #002b36; } .reveal { font-family: "Lato", sans-serif; font-size: 36px; font-weight: normal; color: #93a1a1; } ::selection { color: #fff; background: #d33682; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #eee8d5; font-family: "League Gothic", Impact, sans-serif; font-weight: normal; line-height: 1.2; letter-spacing: normal; text-transform: uppercase; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: none; } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #268bd2; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #78b9e6; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #1a6091; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #93a1a1; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #268bd2; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #268bd2; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #268bd2; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #268bd2; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #268bd2; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #78b9e6; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #78b9e6; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #78b9e6; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #78b9e6; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #268bd2; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/night.css ================================================ /** * Black theme for reveal.js. * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ @import url(https://fonts.googleapis.com/css?family=Montserrat:700); @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700,400italic,700italic); /********************************************* * GLOBAL STYLES *********************************************/ body { background: #111; background-color: #111; } .reveal { font-family: "Open Sans", sans-serif; font-size: 30px; font-weight: normal; color: #eee; } ::selection { color: #fff; background: #e7ad52; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #eee; font-family: "Montserrat", Impact, sans-serif; font-weight: normal; line-height: 1.2; letter-spacing: -0.03em; text-transform: none; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: none; } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #e7ad52; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #f3d7ac; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #d08a1d; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #eee; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #e7ad52; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #e7ad52; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #e7ad52; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #e7ad52; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #e7ad52; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #f3d7ac; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #f3d7ac; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #f3d7ac; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #f3d7ac; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #e7ad52; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/serif.css ================================================ /** * A simple theme for reveal.js presentations, similar * to the default theme. The accent color is brown. * * This theme is Copyright (C) 2012-2013 Owen Versteeg, http://owenversteeg.com - it is MIT licensed. */ .reveal a { line-height: 1.3em; } /********************************************* * GLOBAL STYLES *********************************************/ body { background: #F0F1EB; background-color: #F0F1EB; } .reveal { font-family: "Palatino Linotype", "Book Antiqua", Palatino, FreeSerif, serif; font-size: 36px; font-weight: normal; color: #000; } ::selection { color: #fff; background: #26351C; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #383D3D; font-family: "Palatino Linotype", "Book Antiqua", Palatino, FreeSerif, serif; font-weight: normal; line-height: 1.2; letter-spacing: normal; text-transform: none; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: none; } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #51483D; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #8b7c69; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #25211c; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #000; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #51483D; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #51483D; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #51483D; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #51483D; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #51483D; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #8b7c69; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #8b7c69; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #8b7c69; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #8b7c69; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #51483D; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/simple.css ================================================ /** * A simple theme for reveal.js presentations, similar * to the default theme. The accent color is darkblue. * * This theme is Copyright (C) 2012 Owen Versteeg, https://github.com/StereotypicalApps. It is MIT licensed. * reveal.js is Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ @import url(https://fonts.googleapis.com/css?family=News+Cycle:400,700); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); /********************************************* * GLOBAL STYLES *********************************************/ body { background: #fff; background-color: #fff; } .reveal { font-family: "Lato", sans-serif; font-size: 36px; font-weight: normal; color: #000; } ::selection { color: #fff; background: rgba(0, 0, 0, 0.99); text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #000; font-family: "News Cycle", Impact, sans-serif; font-weight: normal; line-height: 1.2; letter-spacing: normal; text-transform: none; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: none; } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #00008B; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #0000f1; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #00003f; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #000; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #00008B; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #00008B; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #00008B; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #00008B; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #00008B; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #0000f1; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #0000f1; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #0000f1; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #0000f1; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #00008B; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/sky.css ================================================ /** * Sky theme for reveal.js. * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ @import url(https://fonts.googleapis.com/css?family=Quicksand:400,700,400italic,700italic); @import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700); .reveal a { line-height: 1.3em; } /********************************************* * GLOBAL STYLES *********************************************/ body { background: #add9e4; background: -moz-radial-gradient(center, circle cover, #f7fbfc 0%, #add9e4 100%); background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, #f7fbfc), color-stop(100%, #add9e4)); background: -webkit-radial-gradient(center, circle cover, #f7fbfc 0%, #add9e4 100%); background: -o-radial-gradient(center, circle cover, #f7fbfc 0%, #add9e4 100%); background: -ms-radial-gradient(center, circle cover, #f7fbfc 0%, #add9e4 100%); background: radial-gradient(center, circle cover, #f7fbfc 0%, #add9e4 100%); background-color: #f7fbfc; } .reveal { font-family: "Open Sans", sans-serif; font-size: 36px; font-weight: normal; color: #333; } ::selection { color: #fff; background: #134674; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #333; font-family: "Quicksand", sans-serif; font-weight: normal; line-height: 1.2; letter-spacing: -0.08em; text-transform: uppercase; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: none; } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #3b759e; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #74a7cb; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #264c66; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #333; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #3b759e; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #3b759e; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #3b759e; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #3b759e; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #3b759e; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #74a7cb; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #74a7cb; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #74a7cb; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #74a7cb; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #3b759e; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/solarized.css ================================================ /** * Solarized Light theme for reveal.js. * Author: Achim Staebler */ @import url(../../lib/font/league-gothic/league-gothic.css); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); /** * Solarized colors by Ethan Schoonover */ html * { color-profile: sRGB; rendering-intent: auto; } /********************************************* * GLOBAL STYLES *********************************************/ body { background: #fdf6e3; background-color: #fdf6e3; } .reveal { font-family: "Lato", sans-serif; font-size: 36px; font-weight: normal; color: #657b83; } ::selection { color: #fff; background: #d33682; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #586e75; font-family: "League Gothic", Impact, sans-serif; font-weight: normal; line-height: 1.2; letter-spacing: normal; text-transform: uppercase; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 3.77em; } .reveal h2 { font-size: 2.11em; } .reveal h3 { font-size: 1.55em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: none; } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #268bd2; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #78b9e6; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #1a6091; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #657b83; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #268bd2; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #268bd2; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #268bd2; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #268bd2; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #268bd2; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #78b9e6; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #78b9e6; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #78b9e6; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #78b9e6; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #268bd2; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/source/beige.scss ================================================ /** * Beige theme for reveal.js. * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(../../lib/font/league-gothic/league-gothic.css); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); // Override theme settings (see ../template/settings.scss) $mainColor: #333; $headingColor: #333; $headingTextShadow: none; $backgroundColor: #f7f3de; $linkColor: #8b743d; $linkColorHover: lighten( $linkColor, 20% ); $selectionBackgroundColor: rgba(79, 64, 28, 0.99); $heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15); // Background generator @mixin bodyBackground() { @include radial-gradient( rgba(247,242,211,1), rgba(255,255,255,1) ); } // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/black.scss ================================================ /** * Black theme for reveal.js. This is the opposite of the 'white' theme. * * Copyright (C) 2015 Hakim El Hattab, http://hakim.se */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(../../lib/font/source-sans-pro/source-sans-pro.css); // Override theme settings (see ../template/settings.scss) $backgroundColor: #222; $mainColor: #fff; $headingColor: #fff; $mainFontSize: 38px; $mainFont: 'Source Sans Pro', Helvetica, sans-serif; $headingFont: 'Source Sans Pro', Helvetica, sans-serif; $headingTextShadow: none; $headingLetterSpacing: normal; $headingTextTransform: uppercase; $headingFontWeight: 600; $linkColor: #42affa; $linkColorHover: lighten( $linkColor, 15% ); $selectionBackgroundColor: lighten( $linkColor, 25% ); $heading1Size: 2.5em; $heading2Size: 1.6em; $heading3Size: 1.3em; $heading4Size: 1.0em; section.has-light-background { &, h1, h2, h3, h4, h5, h6 { color: #222; } } // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/blood.scss ================================================ /** * Blood theme for reveal.js * Author: Walther http://github.com/Walther * * Designed to be used with highlight.js theme * "monokai_sublime.css" available from * https://github.com/isagalaev/highlight.js/ * * For other themes, change $codeBackground accordingly. * */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,700,300italic,700italic); // Colors used in the theme $blood: #a23; $coal: #222; $codeBackground: #23241f; $backgroundColor: $coal; // Main text $mainFont: Ubuntu, 'sans-serif'; $mainFontSize: 36px; $mainColor: #eee; // Headings $headingFont: Ubuntu, 'sans-serif'; $headingTextShadow: 2px 2px 2px $coal; // h1 shadow, borrowed humbly from // (c) Default theme by Hakim El Hattab $heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15); // Links $linkColor: $blood; $linkColorHover: lighten( $linkColor, 20% ); // Text selection $selectionBackgroundColor: $blood; $selectionColor: #fff; // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- // some overrides after theme template import .reveal p { font-weight: 300; text-shadow: 1px 1px $coal; } .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { font-weight: 700; } .reveal p code { background-color: $codeBackground; display: inline-block; border-radius: 7px; } .reveal small code { vertical-align: baseline; } ================================================ FILE: test/client_server_SUITE_data/css/theme/source/league.scss ================================================ /** * League theme for reveal.js. * * This was the default theme pre-3.0.0. * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(../../lib/font/league-gothic/league-gothic.css); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); // Override theme settings (see ../template/settings.scss) $headingTextShadow: 0px 0px 6px rgba(0,0,0,0.2); $heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15); // Background generator @mixin bodyBackground() { @include radial-gradient( rgba(28,30,32,1), rgba(85,90,95,1) ); } // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/moon.scss ================================================ /** * Solarized Dark theme for reveal.js. * Author: Achim Staebler */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(../../lib/font/league-gothic/league-gothic.css); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); /** * Solarized colors by Ethan Schoonover */ html * { color-profile: sRGB; rendering-intent: auto; } // Solarized colors $base03: #002b36; $base02: #073642; $base01: #586e75; $base00: #657b83; $base0: #839496; $base1: #93a1a1; $base2: #eee8d5; $base3: #fdf6e3; $yellow: #b58900; $orange: #cb4b16; $red: #dc322f; $magenta: #d33682; $violet: #6c71c4; $blue: #268bd2; $cyan: #2aa198; $green: #859900; // Override theme settings (see ../template/settings.scss) $mainColor: $base1; $headingColor: $base2; $headingTextShadow: none; $backgroundColor: $base03; $linkColor: $blue; $linkColorHover: lighten( $linkColor, 20% ); $selectionBackgroundColor: $magenta; // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/night.scss ================================================ /** * Black theme for reveal.js. * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(https://fonts.googleapis.com/css?family=Montserrat:700); @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700,400italic,700italic); // Override theme settings (see ../template/settings.scss) $backgroundColor: #111; $mainFont: 'Open Sans', sans-serif; $linkColor: #e7ad52; $linkColorHover: lighten( $linkColor, 20% ); $headingFont: 'Montserrat', Impact, sans-serif; $headingTextShadow: none; $headingLetterSpacing: -0.03em; $headingTextTransform: none; $selectionBackgroundColor: #e7ad52; $mainFontSize: 30px; // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/serif.scss ================================================ /** * A simple theme for reveal.js presentations, similar * to the default theme. The accent color is brown. * * This theme is Copyright (C) 2012-2013 Owen Versteeg, http://owenversteeg.com - it is MIT licensed. */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Override theme settings (see ../template/settings.scss) $mainFont: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; $mainColor: #000; $headingFont: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; $headingColor: #383D3D; $headingTextShadow: none; $headingTextTransform: none; $backgroundColor: #F0F1EB; $linkColor: #51483D; $linkColorHover: lighten( $linkColor, 20% ); $selectionBackgroundColor: #26351C; .reveal a { line-height: 1.3em; } // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/simple.scss ================================================ /** * A simple theme for reveal.js presentations, similar * to the default theme. The accent color is darkblue. * * This theme is Copyright (C) 2012 Owen Versteeg, https://github.com/StereotypicalApps. It is MIT licensed. * reveal.js is Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(https://fonts.googleapis.com/css?family=News+Cycle:400,700); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); // Override theme settings (see ../template/settings.scss) $mainFont: 'Lato', sans-serif; $mainColor: #000; $headingFont: 'News Cycle', Impact, sans-serif; $headingColor: #000; $headingTextShadow: none; $headingTextTransform: none; $backgroundColor: #fff; $linkColor: #00008B; $linkColorHover: lighten( $linkColor, 20% ); $selectionBackgroundColor: rgba(0, 0, 0, 0.99); // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/sky.scss ================================================ /** * Sky theme for reveal.js. * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(https://fonts.googleapis.com/css?family=Quicksand:400,700,400italic,700italic); @import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700); // Override theme settings (see ../template/settings.scss) $mainFont: 'Open Sans', sans-serif; $mainColor: #333; $headingFont: 'Quicksand', sans-serif; $headingColor: #333; $headingLetterSpacing: -0.08em; $headingTextShadow: none; $backgroundColor: #f7fbfc; $linkColor: #3b759e; $linkColorHover: lighten( $linkColor, 20% ); $selectionBackgroundColor: #134674; // Fix links so they are not cut off .reveal a { line-height: 1.3em; } // Background generator @mixin bodyBackground() { @include radial-gradient( #add9e4, #f7fbfc ); } // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/solarized.scss ================================================ /** * Solarized Light theme for reveal.js. * Author: Achim Staebler */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(../../lib/font/league-gothic/league-gothic.css); @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); /** * Solarized colors by Ethan Schoonover */ html * { color-profile: sRGB; rendering-intent: auto; } // Solarized colors $base03: #002b36; $base02: #073642; $base01: #586e75; $base00: #657b83; $base0: #839496; $base1: #93a1a1; $base2: #eee8d5; $base3: #fdf6e3; $yellow: #b58900; $orange: #cb4b16; $red: #dc322f; $magenta: #d33682; $violet: #6c71c4; $blue: #268bd2; $cyan: #2aa198; $green: #859900; // Override theme settings (see ../template/settings.scss) $mainColor: $base00; $headingColor: $base01; $headingTextShadow: none; $backgroundColor: $base3; $linkColor: $blue; $linkColorHover: lighten( $linkColor, 20% ); $selectionBackgroundColor: $magenta; // Background generator // @mixin bodyBackground() { // @include radial-gradient( rgba($base3,1), rgba(lighten($base3, 20%),1) ); // } // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/source/white.scss ================================================ /** * White theme for reveal.js. This is the opposite of the 'black' theme. * * Copyright (C) 2015 Hakim El Hattab, http://hakim.se */ // Default mixins and settings ----------------- @import "../template/mixins"; @import "../template/settings"; // --------------------------------------------- // Include theme-specific fonts @import url(../../lib/font/source-sans-pro/source-sans-pro.css); // Override theme settings (see ../template/settings.scss) $backgroundColor: #fff; $mainColor: #222; $headingColor: #222; $mainFontSize: 38px; $mainFont: 'Source Sans Pro', Helvetica, sans-serif; $headingFont: 'Source Sans Pro', Helvetica, sans-serif; $headingTextShadow: none; $headingLetterSpacing: normal; $headingTextTransform: uppercase; $headingFontWeight: 600; $linkColor: #2a76dd; $linkColorHover: lighten( $linkColor, 15% ); $selectionBackgroundColor: lighten( $linkColor, 25% ); $heading1Size: 2.5em; $heading2Size: 1.6em; $heading3Size: 1.3em; $heading4Size: 1.0em; section.has-dark-background { &, h1, h2, h3, h4, h5, h6 { color: #fff; } } // Theme template ------------------------------ @import "../template/theme"; // --------------------------------------------- ================================================ FILE: test/client_server_SUITE_data/css/theme/template/mixins.scss ================================================ @mixin vertical-gradient( $top, $bottom ) { background: $top; background: -moz-linear-gradient( top, $top 0%, $bottom 100% ); background: -webkit-gradient( linear, left top, left bottom, color-stop(0%,$top), color-stop(100%,$bottom) ); background: -webkit-linear-gradient( top, $top 0%, $bottom 100% ); background: -o-linear-gradient( top, $top 0%, $bottom 100% ); background: -ms-linear-gradient( top, $top 0%, $bottom 100% ); background: linear-gradient( top, $top 0%, $bottom 100% ); } @mixin horizontal-gradient( $top, $bottom ) { background: $top; background: -moz-linear-gradient( left, $top 0%, $bottom 100% ); background: -webkit-gradient( linear, left top, right top, color-stop(0%,$top), color-stop(100%,$bottom) ); background: -webkit-linear-gradient( left, $top 0%, $bottom 100% ); background: -o-linear-gradient( left, $top 0%, $bottom 100% ); background: -ms-linear-gradient( left, $top 0%, $bottom 100% ); background: linear-gradient( left, $top 0%, $bottom 100% ); } @mixin radial-gradient( $outer, $inner, $type: circle ) { background: $outer; background: -moz-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); background: -webkit-gradient( radial, center center, 0px, center center, 100%, color-stop(0%,$inner), color-stop(100%,$outer) ); background: -webkit-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); background: -o-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); background: -ms-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); background: radial-gradient( center, $type cover, $inner 0%, $outer 100% ); } ================================================ FILE: test/client_server_SUITE_data/css/theme/template/settings.scss ================================================ // Base settings for all themes that can optionally be // overridden by the super-theme // Background of the presentation $backgroundColor: #2b2b2b; // Primary/body text $mainFont: 'Lato', sans-serif; $mainFontSize: 36px; $mainColor: #eee; // Vertical spacing between blocks of text $blockMargin: 20px; // Headings $headingMargin: 0 0 $blockMargin 0; $headingFont: 'League Gothic', Impact, sans-serif; $headingColor: #eee; $headingLineHeight: 1.2; $headingLetterSpacing: normal; $headingTextTransform: uppercase; $headingTextShadow: none; $headingFontWeight: normal; $heading1TextShadow: $headingTextShadow; $heading1Size: 3.77em; $heading2Size: 2.11em; $heading3Size: 1.55em; $heading4Size: 1.00em; // Links and actions $linkColor: #13DAEC; $linkColorHover: lighten( $linkColor, 20% ); // Text selection $selectionBackgroundColor: #FF5E99; $selectionColor: #fff; // Generates the presentation background, can be overridden // to return a background image or gradient @mixin bodyBackground() { background: $backgroundColor; } ================================================ FILE: test/client_server_SUITE_data/css/theme/template/theme.scss ================================================ // Base theme template for reveal.js /********************************************* * GLOBAL STYLES *********************************************/ body { @include bodyBackground(); background-color: $backgroundColor; } .reveal { font-family: $mainFont; font-size: $mainFontSize; font-weight: normal; color: $mainColor; } ::selection { color: $selectionColor; background: $selectionBackgroundColor; text-shadow: none; } .reveal .slides>section, .reveal .slides>section>section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: $headingMargin; color: $headingColor; font-family: $headingFont; font-weight: $headingFontWeight; line-height: $headingLineHeight; letter-spacing: $headingLetterSpacing; text-transform: $headingTextTransform; text-shadow: $headingTextShadow; word-wrap: break-word; } .reveal h1 {font-size: $heading1Size; } .reveal h2 {font-size: $heading2Size; } .reveal h3 {font-size: $heading3Size; } .reveal h4 {font-size: $heading4Size; } .reveal h1 { text-shadow: $heading1TextShadow; } /********************************************* * OTHER *********************************************/ .reveal p { margin: $blockMargin 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: $blockMargin auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0,0,0,0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: $blockMargin auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0,0,0,0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: $linkColor; text-decoration: none; -webkit-transition: color .15s ease; -moz-transition: color .15s ease; transition: color .15s ease; } .reveal a:hover { color: $linkColorHover; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: darken( $linkColor, 15% ); } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255,255,255,0.12); border: 4px solid $mainColor; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all .15s linear; -moz-transition: all .15s linear; transition: all .15s linear; } .reveal a:hover img { background: rgba(255,255,255,0.2); border-color: $linkColor; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: $linkColor; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: $linkColor; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: $linkColor; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: $linkColor; } .reveal .controls .navigate-left.enabled:hover { border-right-color: $linkColorHover; } .reveal .controls .navigate-right.enabled:hover { border-left-color: $linkColorHover; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: $linkColorHover; } .reveal .controls .navigate-down.enabled:hover { border-top-color: $linkColorHover; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0,0,0,0.2); } .reveal .progress span { background: $linkColor; -webkit-transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); -moz-transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); transition: width 800ms cubic-bezier(0.260, 0.860, 0.440, 0.985); } ================================================ FILE: test/client_server_SUITE_data/css/theme/white.css ================================================ /** * White theme for reveal.js. This is the opposite of the 'black' theme. * * Copyright (C) 2015 Hakim El Hattab, http://hakim.se */ @import url(../../lib/font/source-sans-pro/source-sans-pro.css); section.has-dark-background, section.has-dark-background h1, section.has-dark-background h2, section.has-dark-background h3, section.has-dark-background h4, section.has-dark-background h5, section.has-dark-background h6 { color: #fff; } /********************************************* * GLOBAL STYLES *********************************************/ body { background: #fff; background-color: #fff; } .reveal { font-family: "Source Sans Pro", Helvetica, sans-serif; font-size: 38px; font-weight: normal; color: #222; } ::selection { color: #fff; background: #98bdef; text-shadow: none; } .reveal .slides > section, .reveal .slides > section > section { line-height: 1.3; font-weight: inherit; } /********************************************* * HEADERS *********************************************/ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { margin: 0 0 20px 0; color: #222; font-family: "Source Sans Pro", Helvetica, sans-serif; font-weight: 600; line-height: 1.2; letter-spacing: normal; text-transform: uppercase; text-shadow: none; word-wrap: break-word; } .reveal h1 { font-size: 2.5em; } .reveal h2 { font-size: 1.6em; } .reveal h3 { font-size: 1.3em; } .reveal h4 { font-size: 1em; } .reveal h1 { text-shadow: none; } /********************************************* * OTHER *********************************************/ .reveal p { margin: 20px 0; line-height: 1.3; } /* Ensure certain elements are never larger than the slide itself */ .reveal img, .reveal video, .reveal iframe { max-width: 95%; max-height: 95%; } .reveal strong, .reveal b { font-weight: bold; } .reveal em { font-style: italic; } .reveal ol, .reveal dl, .reveal ul { display: inline-block; text-align: left; margin: 0 0 0 1em; } .reveal ol { list-style-type: decimal; } .reveal ul { list-style-type: disc; } .reveal ul ul { list-style-type: square; } .reveal ul ul ul { list-style-type: circle; } .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { display: block; margin-left: 40px; } .reveal dt { font-weight: bold; } .reveal dd { margin-left: 40px; } .reveal q, .reveal blockquote { quotes: none; } .reveal blockquote { display: block; position: relative; width: 70%; margin: 20px auto; padding: 5px; font-style: italic; background: rgba(255, 255, 255, 0.05); box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } .reveal blockquote p:first-child, .reveal blockquote p:last-child { display: inline-block; } .reveal q { font-style: italic; } .reveal pre { display: block; position: relative; width: 90%; margin: 20px auto; text-align: left; font-size: 0.55em; font-family: monospace; line-height: 1.2em; word-wrap: break-word; box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } .reveal code { font-family: monospace; } .reveal pre code { display: block; padding: 5px; overflow: auto; max-height: 400px; word-wrap: normal; } .reveal table { margin: auto; border-collapse: collapse; border-spacing: 0; } .reveal table th { font-weight: bold; } .reveal table th, .reveal table td { text-align: left; padding: 0.2em 0.5em 0.2em 0.5em; border-bottom: 1px solid; } .reveal table th[align="center"], .reveal table td[align="center"] { text-align: center; } .reveal table th[align="right"], .reveal table td[align="right"] { text-align: right; } .reveal table tr:last-child td { border-bottom: none; } .reveal sup { vertical-align: super; } .reveal sub { vertical-align: sub; } .reveal small { display: inline-block; font-size: 0.6em; line-height: 1.2em; vertical-align: top; } .reveal small * { vertical-align: top; } /********************************************* * LINKS *********************************************/ .reveal a { color: #2a76dd; text-decoration: none; -webkit-transition: color 0.15s ease; -moz-transition: color 0.15s ease; transition: color 0.15s ease; } .reveal a:hover { color: #6ca0e8; text-shadow: none; border: none; } .reveal .roll span:after { color: #fff; background: #1a53a1; } /********************************************* * IMAGES *********************************************/ .reveal section img { margin: 15px 0px; background: rgba(255, 255, 255, 0.12); border: 4px solid #222; box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } .reveal section img.plain { border: 0; box-shadow: none; } .reveal a img { -webkit-transition: all 0.15s linear; -moz-transition: all 0.15s linear; transition: all 0.15s linear; } .reveal a:hover img { background: rgba(255, 255, 255, 0.2); border-color: #2a76dd; box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } /********************************************* * NAVIGATION CONTROLS *********************************************/ .reveal .controls .navigate-left, .reveal .controls .navigate-left.enabled { border-right-color: #2a76dd; } .reveal .controls .navigate-right, .reveal .controls .navigate-right.enabled { border-left-color: #2a76dd; } .reveal .controls .navigate-up, .reveal .controls .navigate-up.enabled { border-bottom-color: #2a76dd; } .reveal .controls .navigate-down, .reveal .controls .navigate-down.enabled { border-top-color: #2a76dd; } .reveal .controls .navigate-left.enabled:hover { border-right-color: #6ca0e8; } .reveal .controls .navigate-right.enabled:hover { border-left-color: #6ca0e8; } .reveal .controls .navigate-up.enabled:hover { border-bottom-color: #6ca0e8; } .reveal .controls .navigate-down.enabled:hover { border-top-color: #6ca0e8; } /********************************************* * PROGRESS BAR *********************************************/ .reveal .progress { background: rgba(0, 0, 0, 0.2); } .reveal .progress span { background: #2a76dd; -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } ================================================ FILE: test/client_server_SUITE_data/index.html ================================================ reveal.js – The HTML Presentation Framework

Reveal.js

The HTML Presentation Framework

Created by Hakim El Hattab / @hakimel

Hello There

reveal.js enables you to create beautiful interactive slide decks using HTML. This presentation will show you examples of what it can do.

Vertical Slides

Slides can be nested inside of each other.

Use the Space key to navigate through all slides.


Down arrow

Basement Level 1

Nested slides are useful for adding additional detail underneath a high level horizontal slide.

Basement Level 2

That's it, time to go back up.


Up arrow

Slides

Not a coder? Not a problem. There's a fully-featured visual editor for authoring these, try it out at http://slides.com.

Point of View

Press ESC to enter the slide overview.

Hold down alt and click on any element to zoom in on it using zoom.js. Alt + click anywhere to zoom back out.

Touch Optimized

Presentations look great on touch devices, like mobile phones and tablets. Simply swipe through your slides.

Fragments

Hit the next arrow...

... to step through ...

... a fragmented slide.

Fragment Styles

There's different types of fragments, like:

grow

shrink

fade-out

current-visible

highlight-red

highlight-blue

Transition Styles

You can select from different transitions, like:
None - Fade - Slide - Convex - Concave - Zoom

Themes

reveal.js comes with a few themes built in:
Black (default) - White - League - Sky - Beige - Simple
Serif - Blood - Night - Moon - Solarized

Slide Backgrounds

Set data-background="#dddddd" on a slide to change the background color. All CSS color formats are supported.

Down arrow

Image Backgrounds

<section data-background="image.png">

Tiled Backgrounds

<section data-background="image.png" data-background-repeat="repeat" data-background-size="100px">

Video Backgrounds

<section data-background-video="video.mp4,video.webm">

... and GIFs!

Background Transitions

Different background transitions are available via the backgroundTransition option. This one's called "zoom".

Reveal.configure({ backgroundTransition: 'zoom' })

Background Transitions

You can override background transitions per-slide.

<section data-background-transition="zoom">

Pretty Code


function linkify( selector ) {
  if( supports3DTransforms ) {

    var nodes = document.querySelectorAll( selector );

    for( var i = 0, len = nodes.length; i < len; i++ ) {
      var node = nodes[i];

      if( !node.className ) {
        node.className += ' roll';
      }
    }
  }
}
					

Code syntax highlighting courtesy of highlight.js.

Marvelous List

  • No order here
  • Or here
  • Or here
  • Or here

Fantastic Ordered List

  1. One is smaller than...
  2. Two is smaller than...
  3. Three!

Tabular Tables

Item Value Quantity
Apples $1 7
Lemonade $2 18
Bread $3 2

Clever Quotes

These guys come in two forms, inline: “The nice thing about standards is that there are so many to choose from” and block:

“For years there has been a theory that millions of monkeys typing at random on millions of typewriters would reproduce the entire works of Shakespeare. The Internet has proven this theory to be untrue.”

Intergalactic Interconnections

You can link between slides internally, like this.

Speaker View

There's a speaker view. It includes a timer, preview of the upcoming slide as well as your speaker notes.

Press the S key to try it out.

Export to PDF

Presentations can be exported to PDF, here's an example:

Global State

Set data-state="something" on a slide and "something" will be added as a class to the document element when the slide is open. This lets you apply broader style changes, like switching the page background.

State Events

Additionally custom events can be triggered on a per slide basis by binding to the data-state name.


Reveal.addEventListener( 'customevent', function() {
	console.log( '"customevent" has fired' );
} );
					

Take a Moment

Press B or . on your keyboard to pause the presentation. This is helpful when you're on stage and want to take distracting slides off the screen.

Much more

THE END

- Try the online editor
- Source code & documentation

================================================ FILE: test/client_server_SUITE_data/js/reveal.js ================================================ /*! * reveal.js * http://lab.hakim.se/reveal-js * MIT licensed * * Copyright (C) 2015 Hakim El Hattab, http://hakim.se */ (function( root, factory ) { if( typeof define === 'function' && define.amd ) { // AMD. Register as an anonymous module. define( function() { root.Reveal = factory(); return root.Reveal; } ); } else if( typeof exports === 'object' ) { // Node. Does not work with strict CommonJS. module.exports = factory(); } else { // Browser globals. root.Reveal = factory(); } }( this, function() { 'use strict'; var Reveal; var SLIDES_SELECTOR = '.slides section', HORIZONTAL_SLIDES_SELECTOR = '.slides>section', VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section', HOME_SLIDE_SELECTOR = '.slides>section:first-of-type', // Configuration defaults, can be overridden at initialization time config = { // The "normal" size of the presentation, aspect ratio will be preserved // when the presentation is scaled to fit different resolutions width: 960, height: 700, // Factor of the display size that should remain empty around the content margin: 0.1, // Bounds for smallest/largest possible scale to apply to content minScale: 0.2, maxScale: 1.5, // Display controls in the bottom right corner controls: true, // Display a presentation progress bar progress: true, // Display the page number of the current slide slideNumber: false, // Push each slide change to the browser history history: false, // Enable keyboard shortcuts for navigation keyboard: true, // Optional function that blocks keyboard events when retuning false keyboardCondition: null, // Enable the slide overview mode overview: true, // Vertical centering of slides center: true, // Enables touch navigation on devices with touch input touch: true, // Loop the presentation loop: false, // Change the presentation direction to be RTL rtl: false, // Turns fragments on and off globally fragments: true, // Flags if the presentation is running in an embedded mode, // i.e. contained within a limited portion of the screen embedded: false, // Flags if we should show a help overlay when the questionmark // key is pressed help: true, // Flags if it should be possible to pause the presentation (blackout) pause: true, // Flags if speaker notes should be visible to all viewers showNotes: false, // Number of milliseconds between automatically proceeding to the // next slide, disabled when set to 0, this value can be overwritten // by using a data-autoslide attribute on your slides autoSlide: 0, // Stop auto-sliding after user input autoSlideStoppable: true, // Enable slide navigation via mouse wheel mouseWheel: false, // Apply a 3D roll to links on hover rollingLinks: false, // Hides the address bar on mobile devices hideAddressBar: true, // Opens links in an iframe preview overlay previewLinks: false, // Exposes the reveal.js API through window.postMessage postMessage: true, // Dispatches all reveal.js events to the parent window through postMessage postMessageEvents: false, // Focuses body when page changes visibility to ensure keyboard shortcuts work focusBodyOnPageVisibilityChange: true, // Transition style transition: 'slide', // none/fade/slide/convex/concave/zoom // Transition speed transitionSpeed: 'default', // default/fast/slow // Transition style for full page slide backgrounds backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom // Parallax background image parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg" // Parallax background size parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px" // Amount of pixels to move the parallax background per slide step parallaxBackgroundHorizontal: null, parallaxBackgroundVertical: null, // Number of slides away from the current that are visible viewDistance: 3, // Script dependencies to load dependencies: [] }, // Flags if reveal.js is loaded (has dispatched the 'ready' event) loaded = false, // Flags if the overview mode is currently active overview = false, // The horizontal and vertical index of the currently active slide indexh, indexv, // The previous and current slide HTML elements previousSlide, currentSlide, previousBackground, // Slides may hold a data-state attribute which we pick up and apply // as a class to the body. This list contains the combined state of // all current slides. state = [], // The current scale of the presentation (see width/height config) scale = 1, // CSS transform that is currently applied to the slides container, // split into two groups slidesTransform = { layout: '', overview: '' }, // Cached references to DOM elements dom = {}, // Features supported by the browser, see #checkCapabilities() features = {}, // Client is a mobile device, see #checkCapabilities() isMobileDevice, // Throttles mouse wheel navigation lastMouseWheelStep = 0, // Delays updates to the URL due to a Chrome thumbnailer bug writeURLTimeout = 0, // Flags if the interaction event listeners are bound eventsAreBound = false, // The current auto-slide duration autoSlide = 0, // Auto slide properties autoSlidePlayer, autoSlideTimeout = 0, autoSlideStartTime = -1, autoSlidePaused = false, // Holds information about the currently ongoing touch input touch = { startX: 0, startY: 0, startSpan: 0, startCount: 0, captured: false, threshold: 40 }, // Holds information about the keyboard shortcuts keyboardShortcuts = { 'N , SPACE': 'Next slide', 'P': 'Previous slide', '← , H': 'Navigate left', '→ , L': 'Navigate right', '↑ , K': 'Navigate up', '↓ , J': 'Navigate down', 'Home': 'First slide', 'End': 'Last slide', 'B , .': 'Pause', 'F': 'Fullscreen', 'ESC, O': 'Slide overview' }; /** * Starts up the presentation if the client is capable. */ function initialize( options ) { checkCapabilities(); if( !features.transforms2d && !features.transforms3d ) { document.body.setAttribute( 'class', 'no-transforms' ); // Since JS won't be running any further, we load all lazy // loading elements upfront var images = toArray( document.getElementsByTagName( 'img' ) ), iframes = toArray( document.getElementsByTagName( 'iframe' ) ); var lazyLoadable = images.concat( iframes ); for( var i = 0, len = lazyLoadable.length; i < len; i++ ) { var element = lazyLoadable[i]; if( element.getAttribute( 'data-src' ) ) { element.setAttribute( 'src', element.getAttribute( 'data-src' ) ); element.removeAttribute( 'data-src' ); } } // If the browser doesn't support core features we won't be // using JavaScript to control the presentation return; } // Cache references to key DOM elements dom.wrapper = document.querySelector( '.reveal' ); dom.slides = document.querySelector( '.reveal .slides' ); // Force a layout when the whole page, incl fonts, has loaded window.addEventListener( 'load', layout, false ); var query = Reveal.getQueryHash(); // Do not accept new dependencies via query config to avoid // the potential of malicious script injection if( typeof query['dependencies'] !== 'undefined' ) delete query['dependencies']; // Copy options over to our config object extend( config, options ); extend( config, query ); // Hide the address bar in mobile browsers hideAddressBar(); // Loads the dependencies and continues to #start() once done load(); } /** * Inspect the client to see what it's capable of, this * should only happens once per runtime. */ function checkCapabilities() { features.transforms3d = 'WebkitPerspective' in document.body.style || 'MozPerspective' in document.body.style || 'msPerspective' in document.body.style || 'OPerspective' in document.body.style || 'perspective' in document.body.style; features.transforms2d = 'WebkitTransform' in document.body.style || 'MozTransform' in document.body.style || 'msTransform' in document.body.style || 'OTransform' in document.body.style || 'transform' in document.body.style; features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function'; features.canvas = !!document.createElement( 'canvas' ).getContext; features.touch = !!( 'ontouchstart' in window ); // Transitions in the overview are disabled in desktop and // mobile Safari due to lag features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test( navigator.userAgent ); isMobileDevice = /(iphone|ipod|ipad|android)/gi.test( navigator.userAgent ); } /** * Loads the dependencies of reveal.js. Dependencies are * defined via the configuration option 'dependencies' * and will be loaded prior to starting/binding reveal.js. * Some dependencies may have an 'async' flag, if so they * will load after reveal.js has been started up. */ function load() { var scripts = [], scriptsAsync = [], scriptsToPreload = 0; // Called once synchronous scripts finish loading function proceed() { if( scriptsAsync.length ) { // Load asynchronous scripts head.js.apply( null, scriptsAsync ); } start(); } function loadScript( s ) { head.ready( s.src.match( /([\w\d_\-]*)\.?js$|[^\\\/]*$/i )[0], function() { // Extension may contain callback functions if( typeof s.callback === 'function' ) { s.callback.apply( this ); } if( --scriptsToPreload === 0 ) { proceed(); } }); } for( var i = 0, len = config.dependencies.length; i < len; i++ ) { var s = config.dependencies[i]; // Load if there's no condition or the condition is truthy if( !s.condition || s.condition() ) { if( s.async ) { scriptsAsync.push( s.src ); } else { scripts.push( s.src ); } loadScript( s ); } } if( scripts.length ) { scriptsToPreload = scripts.length; // Load synchronous scripts head.js.apply( null, scripts ); } else { proceed(); } } /** * Starts up reveal.js by binding input events and navigating * to the current URL deeplink if there is one. */ function start() { // Make sure we've got all the DOM elements we need setupDOM(); // Listen to messages posted to this window setupPostMessage(); // Prevent iframes from scrolling the slides out of view setupIframeScrollPrevention(); // Resets all vertical slides so that only the first is visible resetVerticalSlides(); // Updates the presentation to match the current configuration values configure(); // Read the initial hash readURL(); // Update all backgrounds updateBackground( true ); // Notify listeners that the presentation is ready but use a 1ms // timeout to ensure it's not fired synchronously after #initialize() setTimeout( function() { // Enable transitions now that we're loaded dom.slides.classList.remove( 'no-transition' ); loaded = true; dispatchEvent( 'ready', { 'indexh': indexh, 'indexv': indexv, 'currentSlide': currentSlide } ); }, 1 ); // Special setup and config is required when printing to PDF if( isPrintingPDF() ) { removeEventListeners(); // The document needs to have loaded for the PDF layout // measurements to be accurate if( document.readyState === 'complete' ) { setupPDF(); } else { window.addEventListener( 'load', setupPDF ); } } } /** * Finds and stores references to DOM elements which are * required by the presentation. If a required element is * not found, it is created. */ function setupDOM() { // Prevent transitions while we're loading dom.slides.classList.add( 'no-transition' ); // Background element dom.background = createSingletonNode( dom.wrapper, 'div', 'backgrounds', null ); // Progress bar dom.progress = createSingletonNode( dom.wrapper, 'div', 'progress', '' ); dom.progressbar = dom.progress.querySelector( 'span' ); // Arrow controls createSingletonNode( dom.wrapper, 'aside', 'controls', '' + '' + '' + '' ); // Slide number dom.slideNumber = createSingletonNode( dom.wrapper, 'div', 'slide-number', '' ); // Element containing notes that are visible to the audience dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null ); dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' ); // Overlay graphic which is displayed during the paused mode createSingletonNode( dom.wrapper, 'div', 'pause-overlay', null ); // Cache references to elements dom.controls = document.querySelector( '.reveal .controls' ); dom.theme = document.querySelector( '#theme' ); dom.wrapper.setAttribute( 'role', 'application' ); // There can be multiple instances of controls throughout the page dom.controlsLeft = toArray( document.querySelectorAll( '.navigate-left' ) ); dom.controlsRight = toArray( document.querySelectorAll( '.navigate-right' ) ); dom.controlsUp = toArray( document.querySelectorAll( '.navigate-up' ) ); dom.controlsDown = toArray( document.querySelectorAll( '.navigate-down' ) ); dom.controlsPrev = toArray( document.querySelectorAll( '.navigate-prev' ) ); dom.controlsNext = toArray( document.querySelectorAll( '.navigate-next' ) ); dom.statusDiv = createStatusDiv(); } /** * Creates a hidden div with role aria-live to announce the * current slide content. Hide the div off-screen to make it * available only to Assistive Technologies. */ function createStatusDiv() { var statusDiv = document.getElementById( 'aria-status-div' ); if( !statusDiv ) { statusDiv = document.createElement( 'div' ); statusDiv.style.position = 'absolute'; statusDiv.style.height = '1px'; statusDiv.style.width = '1px'; statusDiv.style.overflow ='hidden'; statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )'; statusDiv.setAttribute( 'id', 'aria-status-div' ); statusDiv.setAttribute( 'aria-live', 'polite' ); statusDiv.setAttribute( 'aria-atomic','true' ); dom.wrapper.appendChild( statusDiv ); } return statusDiv; } /** * Configures the presentation for printing to a static * PDF. */ function setupPDF() { var slideSize = getComputedSlideSize( window.innerWidth, window.innerHeight ); // Dimensions of the PDF pages var pageWidth = Math.floor( slideSize.width * ( 1 + config.margin ) ), pageHeight = Math.floor( slideSize.height * ( 1 + config.margin ) ); // Dimensions of slides within the pages var slideWidth = slideSize.width, slideHeight = slideSize.height; // Let the browser know what page size we want to print injectStyleSheet( '@page{size:'+ pageWidth +'px '+ pageHeight +'px; margin: 0;}' ); // Limit the size of certain elements to the dimensions of the slide injectStyleSheet( '.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: '+ slideWidth +'px; max-height:'+ slideHeight +'px}' ); document.body.classList.add( 'print-pdf' ); document.body.style.width = pageWidth + 'px'; document.body.style.height = pageHeight + 'px'; // Add each slide's index as attributes on itself, we need these // indices to generate slide numbers below toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) { hslide.setAttribute( 'data-index-h', h ); if( hslide.classList.contains( 'stack' ) ) { toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) { vslide.setAttribute( 'data-index-h', h ); vslide.setAttribute( 'data-index-v', v ); } ); } } ); // Slide and slide background layout toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) { // Vertical stacks are not centred since their section // children will be if( slide.classList.contains( 'stack' ) === false ) { // Center the slide inside of the page, giving the slide some margin var left = ( pageWidth - slideWidth ) / 2, top = ( pageHeight - slideHeight ) / 2; var contentHeight = getAbsoluteHeight( slide ); var numberOfPages = Math.max( Math.ceil( contentHeight / pageHeight ), 1 ); // Center slides vertically if( numberOfPages === 1 && config.center || slide.classList.contains( 'center' ) ) { top = Math.max( ( pageHeight - contentHeight ) / 2, 0 ); } // Position the slide inside of the page slide.style.left = left + 'px'; slide.style.top = top + 'px'; slide.style.width = slideWidth + 'px'; // TODO Backgrounds need to be multiplied when the slide // stretches over multiple pages var background = slide.querySelector( '.slide-background' ); if( background ) { background.style.width = pageWidth + 'px'; background.style.height = ( pageHeight * numberOfPages ) + 'px'; background.style.top = -top + 'px'; background.style.left = -left + 'px'; } // Inject notes if `showNotes` is enabled if( config.showNotes ) { var notes = getSlideNotes( slide ); if( notes ) { var notesSpacing = 8; var notesElement = document.createElement( 'div' ); notesElement.classList.add( 'speaker-notes' ); notesElement.classList.add( 'speaker-notes-pdf' ); notesElement.innerHTML = notes; notesElement.style.left = ( notesSpacing - left ) + 'px'; notesElement.style.bottom = ( notesSpacing - top ) + 'px'; notesElement.style.width = ( pageWidth - notesSpacing*2 ) + 'px'; slide.appendChild( notesElement ); } } // Inject slide numbers if `slideNumbers` are enabled if( config.slideNumber ) { var slideNumberH = parseInt( slide.getAttribute( 'data-index-h' ), 10 ) + 1, slideNumberV = parseInt( slide.getAttribute( 'data-index-v' ), 10 ) + 1; var numberElement = document.createElement( 'div' ); numberElement.classList.add( 'slide-number' ); numberElement.classList.add( 'slide-number-pdf' ); numberElement.innerHTML = formatSlideNumber( slideNumberH, '.', slideNumberV ); background.appendChild( numberElement ); } } } ); // Show all fragments toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' .fragment' ) ).forEach( function( fragment ) { fragment.classList.add( 'visible' ); } ); } /** * This is an unfortunate necessity. Iframes can trigger the * parent window to scroll, for example by focusing an input. * This scrolling can not be prevented by hiding overflow in * CSS so we have to resort to repeatedly checking if the * browser has decided to offset our slides :( */ function setupIframeScrollPrevention() { if( dom.slides.querySelector( 'iframe' ) ) { setInterval( function() { if( dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0 ) { dom.wrapper.scrollTop = 0; dom.wrapper.scrollLeft = 0; } }, 500 ); } } /** * Creates an HTML element and returns a reference to it. * If the element already exists the existing instance will * be returned. */ function createSingletonNode( container, tagname, classname, innerHTML ) { // Find all nodes matching the description var nodes = container.querySelectorAll( '.' + classname ); // Check all matches to find one which is a direct child of // the specified container for( var i = 0; i < nodes.length; i++ ) { var testNode = nodes[i]; if( testNode.parentNode === container ) { return testNode; } } // If no node was found, create it now var node = document.createElement( tagname ); node.classList.add( classname ); if( typeof innerHTML === 'string' ) { node.innerHTML = innerHTML; } container.appendChild( node ); return node; } /** * Creates the slide background elements and appends them * to the background container. One element is created per * slide no matter if the given slide has visible background. */ function createBackgrounds() { var printMode = isPrintingPDF(); // Clear prior backgrounds dom.background.innerHTML = ''; dom.background.classList.add( 'no-transition' ); // Iterate over all horizontal slides toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( slideh ) { var backgroundStack; if( printMode ) { backgroundStack = createBackground( slideh, slideh ); } else { backgroundStack = createBackground( slideh, dom.background ); } // Iterate over all vertical slides toArray( slideh.querySelectorAll( 'section' ) ).forEach( function( slidev ) { if( printMode ) { createBackground( slidev, slidev ); } else { createBackground( slidev, backgroundStack ); } backgroundStack.classList.add( 'stack' ); } ); } ); // Add parallax background if specified if( config.parallaxBackgroundImage ) { dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")'; dom.background.style.backgroundSize = config.parallaxBackgroundSize; // Make sure the below properties are set on the element - these properties are // needed for proper transitions to be set on the element via CSS. To remove // annoying background slide-in effect when the presentation starts, apply // these properties after short time delay setTimeout( function() { dom.wrapper.classList.add( 'has-parallax-background' ); }, 1 ); } else { dom.background.style.backgroundImage = ''; dom.wrapper.classList.remove( 'has-parallax-background' ); } } /** * Creates a background for the given slide. * * @param {HTMLElement} slide * @param {HTMLElement} container The element that the background * should be appended to */ function createBackground( slide, container ) { var data = { background: slide.getAttribute( 'data-background' ), backgroundSize: slide.getAttribute( 'data-background-size' ), backgroundImage: slide.getAttribute( 'data-background-image' ), backgroundVideo: slide.getAttribute( 'data-background-video' ), backgroundIframe: slide.getAttribute( 'data-background-iframe' ), backgroundColor: slide.getAttribute( 'data-background-color' ), backgroundRepeat: slide.getAttribute( 'data-background-repeat' ), backgroundPosition: slide.getAttribute( 'data-background-position' ), backgroundTransition: slide.getAttribute( 'data-background-transition' ) }; var element = document.createElement( 'div' ); // Carry over custom classes from the slide to the background element.className = 'slide-background ' + slide.className.replace( /present|past|future/, '' ); if( data.background ) { // Auto-wrap image urls in url(...) if( /^(http|file|\/\/)/gi.test( data.background ) || /\.(svg|png|jpg|jpeg|gif|bmp)$/gi.test( data.background ) ) { slide.setAttribute( 'data-background-image', data.background ); } else { element.style.background = data.background; } } // Create a hash for this combination of background settings. // This is used to determine when two slide backgrounds are // the same. if( data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe ) { element.setAttribute( 'data-background-hash', data.background + data.backgroundSize + data.backgroundImage + data.backgroundVideo + data.backgroundIframe + data.backgroundColor + data.backgroundRepeat + data.backgroundPosition + data.backgroundTransition ); } // Additional and optional background properties if( data.backgroundSize ) element.style.backgroundSize = data.backgroundSize; if( data.backgroundColor ) element.style.backgroundColor = data.backgroundColor; if( data.backgroundRepeat ) element.style.backgroundRepeat = data.backgroundRepeat; if( data.backgroundPosition ) element.style.backgroundPosition = data.backgroundPosition; if( data.backgroundTransition ) element.setAttribute( 'data-background-transition', data.backgroundTransition ); container.appendChild( element ); // If backgrounds are being recreated, clear old classes slide.classList.remove( 'has-dark-background' ); slide.classList.remove( 'has-light-background' ); // If this slide has a background color, add a class that // signals if it is light or dark. If the slide has no background // color, no class will be set var computedBackgroundColor = window.getComputedStyle( element ).backgroundColor; if( computedBackgroundColor ) { var rgb = colorToRgb( computedBackgroundColor ); // Ignore fully transparent backgrounds. Some browsers return // rgba(0,0,0,0) when reading the computed background color of // an element with no background if( rgb && rgb.a !== 0 ) { if( colorBrightness( computedBackgroundColor ) < 128 ) { slide.classList.add( 'has-dark-background' ); } else { slide.classList.add( 'has-light-background' ); } } } return element; } /** * Registers a listener to postMessage events, this makes it * possible to call all reveal.js API methods from another * window. For example: * * revealWindow.postMessage( JSON.stringify({ * method: 'slide', * args: [ 2 ] * }), '*' ); */ function setupPostMessage() { if( config.postMessage ) { window.addEventListener( 'message', function ( event ) { var data = event.data; // Make sure we're dealing with JSON if( typeof data === 'string' && data.charAt( 0 ) === '{' && data.charAt( data.length - 1 ) === '}' ) { data = JSON.parse( data ); // Check if the requested method can be found if( data.method && typeof Reveal[data.method] === 'function' ) { Reveal[data.method].apply( Reveal, data.args ); } } }, false ); } } /** * Applies the configuration settings from the config * object. May be called multiple times. */ function configure( options ) { var numberOfSlides = dom.wrapper.querySelectorAll( SLIDES_SELECTOR ).length; dom.wrapper.classList.remove( config.transition ); // New config options may be passed when this method // is invoked through the API after initialization if( typeof options === 'object' ) extend( config, options ); // Force linear transition based on browser capabilities if( features.transforms3d === false ) config.transition = 'linear'; dom.wrapper.classList.add( config.transition ); dom.wrapper.setAttribute( 'data-transition-speed', config.transitionSpeed ); dom.wrapper.setAttribute( 'data-background-transition', config.backgroundTransition ); dom.controls.style.display = config.controls ? 'block' : 'none'; dom.progress.style.display = config.progress ? 'block' : 'none'; dom.slideNumber.style.display = config.slideNumber && !isPrintingPDF() ? 'block' : 'none'; if( config.rtl ) { dom.wrapper.classList.add( 'rtl' ); } else { dom.wrapper.classList.remove( 'rtl' ); } if( config.center ) { dom.wrapper.classList.add( 'center' ); } else { dom.wrapper.classList.remove( 'center' ); } // Exit the paused mode if it was configured off if( config.pause === false ) { resume(); } if( config.showNotes ) { dom.speakerNotes.classList.add( 'visible' ); } else { dom.speakerNotes.classList.remove( 'visible' ); } if( config.mouseWheel ) { document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF document.addEventListener( 'mousewheel', onDocumentMouseScroll, false ); } else { document.removeEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF document.removeEventListener( 'mousewheel', onDocumentMouseScroll, false ); } // Rolling 3D links if( config.rollingLinks ) { enableRollingLinks(); } else { disableRollingLinks(); } // Iframe link previews if( config.previewLinks ) { enablePreviewLinks(); } else { disablePreviewLinks(); enablePreviewLinks( '[data-preview-link]' ); } // Remove existing auto-slide controls if( autoSlidePlayer ) { autoSlidePlayer.destroy(); autoSlidePlayer = null; } // Generate auto-slide controls if needed if( numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame ) { autoSlidePlayer = new Playback( dom.wrapper, function() { return Math.min( Math.max( ( Date.now() - autoSlideStartTime ) / autoSlide, 0 ), 1 ); } ); autoSlidePlayer.on( 'click', onAutoSlidePlayerClick ); autoSlidePaused = false; } // When fragments are turned off they should be visible if( config.fragments === false ) { toArray( dom.slides.querySelectorAll( '.fragment' ) ).forEach( function( element ) { element.classList.add( 'visible' ); element.classList.remove( 'current-fragment' ); } ); } sync(); } /** * Binds all event listeners. */ function addEventListeners() { eventsAreBound = true; window.addEventListener( 'hashchange', onWindowHashChange, false ); window.addEventListener( 'resize', onWindowResize, false ); if( config.touch ) { dom.wrapper.addEventListener( 'touchstart', onTouchStart, false ); dom.wrapper.addEventListener( 'touchmove', onTouchMove, false ); dom.wrapper.addEventListener( 'touchend', onTouchEnd, false ); // Support pointer-style touch interaction as well if( window.navigator.pointerEnabled ) { // IE 11 uses un-prefixed version of pointer events dom.wrapper.addEventListener( 'pointerdown', onPointerDown, false ); dom.wrapper.addEventListener( 'pointermove', onPointerMove, false ); dom.wrapper.addEventListener( 'pointerup', onPointerUp, false ); } else if( window.navigator.msPointerEnabled ) { // IE 10 uses prefixed version of pointer events dom.wrapper.addEventListener( 'MSPointerDown', onPointerDown, false ); dom.wrapper.addEventListener( 'MSPointerMove', onPointerMove, false ); dom.wrapper.addEventListener( 'MSPointerUp', onPointerUp, false ); } } if( config.keyboard ) { document.addEventListener( 'keydown', onDocumentKeyDown, false ); document.addEventListener( 'keypress', onDocumentKeyPress, false ); } if( config.progress && dom.progress ) { dom.progress.addEventListener( 'click', onProgressClicked, false ); } if( config.focusBodyOnPageVisibilityChange ) { var visibilityChange; if( 'hidden' in document ) { visibilityChange = 'visibilitychange'; } else if( 'msHidden' in document ) { visibilityChange = 'msvisibilitychange'; } else if( 'webkitHidden' in document ) { visibilityChange = 'webkitvisibilitychange'; } if( visibilityChange ) { document.addEventListener( visibilityChange, onPageVisibilityChange, false ); } } // Listen to both touch and click events, in case the device // supports both var pointerEvents = [ 'touchstart', 'click' ]; // Only support touch for Android, fixes double navigations in // stock browser if( navigator.userAgent.match( /android/gi ) ) { pointerEvents = [ 'touchstart' ]; } pointerEvents.forEach( function( eventName ) { dom.controlsLeft.forEach( function( el ) { el.addEventListener( eventName, onNavigateLeftClicked, false ); } ); dom.controlsRight.forEach( function( el ) { el.addEventListener( eventName, onNavigateRightClicked, false ); } ); dom.controlsUp.forEach( function( el ) { el.addEventListener( eventName, onNavigateUpClicked, false ); } ); dom.controlsDown.forEach( function( el ) { el.addEventListener( eventName, onNavigateDownClicked, false ); } ); dom.controlsPrev.forEach( function( el ) { el.addEventListener( eventName, onNavigatePrevClicked, false ); } ); dom.controlsNext.forEach( function( el ) { el.addEventListener( eventName, onNavigateNextClicked, false ); } ); } ); } /** * Unbinds all event listeners. */ function removeEventListeners() { eventsAreBound = false; document.removeEventListener( 'keydown', onDocumentKeyDown, false ); document.removeEventListener( 'keypress', onDocumentKeyPress, false ); window.removeEventListener( 'hashchange', onWindowHashChange, false ); window.removeEventListener( 'resize', onWindowResize, false ); dom.wrapper.removeEventListener( 'touchstart', onTouchStart, false ); dom.wrapper.removeEventListener( 'touchmove', onTouchMove, false ); dom.wrapper.removeEventListener( 'touchend', onTouchEnd, false ); // IE11 if( window.navigator.pointerEnabled ) { dom.wrapper.removeEventListener( 'pointerdown', onPointerDown, false ); dom.wrapper.removeEventListener( 'pointermove', onPointerMove, false ); dom.wrapper.removeEventListener( 'pointerup', onPointerUp, false ); } // IE10 else if( window.navigator.msPointerEnabled ) { dom.wrapper.removeEventListener( 'MSPointerDown', onPointerDown, false ); dom.wrapper.removeEventListener( 'MSPointerMove', onPointerMove, false ); dom.wrapper.removeEventListener( 'MSPointerUp', onPointerUp, false ); } if ( config.progress && dom.progress ) { dom.progress.removeEventListener( 'click', onProgressClicked, false ); } [ 'touchstart', 'click' ].forEach( function( eventName ) { dom.controlsLeft.forEach( function( el ) { el.removeEventListener( eventName, onNavigateLeftClicked, false ); } ); dom.controlsRight.forEach( function( el ) { el.removeEventListener( eventName, onNavigateRightClicked, false ); } ); dom.controlsUp.forEach( function( el ) { el.removeEventListener( eventName, onNavigateUpClicked, false ); } ); dom.controlsDown.forEach( function( el ) { el.removeEventListener( eventName, onNavigateDownClicked, false ); } ); dom.controlsPrev.forEach( function( el ) { el.removeEventListener( eventName, onNavigatePrevClicked, false ); } ); dom.controlsNext.forEach( function( el ) { el.removeEventListener( eventName, onNavigateNextClicked, false ); } ); } ); } /** * Extend object a with the properties of object b. * If there's a conflict, object b takes precedence. */ function extend( a, b ) { for( var i in b ) { a[ i ] = b[ i ]; } } /** * Converts the target object to an array. */ function toArray( o ) { return Array.prototype.slice.call( o ); } /** * Utility for deserializing a value. */ function deserialize( value ) { if( typeof value === 'string' ) { if( value === 'null' ) return null; else if( value === 'true' ) return true; else if( value === 'false' ) return false; else if( value.match( /^\d+$/ ) ) return parseFloat( value ); } return value; } /** * Measures the distance in pixels between point a * and point b. * * @param {Object} a point with x/y properties * @param {Object} b point with x/y properties */ function distanceBetween( a, b ) { var dx = a.x - b.x, dy = a.y - b.y; return Math.sqrt( dx*dx + dy*dy ); } /** * Applies a CSS transform to the target element. */ function transformElement( element, transform ) { element.style.WebkitTransform = transform; element.style.MozTransform = transform; element.style.msTransform = transform; element.style.transform = transform; } /** * Applies CSS transforms to the slides container. The container * is transformed from two separate sources: layout and the overview * mode. */ function transformSlides( transforms ) { // Pick up new transforms from arguments if( typeof transforms.layout === 'string' ) slidesTransform.layout = transforms.layout; if( typeof transforms.overview === 'string' ) slidesTransform.overview = transforms.overview; // Apply the transforms to the slides container if( slidesTransform.layout ) { transformElement( dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview ); } else { transformElement( dom.slides, slidesTransform.overview ); } } /** * Injects the given CSS styles into the DOM. */ function injectStyleSheet( value ) { var tag = document.createElement( 'style' ); tag.type = 'text/css'; if( tag.styleSheet ) { tag.styleSheet.cssText = value; } else { tag.appendChild( document.createTextNode( value ) ); } document.getElementsByTagName( 'head' )[0].appendChild( tag ); } /** * Converts various color input formats to an {r:0,g:0,b:0} object. * * @param {String} color The string representation of a color, * the following formats are supported: * - #000 * - #000000 * - rgb(0,0,0) */ function colorToRgb( color ) { var hex3 = color.match( /^#([0-9a-f]{3})$/i ); if( hex3 && hex3[1] ) { hex3 = hex3[1]; return { r: parseInt( hex3.charAt( 0 ), 16 ) * 0x11, g: parseInt( hex3.charAt( 1 ), 16 ) * 0x11, b: parseInt( hex3.charAt( 2 ), 16 ) * 0x11 }; } var hex6 = color.match( /^#([0-9a-f]{6})$/i ); if( hex6 && hex6[1] ) { hex6 = hex6[1]; return { r: parseInt( hex6.substr( 0, 2 ), 16 ), g: parseInt( hex6.substr( 2, 2 ), 16 ), b: parseInt( hex6.substr( 4, 2 ), 16 ) }; } var rgb = color.match( /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i ); if( rgb ) { return { r: parseInt( rgb[1], 10 ), g: parseInt( rgb[2], 10 ), b: parseInt( rgb[3], 10 ) }; } var rgba = color.match( /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i ); if( rgba ) { return { r: parseInt( rgba[1], 10 ), g: parseInt( rgba[2], 10 ), b: parseInt( rgba[3], 10 ), a: parseFloat( rgba[4] ) }; } return null; } /** * Calculates brightness on a scale of 0-255. * * @param color See colorStringToRgb for supported formats. */ function colorBrightness( color ) { if( typeof color === 'string' ) color = colorToRgb( color ); if( color ) { return ( color.r * 299 + color.g * 587 + color.b * 114 ) / 1000; } return null; } /** * Retrieves the height of the given element by looking * at the position and height of its immediate children. */ function getAbsoluteHeight( element ) { var height = 0; if( element ) { var absoluteChildren = 0; toArray( element.childNodes ).forEach( function( child ) { if( typeof child.offsetTop === 'number' && child.style ) { // Count # of abs children if( window.getComputedStyle( child ).position === 'absolute' ) { absoluteChildren += 1; } height = Math.max( height, child.offsetTop + child.offsetHeight ); } } ); // If there are no absolute children, use offsetHeight if( absoluteChildren === 0 ) { height = element.offsetHeight; } } return height; } /** * Returns the remaining height within the parent of the * target element. * * remaining height = [ configured parent height ] - [ current parent height ] */ function getRemainingHeight( element, height ) { height = height || 0; if( element ) { var newHeight, oldHeight = element.style.height; // Change the .stretch element height to 0 in order find the height of all // the other elements element.style.height = '0px'; newHeight = height - element.parentNode.offsetHeight; // Restore the old height, just in case element.style.height = oldHeight + 'px'; return newHeight; } return height; } /** * Checks if this instance is being used to print a PDF. */ function isPrintingPDF() { return ( /print-pdf/gi ).test( window.location.search ); } /** * Hides the address bar if we're on a mobile device. */ function hideAddressBar() { if( config.hideAddressBar && isMobileDevice ) { // Events that should trigger the address bar to hide window.addEventListener( 'load', removeAddressBar, false ); window.addEventListener( 'orientationchange', removeAddressBar, false ); } } /** * Causes the address bar to hide on mobile devices, * more vertical space ftw. */ function removeAddressBar() { setTimeout( function() { window.scrollTo( 0, 1 ); }, 10 ); } /** * Dispatches an event of the specified type from the * reveal DOM element. */ function dispatchEvent( type, args ) { var event = document.createEvent( 'HTMLEvents', 1, 2 ); event.initEvent( type, true, true ); extend( event, args ); dom.wrapper.dispatchEvent( event ); // If we're in an iframe, post each reveal.js event to the // parent window. Used by the notes plugin if( config.postMessageEvents && window.parent !== window.self ) { window.parent.postMessage( JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*' ); } } /** * Wrap all links in 3D goodness. */ function enableRollingLinks() { if( features.transforms3d && !( 'msPerspective' in document.body.style ) ) { var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a' ); for( var i = 0, len = anchors.length; i < len; i++ ) { var anchor = anchors[i]; if( anchor.textContent && !anchor.querySelector( '*' ) && ( !anchor.className || !anchor.classList.contains( anchor, 'roll' ) ) ) { var span = document.createElement('span'); span.setAttribute('data-title', anchor.text); span.innerHTML = anchor.innerHTML; anchor.classList.add( 'roll' ); anchor.innerHTML = ''; anchor.appendChild(span); } } } } /** * Unwrap all 3D links. */ function disableRollingLinks() { var anchors = dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ' a.roll' ); for( var i = 0, len = anchors.length; i < len; i++ ) { var anchor = anchors[i]; var span = anchor.querySelector( 'span' ); if( span ) { anchor.classList.remove( 'roll' ); anchor.innerHTML = span.innerHTML; } } } /** * Bind preview frame links. */ function enablePreviewLinks( selector ) { var anchors = toArray( document.querySelectorAll( selector ? selector : 'a' ) ); anchors.forEach( function( element ) { if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { element.addEventListener( 'click', onPreviewLinkClicked, false ); } } ); } /** * Unbind preview frame links. */ function disablePreviewLinks() { var anchors = toArray( document.querySelectorAll( 'a' ) ); anchors.forEach( function( element ) { if( /^(http|www)/gi.test( element.getAttribute( 'href' ) ) ) { element.removeEventListener( 'click', onPreviewLinkClicked, false ); } } ); } /** * Opens a preview window for the target URL. */ function showPreview( url ) { closeOverlay(); dom.overlay = document.createElement( 'div' ); dom.overlay.classList.add( 'overlay' ); dom.overlay.classList.add( 'overlay-preview' ); dom.wrapper.appendChild( dom.overlay ); dom.overlay.innerHTML = [ '
', '', '', '
', '
', '
', '', '
' ].join(''); dom.overlay.querySelector( 'iframe' ).addEventListener( 'load', function( event ) { dom.overlay.classList.add( 'loaded' ); }, false ); dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) { closeOverlay(); event.preventDefault(); }, false ); dom.overlay.querySelector( '.external' ).addEventListener( 'click', function( event ) { closeOverlay(); }, false ); setTimeout( function() { dom.overlay.classList.add( 'visible' ); }, 1 ); } /** * Opens a overlay window with help material. */ function showHelp() { if( config.help ) { closeOverlay(); dom.overlay = document.createElement( 'div' ); dom.overlay.classList.add( 'overlay' ); dom.overlay.classList.add( 'overlay-help' ); dom.wrapper.appendChild( dom.overlay ); var html = '

Keyboard Shortcuts


'; html += ''; for( var key in keyboardShortcuts ) { html += ''; } html += '
KEYACTION
' + key + '' + keyboardShortcuts[ key ] + '
'; dom.overlay.innerHTML = [ '
', '', '
', '
', '
'+ html +'
', '
' ].join(''); dom.overlay.querySelector( '.close' ).addEventListener( 'click', function( event ) { closeOverlay(); event.preventDefault(); }, false ); setTimeout( function() { dom.overlay.classList.add( 'visible' ); }, 1 ); } } /** * Closes any currently open overlay. */ function closeOverlay() { if( dom.overlay ) { dom.overlay.parentNode.removeChild( dom.overlay ); dom.overlay = null; } } /** * Applies JavaScript-controlled layout rules to the * presentation. */ function layout() { if( dom.wrapper && !isPrintingPDF() ) { var size = getComputedSlideSize(); var slidePadding = 20; // TODO Dig this out of DOM // Layout the contents of the slides layoutSlideContents( config.width, config.height, slidePadding ); dom.slides.style.width = size.width + 'px'; dom.slides.style.height = size.height + 'px'; // Determine scale of content to fit within available space scale = Math.min( size.presentationWidth / size.width, size.presentationHeight / size.height ); // Respect max/min scale settings scale = Math.max( scale, config.minScale ); scale = Math.min( scale, config.maxScale ); // Don't apply any scaling styles if scale is 1 if( scale === 1 ) { dom.slides.style.zoom = ''; dom.slides.style.left = ''; dom.slides.style.top = ''; dom.slides.style.bottom = ''; dom.slides.style.right = ''; transformSlides( { layout: '' } ); } else { // Use zoom to scale up in desktop Chrome so that content // remains crisp. We don't use zoom to scale down since that // can lead to shifts in text layout/line breaks. if( scale > 1 && !isMobileDevice && /chrome/i.test( navigator.userAgent ) && typeof dom.slides.style.zoom !== 'undefined' ) { dom.slides.style.zoom = scale; dom.slides.style.left = ''; dom.slides.style.top = ''; dom.slides.style.bottom = ''; dom.slides.style.right = ''; transformSlides( { layout: '' } ); } // Apply scale transform as a fallback else { dom.slides.style.zoom = ''; dom.slides.style.left = '50%'; dom.slides.style.top = '50%'; dom.slides.style.bottom = 'auto'; dom.slides.style.right = 'auto'; transformSlides( { layout: 'translate(-50%, -50%) scale('+ scale +')' } ); } } // Select all slides, vertical and horizontal var slides = toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ); for( var i = 0, len = slides.length; i < len; i++ ) { var slide = slides[ i ]; // Don't bother updating invisible slides if( slide.style.display === 'none' ) { continue; } if( config.center || slide.classList.contains( 'center' ) ) { // Vertical stacks are not centred since their section // children will be if( slide.classList.contains( 'stack' ) ) { slide.style.top = 0; } else { slide.style.top = Math.max( ( ( size.height - getAbsoluteHeight( slide ) ) / 2 ) - slidePadding, 0 ) + 'px'; } } else { slide.style.top = ''; } } updateProgress(); updateParallax(); } } /** * Applies layout logic to the contents of all slides in * the presentation. */ function layoutSlideContents( width, height, padding ) { // Handle sizing of elements with the 'stretch' class toArray( dom.slides.querySelectorAll( 'section > .stretch' ) ).forEach( function( element ) { // Determine how much vertical space we can use var remainingHeight = getRemainingHeight( element, height ); // Consider the aspect ratio of media elements if( /(img|video)/gi.test( element.nodeName ) ) { var nw = element.naturalWidth || element.videoWidth, nh = element.naturalHeight || element.videoHeight; var es = Math.min( width / nw, remainingHeight / nh ); element.style.width = ( nw * es ) + 'px'; element.style.height = ( nh * es ) + 'px'; } else { element.style.width = width + 'px'; element.style.height = remainingHeight + 'px'; } } ); } /** * Calculates the computed pixel size of our slides. These * values are based on the width and height configuration * options. */ function getComputedSlideSize( presentationWidth, presentationHeight ) { var size = { // Slide size width: config.width, height: config.height, // Presentation size presentationWidth: presentationWidth || dom.wrapper.offsetWidth, presentationHeight: presentationHeight || dom.wrapper.offsetHeight }; // Reduce available space by margin size.presentationWidth -= ( size.presentationWidth * config.margin ); size.presentationHeight -= ( size.presentationHeight * config.margin ); // Slide width may be a percentage of available width if( typeof size.width === 'string' && /%$/.test( size.width ) ) { size.width = parseInt( size.width, 10 ) / 100 * size.presentationWidth; } // Slide height may be a percentage of available height if( typeof size.height === 'string' && /%$/.test( size.height ) ) { size.height = parseInt( size.height, 10 ) / 100 * size.presentationHeight; } return size; } /** * Stores the vertical index of a stack so that the same * vertical slide can be selected when navigating to and * from the stack. * * @param {HTMLElement} stack The vertical stack element * @param {int} v Index to memorize */ function setPreviousVerticalIndex( stack, v ) { if( typeof stack === 'object' && typeof stack.setAttribute === 'function' ) { stack.setAttribute( 'data-previous-indexv', v || 0 ); } } /** * Retrieves the vertical index which was stored using * #setPreviousVerticalIndex() or 0 if no previous index * exists. * * @param {HTMLElement} stack The vertical stack element */ function getPreviousVerticalIndex( stack ) { if( typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains( 'stack' ) ) { // Prefer manually defined start-indexv var attributeName = stack.hasAttribute( 'data-start-indexv' ) ? 'data-start-indexv' : 'data-previous-indexv'; return parseInt( stack.getAttribute( attributeName ) || 0, 10 ); } return 0; } /** * Displays the overview of slides (quick nav) by scaling * down and arranging all slide elements. */ function activateOverview() { // Only proceed if enabled in config if( config.overview && !isOverview() ) { overview = true; dom.wrapper.classList.add( 'overview' ); dom.wrapper.classList.remove( 'overview-deactivating' ); if( features.overviewTransitions ) { setTimeout( function() { dom.wrapper.classList.add( 'overview-animated' ); }, 1 ); } // Don't auto-slide while in overview mode cancelAutoSlide(); // Move the backgrounds element into the slide container to // that the same scaling is applied dom.slides.appendChild( dom.background ); // Clicking on an overview slide navigates to it toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) { if( !slide.classList.contains( 'stack' ) ) { slide.addEventListener( 'click', onOverviewSlideClicked, true ); } } ); updateSlidesVisibility(); layoutOverview(); updateOverview(); layout(); // Notify observers of the overview showing dispatchEvent( 'overviewshown', { 'indexh': indexh, 'indexv': indexv, 'currentSlide': currentSlide } ); } } /** * Uses CSS transforms to position all slides in a grid for * display inside of the overview mode. */ function layoutOverview() { var margin = 70; var slideWidth = config.width + margin, slideHeight = config.height + margin; // Reverse in RTL mode if( config.rtl ) { slideWidth = -slideWidth; } // Layout slides toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ).forEach( function( hslide, h ) { hslide.setAttribute( 'data-index-h', h ); transformElement( hslide, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' ); if( hslide.classList.contains( 'stack' ) ) { toArray( hslide.querySelectorAll( 'section' ) ).forEach( function( vslide, v ) { vslide.setAttribute( 'data-index-h', h ); vslide.setAttribute( 'data-index-v', v ); transformElement( vslide, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' ); } ); } } ); // Layout slide backgrounds toArray( dom.background.childNodes ).forEach( function( hbackground, h ) { transformElement( hbackground, 'translate3d(' + ( h * slideWidth ) + 'px, 0, 0)' ); toArray( hbackground.querySelectorAll( '.slide-background' ) ).forEach( function( vbackground, v ) { transformElement( vbackground, 'translate3d(0, ' + ( v * slideHeight ) + 'px, 0)' ); } ); } ); } /** * Moves the overview viewport to the current slides. * Called each time the current slide changes. */ function updateOverview() { var margin = 70; var slideWidth = config.width + margin, slideHeight = config.height + margin; // Reverse in RTL mode if( config.rtl ) { slideWidth = -slideWidth; } transformSlides( { overview: [ 'translateX('+ ( -indexh * slideWidth ) +'px)', 'translateY('+ ( -indexv * slideHeight ) +'px)', 'translateZ('+ ( window.innerWidth < 400 ? -1000 : -2500 ) +'px)' ].join( ' ' ) } ); } /** * Exits the slide overview and enters the currently * active slide. */ function deactivateOverview() { // Only proceed if enabled in config if( config.overview ) { overview = false; dom.wrapper.classList.remove( 'overview' ); dom.wrapper.classList.remove( 'overview-animated' ); // Temporarily add a class so that transitions can do different things // depending on whether they are exiting/entering overview, or just // moving from slide to slide dom.wrapper.classList.add( 'overview-deactivating' ); setTimeout( function () { dom.wrapper.classList.remove( 'overview-deactivating' ); }, 1 ); // Move the background element back out dom.wrapper.appendChild( dom.background ); // Clean up changes made to slides toArray( dom.wrapper.querySelectorAll( SLIDES_SELECTOR ) ).forEach( function( slide ) { transformElement( slide, '' ); slide.removeEventListener( 'click', onOverviewSlideClicked, true ); } ); // Clean up changes made to backgrounds toArray( dom.background.querySelectorAll( '.slide-background' ) ).forEach( function( background ) { transformElement( background, '' ); } ); transformSlides( { overview: '' } ); slide( indexh, indexv ); layout(); cueAutoSlide(); // Notify observers of the overview hiding dispatchEvent( 'overviewhidden', { 'indexh': indexh, 'indexv': indexv, 'currentSlide': currentSlide } ); } } /** * Toggles the slide overview mode on and off. * * @param {Boolean} override Optional flag which overrides the * toggle logic and forcibly sets the desired state. True means * overview is open, false means it's closed. */ function toggleOverview( override ) { if( typeof override === 'boolean' ) { override ? activateOverview() : deactivateOverview(); } else { isOverview() ? deactivateOverview() : activateOverview(); } } /** * Checks if the overview is currently active. * * @return {Boolean} true if the overview is active, * false otherwise */ function isOverview() { return overview; } /** * Checks if the current or specified slide is vertical * (nested within another slide). * * @param {HTMLElement} slide [optional] The slide to check * orientation of */ function isVerticalSlide( slide ) { // Prefer slide argument, otherwise use current slide slide = slide ? slide : currentSlide; return slide && slide.parentNode && !!slide.parentNode.nodeName.match( /section/i ); } /** * Handling the fullscreen functionality via the fullscreen API * * @see http://fullscreen.spec.whatwg.org/ * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode */ function enterFullscreen() { var element = document.body; // Check which implementation is available var requestMethod = element.requestFullScreen || element.webkitRequestFullscreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullscreen; if( requestMethod ) { requestMethod.apply( element ); } } /** * Enters the paused mode which fades everything on screen to * black. */ function pause() { if( config.pause ) { var wasPaused = dom.wrapper.classList.contains( 'paused' ); cancelAutoSlide(); dom.wrapper.classList.add( 'paused' ); if( wasPaused === false ) { dispatchEvent( 'paused' ); } } } /** * Exits from the paused mode. */ function resume() { var wasPaused = dom.wrapper.classList.contains( 'paused' ); dom.wrapper.classList.remove( 'paused' ); cueAutoSlide(); if( wasPaused ) { dispatchEvent( 'resumed' ); } } /** * Toggles the paused mode on and off. */ function togglePause( override ) { if( typeof override === 'boolean' ) { override ? pause() : resume(); } else { isPaused() ? resume() : pause(); } } /** * Checks if we are currently in the paused mode. */ function isPaused() { return dom.wrapper.classList.contains( 'paused' ); } /** * Toggles the auto slide mode on and off. * * @param {Boolean} override Optional flag which sets the desired state. * True means autoplay starts, false means it stops. */ function toggleAutoSlide( override ) { if( typeof override === 'boolean' ) { override ? resumeAutoSlide() : pauseAutoSlide(); } else { autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide(); } } /** * Checks if the auto slide mode is currently on. */ function isAutoSliding() { return !!( autoSlide && !autoSlidePaused ); } /** * Steps from the current point in the presentation to the * slide which matches the specified horizontal and vertical * indices. * * @param {int} h Horizontal index of the target slide * @param {int} v Vertical index of the target slide * @param {int} f Optional index of a fragment within the * target slide to activate * @param {int} o Optional origin for use in multimaster environments */ function slide( h, v, f, o ) { // Remember where we were at before previousSlide = currentSlide; // Query all horizontal slides in the deck var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ); // If no vertical index is specified and the upcoming slide is a // stack, resume at its previous vertical index if( v === undefined && !isOverview() ) { v = getPreviousVerticalIndex( horizontalSlides[ h ] ); } // If we were on a vertical stack, remember what vertical index // it was on so we can resume at the same position when returning if( previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains( 'stack' ) ) { setPreviousVerticalIndex( previousSlide.parentNode, indexv ); } // Remember the state before this slide var stateBefore = state.concat(); // Reset the state array state.length = 0; var indexhBefore = indexh || 0, indexvBefore = indexv || 0; // Activate and transition to the new slide indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h ); indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v ); // Update the visibility of slides now that the indices have changed updateSlidesVisibility(); layout(); // Apply the new state stateLoop: for( var i = 0, len = state.length; i < len; i++ ) { // Check if this state existed on the previous slide. If it // did, we will avoid adding it repeatedly for( var j = 0; j < stateBefore.length; j++ ) { if( stateBefore[j] === state[i] ) { stateBefore.splice( j, 1 ); continue stateLoop; } } document.documentElement.classList.add( state[i] ); // Dispatch custom event matching the state's name dispatchEvent( state[i] ); } // Clean up the remains of the previous state while( stateBefore.length ) { document.documentElement.classList.remove( stateBefore.pop() ); } // Update the overview if it's currently active if( isOverview() ) { updateOverview(); } // Find the current horizontal slide and any possible vertical slides // within it var currentHorizontalSlide = horizontalSlides[ indexh ], currentVerticalSlides = currentHorizontalSlide.querySelectorAll( 'section' ); // Store references to the previous and current slides currentSlide = currentVerticalSlides[ indexv ] || currentHorizontalSlide; // Show fragment, if specified if( typeof f !== 'undefined' ) { navigateFragment( f ); } // Dispatch an event if the slide changed var slideChanged = ( indexh !== indexhBefore || indexv !== indexvBefore ); if( slideChanged ) { dispatchEvent( 'slidechanged', { 'indexh': indexh, 'indexv': indexv, 'previousSlide': previousSlide, 'currentSlide': currentSlide, 'origin': o } ); } else { // Ensure that the previous slide is never the same as the current previousSlide = null; } // Solves an edge case where the previous slide maintains the // 'present' class when navigating between adjacent vertical // stacks if( previousSlide ) { previousSlide.classList.remove( 'present' ); previousSlide.setAttribute( 'aria-hidden', 'true' ); // Reset all slides upon navigate to home // Issue: #285 if ( dom.wrapper.querySelector( HOME_SLIDE_SELECTOR ).classList.contains( 'present' ) ) { // Launch async task setTimeout( function () { var slides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.stack') ), i; for( i in slides ) { if( slides[i] ) { // Reset stack setPreviousVerticalIndex( slides[i], 0 ); } } }, 0 ); } } // Handle embedded content if( slideChanged || !previousSlide ) { stopEmbeddedContent( previousSlide ); startEmbeddedContent( currentSlide ); } // Announce the current slide contents, for screen readers dom.statusDiv.textContent = currentSlide.textContent; updateControls(); updateProgress(); updateBackground(); updateParallax(); updateSlideNumber(); updateNotes(); // Update the URL hash writeURL(); cueAutoSlide(); } /** * Syncs the presentation with the current DOM. Useful * when new slides or control elements are added or when * the configuration has changed. */ function sync() { // Subscribe to input removeEventListeners(); addEventListeners(); // Force a layout to make sure the current config is accounted for layout(); // Reflect the current autoSlide value autoSlide = config.autoSlide; // Start auto-sliding if it's enabled cueAutoSlide(); // Re-create the slide backgrounds createBackgrounds(); // Write the current hash to the URL writeURL(); sortAllFragments(); updateControls(); updateProgress(); updateBackground( true ); updateSlideNumber(); updateSlidesVisibility(); updateNotes(); formatEmbeddedContent(); startEmbeddedContent( currentSlide ); if( isOverview() ) { layoutOverview(); } } /** * Resets all vertical slides so that only the first * is visible. */ function resetVerticalSlides() { var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); horizontalSlides.forEach( function( horizontalSlide ) { var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); verticalSlides.forEach( function( verticalSlide, y ) { if( y > 0 ) { verticalSlide.classList.remove( 'present' ); verticalSlide.classList.remove( 'past' ); verticalSlide.classList.add( 'future' ); verticalSlide.setAttribute( 'aria-hidden', 'true' ); } } ); } ); } /** * Sorts and formats all of fragments in the * presentation. */ function sortAllFragments() { var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); horizontalSlides.forEach( function( horizontalSlide ) { var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); verticalSlides.forEach( function( verticalSlide, y ) { sortFragments( verticalSlide.querySelectorAll( '.fragment' ) ); } ); if( verticalSlides.length === 0 ) sortFragments( horizontalSlide.querySelectorAll( '.fragment' ) ); } ); } /** * Updates one dimension of slides by showing the slide * with the specified index. * * @param {String} selector A CSS selector that will fetch * the group of slides we are working with * @param {Number} index The index of the slide that should be * shown * * @return {Number} The index of the slide that is now shown, * might differ from the passed in index if it was out of * bounds. */ function updateSlides( selector, index ) { // Select all slides and convert the NodeList result to // an array var slides = toArray( dom.wrapper.querySelectorAll( selector ) ), slidesLength = slides.length; var printMode = isPrintingPDF(); if( slidesLength ) { // Should the index loop? if( config.loop ) { index %= slidesLength; if( index < 0 ) { index = slidesLength + index; } } // Enforce max and minimum index bounds index = Math.max( Math.min( index, slidesLength - 1 ), 0 ); for( var i = 0; i < slidesLength; i++ ) { var element = slides[i]; var reverse = config.rtl && !isVerticalSlide( element ); element.classList.remove( 'past' ); element.classList.remove( 'present' ); element.classList.remove( 'future' ); // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute element.setAttribute( 'hidden', '' ); element.setAttribute( 'aria-hidden', 'true' ); // If this element contains vertical slides if( element.querySelector( 'section' ) ) { element.classList.add( 'stack' ); } // If we're printing static slides, all slides are "present" if( printMode ) { element.classList.add( 'present' ); continue; } if( i < index ) { // Any element previous to index is given the 'past' class element.classList.add( reverse ? 'future' : 'past' ); if( config.fragments ) { var pastFragments = toArray( element.querySelectorAll( '.fragment' ) ); // Show all fragments on prior slides while( pastFragments.length ) { var pastFragment = pastFragments.pop(); pastFragment.classList.add( 'visible' ); pastFragment.classList.remove( 'current-fragment' ); } } } else if( i > index ) { // Any element subsequent to index is given the 'future' class element.classList.add( reverse ? 'past' : 'future' ); if( config.fragments ) { var futureFragments = toArray( element.querySelectorAll( '.fragment.visible' ) ); // No fragments in future slides should be visible ahead of time while( futureFragments.length ) { var futureFragment = futureFragments.pop(); futureFragment.classList.remove( 'visible' ); futureFragment.classList.remove( 'current-fragment' ); } } } } // Mark the current slide as present slides[index].classList.add( 'present' ); slides[index].removeAttribute( 'hidden' ); slides[index].removeAttribute( 'aria-hidden' ); // If this slide has a state associated with it, add it // onto the current state of the deck var slideState = slides[index].getAttribute( 'data-state' ); if( slideState ) { state = state.concat( slideState.split( ' ' ) ); } } else { // Since there are no slides we can't be anywhere beyond the // zeroth index index = 0; } return index; } /** * Optimization method; hide all slides that are far away * from the present slide. */ function updateSlidesVisibility() { // Select all slides and convert the NodeList result to // an array var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ), horizontalSlidesLength = horizontalSlides.length, distanceX, distanceY; if( horizontalSlidesLength && typeof indexh !== 'undefined' ) { // The number of steps away from the present slide that will // be visible var viewDistance = isOverview() ? 10 : config.viewDistance; // Limit view distance on weaker devices if( isMobileDevice ) { viewDistance = isOverview() ? 6 : 2; } // All slides need to be visible when exporting to PDF if( isPrintingPDF() ) { viewDistance = Number.MAX_VALUE; } for( var x = 0; x < horizontalSlidesLength; x++ ) { var horizontalSlide = horizontalSlides[x]; var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ), verticalSlidesLength = verticalSlides.length; // Determine how far away this slide is from the present distanceX = Math.abs( ( indexh || 0 ) - x ) || 0; // If the presentation is looped, distance should measure // 1 between the first and last slides if( config.loop ) { distanceX = Math.abs( ( ( indexh || 0 ) - x ) % ( horizontalSlidesLength - viewDistance ) ) || 0; } // Show the horizontal slide if it's within the view distance if( distanceX < viewDistance ) { showSlide( horizontalSlide ); } else { hideSlide( horizontalSlide ); } if( verticalSlidesLength ) { var oy = getPreviousVerticalIndex( horizontalSlide ); for( var y = 0; y < verticalSlidesLength; y++ ) { var verticalSlide = verticalSlides[y]; distanceY = x === ( indexh || 0 ) ? Math.abs( ( indexv || 0 ) - y ) : Math.abs( y - oy ); if( distanceX + distanceY < viewDistance ) { showSlide( verticalSlide ); } else { hideSlide( verticalSlide ); } } } } } } /** * Pick up notes from the current slide and display tham * to the viewer. * * @see `showNotes` config value */ function updateNotes() { if( config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF() ) { dom.speakerNotes.innerHTML = getSlideNotes() || ''; } } /** * Updates the progress bar to reflect the current slide. */ function updateProgress() { // Update progress if enabled if( config.progress && dom.progressbar ) { dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px'; } } /** * Updates the slide number div to reflect the current slide. * * The following slide number formats are available: * "h.v": horizontal . vertical slide number (default) * "h/v": horizontal / vertical slide number * "c": flattened slide number * "c/t": flattened slide number / total slides */ function updateSlideNumber() { // Update slide number if enabled if( config.slideNumber && dom.slideNumber ) { var value = []; var format = 'h.v'; // Check if a custom number format is available if( typeof config.slideNumber === 'string' ) { format = config.slideNumber; } switch( format ) { case 'c': value.push( getSlidePastCount() + 1 ); break; case 'c/t': value.push( getSlidePastCount() + 1, '/', getTotalSlides() ); break; case 'h/v': value.push( indexh + 1 ); if( isVerticalSlide() ) value.push( '/', indexv + 1 ); break; default: value.push( indexh + 1 ); if( isVerticalSlide() ) value.push( '.', indexv + 1 ); } dom.slideNumber.innerHTML = formatSlideNumber( value[0], value[1], value[2] ); } } /** * Applies HTML formatting to a slide number before it's * written to the DOM. */ function formatSlideNumber( a, delimiter, b ) { if( typeof b === 'number' && !isNaN( b ) ) { return ''+ a +'' + ''+ delimiter +'' + ''+ b +''; } else { return ''+ a +''; } } /** * Updates the state of all control/navigation arrows. */ function updateControls() { var routes = availableRoutes(); var fragments = availableFragments(); // Remove the 'enabled' class from all directions dom.controlsLeft.concat( dom.controlsRight ) .concat( dom.controlsUp ) .concat( dom.controlsDown ) .concat( dom.controlsPrev ) .concat( dom.controlsNext ).forEach( function( node ) { node.classList.remove( 'enabled' ); node.classList.remove( 'fragmented' ); } ); // Add the 'enabled' class to the available routes if( routes.left ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'enabled' ); } ); if( routes.right ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'enabled' ); } ); if( routes.up ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'enabled' ); } ); if( routes.down ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'enabled' ); } ); // Prev/next buttons if( routes.left || routes.up ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'enabled' ); } ); if( routes.right || routes.down ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'enabled' ); } ); // Highlight fragment directions if( currentSlide ) { // Always apply fragment decorator to prev/next buttons if( fragments.prev ) dom.controlsPrev.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); if( fragments.next ) dom.controlsNext.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); // Apply fragment decorators to directional buttons based on // what slide axis they are in if( isVerticalSlide( currentSlide ) ) { if( fragments.prev ) dom.controlsUp.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); if( fragments.next ) dom.controlsDown.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); } else { if( fragments.prev ) dom.controlsLeft.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); if( fragments.next ) dom.controlsRight.forEach( function( el ) { el.classList.add( 'fragmented', 'enabled' ); } ); } } } /** * Updates the background elements to reflect the current * slide. * * @param {Boolean} includeAll If true, the backgrounds of * all vertical slides (not just the present) will be updated. */ function updateBackground( includeAll ) { var currentBackground = null; // Reverse past/future classes when in RTL mode var horizontalPast = config.rtl ? 'future' : 'past', horizontalFuture = config.rtl ? 'past' : 'future'; // Update the classes of all backgrounds to match the // states of their slides (past/present/future) toArray( dom.background.childNodes ).forEach( function( backgroundh, h ) { backgroundh.classList.remove( 'past' ); backgroundh.classList.remove( 'present' ); backgroundh.classList.remove( 'future' ); if( h < indexh ) { backgroundh.classList.add( horizontalPast ); } else if ( h > indexh ) { backgroundh.classList.add( horizontalFuture ); } else { backgroundh.classList.add( 'present' ); // Store a reference to the current background element currentBackground = backgroundh; } if( includeAll || h === indexh ) { toArray( backgroundh.querySelectorAll( '.slide-background' ) ).forEach( function( backgroundv, v ) { backgroundv.classList.remove( 'past' ); backgroundv.classList.remove( 'present' ); backgroundv.classList.remove( 'future' ); if( v < indexv ) { backgroundv.classList.add( 'past' ); } else if ( v > indexv ) { backgroundv.classList.add( 'future' ); } else { backgroundv.classList.add( 'present' ); // Only if this is the present horizontal and vertical slide if( h === indexh ) currentBackground = backgroundv; } } ); } } ); // Stop any currently playing video background if( previousBackground ) { var previousVideo = previousBackground.querySelector( 'video' ); if( previousVideo ) previousVideo.pause(); } if( currentBackground ) { // Start video playback var currentVideo = currentBackground.querySelector( 'video' ); if( currentVideo ) { if( currentVideo.currentTime > 0 ) currentVideo.currentTime = 0; currentVideo.play(); } var backgroundImageURL = currentBackground.style.backgroundImage || ''; // Restart GIFs (doesn't work in Firefox) if( /\.gif/i.test( backgroundImageURL ) ) { currentBackground.style.backgroundImage = ''; window.getComputedStyle( currentBackground ).opacity; currentBackground.style.backgroundImage = backgroundImageURL; } // Don't transition between identical backgrounds. This // prevents unwanted flicker. var previousBackgroundHash = previousBackground ? previousBackground.getAttribute( 'data-background-hash' ) : null; var currentBackgroundHash = currentBackground.getAttribute( 'data-background-hash' ); if( currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground ) { dom.background.classList.add( 'no-transition' ); } previousBackground = currentBackground; } // If there's a background brightness flag for this slide, // bubble it to the .reveal container if( currentSlide ) { [ 'has-light-background', 'has-dark-background' ].forEach( function( classToBubble ) { if( currentSlide.classList.contains( classToBubble ) ) { dom.wrapper.classList.add( classToBubble ); } else { dom.wrapper.classList.remove( classToBubble ); } } ); } // Allow the first background to apply without transition setTimeout( function() { dom.background.classList.remove( 'no-transition' ); }, 1 ); } /** * Updates the position of the parallax background based * on the current slide index. */ function updateParallax() { if( config.parallaxBackgroundImage ) { var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); var backgroundSize = dom.background.style.backgroundSize.split( ' ' ), backgroundWidth, backgroundHeight; if( backgroundSize.length === 1 ) { backgroundWidth = backgroundHeight = parseInt( backgroundSize[0], 10 ); } else { backgroundWidth = parseInt( backgroundSize[0], 10 ); backgroundHeight = parseInt( backgroundSize[1], 10 ); } var slideWidth = dom.background.offsetWidth, horizontalSlideCount = horizontalSlides.length, horizontalOffsetMultiplier, horizontalOffset; if( typeof config.parallaxBackgroundHorizontal === 'number' ) { horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal; } else { horizontalOffsetMultiplier = ( backgroundWidth - slideWidth ) / ( horizontalSlideCount-1 ); } horizontalOffset = horizontalOffsetMultiplier * indexh * -1; var slideHeight = dom.background.offsetHeight, verticalSlideCount = verticalSlides.length, verticalOffsetMultiplier, verticalOffset; if( typeof config.parallaxBackgroundVertical === 'number' ) { verticalOffsetMultiplier = config.parallaxBackgroundVertical; } else { verticalOffsetMultiplier = ( backgroundHeight - slideHeight ) / ( verticalSlideCount-1 ); } verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv * 1 : 0; dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px'; } } /** * Called when the given slide is within the configured view * distance. Shows the slide element and loads any content * that is set to load lazily (data-src). */ function showSlide( slide ) { // Show the slide element slide.style.display = 'block'; // Media elements with data-src attributes toArray( slide.querySelectorAll( 'img[data-src], video[data-src], audio[data-src]' ) ).forEach( function( element ) { element.setAttribute( 'src', element.getAttribute( 'data-src' ) ); element.removeAttribute( 'data-src' ); } ); // Media elements with children toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( media ) { var sources = 0; toArray( media.querySelectorAll( 'source[data-src]' ) ).forEach( function( source ) { source.setAttribute( 'src', source.getAttribute( 'data-src' ) ); source.removeAttribute( 'data-src' ); sources += 1; } ); // If we rewrote sources for this video/audio element, we need // to manually tell it to load from its new origin if( sources > 0 ) { media.load(); } } ); // Show the corresponding background element var indices = getIndices( slide ); var background = getSlideBackground( indices.h, indices.v ); if( background ) { background.style.display = 'block'; // If the background contains media, load it if( background.hasAttribute( 'data-loaded' ) === false ) { background.setAttribute( 'data-loaded', 'true' ); var backgroundImage = slide.getAttribute( 'data-background-image' ), backgroundVideo = slide.getAttribute( 'data-background-video' ), backgroundVideoLoop = slide.hasAttribute( 'data-background-video-loop' ), backgroundIframe = slide.getAttribute( 'data-background-iframe' ); // Images if( backgroundImage ) { background.style.backgroundImage = 'url('+ backgroundImage +')'; } // Videos else if ( backgroundVideo && !isSpeakerNotes() ) { var video = document.createElement( 'video' ); if( backgroundVideoLoop ) { video.setAttribute( 'loop', '' ); } // Support comma separated lists of video sources backgroundVideo.split( ',' ).forEach( function( source ) { video.innerHTML += ''; } ); background.appendChild( video ); } // Iframes else if( backgroundIframe ) { var iframe = document.createElement( 'iframe' ); iframe.setAttribute( 'src', backgroundIframe ); iframe.style.width = '100%'; iframe.style.height = '100%'; iframe.style.maxHeight = '100%'; iframe.style.maxWidth = '100%'; background.appendChild( iframe ); } } } } /** * Called when the given slide is moved outside of the * configured view distance. */ function hideSlide( slide ) { // Hide the slide element slide.style.display = 'none'; // Hide the corresponding background element var indices = getIndices( slide ); var background = getSlideBackground( indices.h, indices.v ); if( background ) { background.style.display = 'none'; } } /** * Determine what available routes there are for navigation. * * @return {Object} containing four booleans: left/right/up/down */ function availableRoutes() { var horizontalSlides = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ), verticalSlides = dom.wrapper.querySelectorAll( VERTICAL_SLIDES_SELECTOR ); var routes = { left: indexh > 0 || config.loop, right: indexh < horizontalSlides.length - 1 || config.loop, up: indexv > 0, down: indexv < verticalSlides.length - 1 }; // reverse horizontal controls for rtl if( config.rtl ) { var left = routes.left; routes.left = routes.right; routes.right = left; } return routes; } /** * Returns an object describing the available fragment * directions. * * @return {Object} two boolean properties: prev/next */ function availableFragments() { if( currentSlide && config.fragments ) { var fragments = currentSlide.querySelectorAll( '.fragment' ); var hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' ); return { prev: fragments.length - hiddenFragments.length > 0, next: !!hiddenFragments.length }; } else { return { prev: false, next: false }; } } /** * Enforces origin-specific format rules for embedded media. */ function formatEmbeddedContent() { var _appendParamToIframeSource = function( sourceAttribute, sourceURL, param ) { toArray( dom.slides.querySelectorAll( 'iframe['+ sourceAttribute +'*="'+ sourceURL +'"]' ) ).forEach( function( el ) { var src = el.getAttribute( sourceAttribute ); if( src && src.indexOf( param ) === -1 ) { el.setAttribute( sourceAttribute, src + ( !/\?/.test( src ) ? '?' : '&' ) + param ); } }); }; // YouTube frames must include "?enablejsapi=1" _appendParamToIframeSource( 'src', 'youtube.com/embed/', 'enablejsapi=1' ); _appendParamToIframeSource( 'data-src', 'youtube.com/embed/', 'enablejsapi=1' ); // Vimeo frames must include "?api=1" _appendParamToIframeSource( 'src', 'player.vimeo.com/', 'api=1' ); _appendParamToIframeSource( 'data-src', 'player.vimeo.com/', 'api=1' ); } /** * Start playback of any embedded content inside of * the targeted slide. */ function startEmbeddedContent( slide ) { if( slide && !isSpeakerNotes() ) { // Restart GIFs toArray( slide.querySelectorAll( 'img[src$=".gif"]' ) ).forEach( function( el ) { // Setting the same unchanged source like this was confirmed // to work in Chrome, FF & Safari el.setAttribute( 'src', el.getAttribute( 'src' ) ); } ); // HTML5 media elements toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { if( el.hasAttribute( 'data-autoplay' ) && typeof el.play === 'function' ) { el.play(); } } ); // Normal iframes toArray( slide.querySelectorAll( 'iframe[src]' ) ).forEach( function( el ) { startEmbeddedIframe( { target: el } ); } ); // Lazy loading iframes toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) { if( el.getAttribute( 'src' ) !== el.getAttribute( 'data-src' ) ) { el.removeEventListener( 'load', startEmbeddedIframe ); // remove first to avoid dupes el.addEventListener( 'load', startEmbeddedIframe ); el.setAttribute( 'src', el.getAttribute( 'data-src' ) ); } } ); } } /** * "Starts" the content of an embedded iframe using the * postmessage API. */ function startEmbeddedIframe( event ) { var iframe = event.target; // YouTube postMessage API if( /youtube\.com\/embed\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) { iframe.contentWindow.postMessage( '{"event":"command","func":"playVideo","args":""}', '*' ); } // Vimeo postMessage API else if( /player\.vimeo\.com\//.test( iframe.getAttribute( 'src' ) ) && iframe.hasAttribute( 'data-autoplay' ) ) { iframe.contentWindow.postMessage( '{"method":"play"}', '*' ); } // Generic postMessage API else { iframe.contentWindow.postMessage( 'slide:start', '*' ); } } /** * Stop playback of any embedded content inside of * the targeted slide. */ function stopEmbeddedContent( slide ) { if( slide && slide.parentNode ) { // HTML5 media elements toArray( slide.querySelectorAll( 'video, audio' ) ).forEach( function( el ) { if( !el.hasAttribute( 'data-ignore' ) && typeof el.pause === 'function' ) { el.pause(); } } ); // Generic postMessage API for non-lazy loaded iframes toArray( slide.querySelectorAll( 'iframe' ) ).forEach( function( el ) { el.contentWindow.postMessage( 'slide:stop', '*' ); el.removeEventListener( 'load', startEmbeddedIframe ); }); // YouTube postMessage API toArray( slide.querySelectorAll( 'iframe[src*="youtube.com/embed/"]' ) ).forEach( function( el ) { if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) { el.contentWindow.postMessage( '{"event":"command","func":"pauseVideo","args":""}', '*' ); } }); // Vimeo postMessage API toArray( slide.querySelectorAll( 'iframe[src*="player.vimeo.com/"]' ) ).forEach( function( el ) { if( !el.hasAttribute( 'data-ignore' ) && typeof el.contentWindow.postMessage === 'function' ) { el.contentWindow.postMessage( '{"method":"pause"}', '*' ); } }); // Lazy loading iframes toArray( slide.querySelectorAll( 'iframe[data-src]' ) ).forEach( function( el ) { // Only removing the src doesn't actually unload the frame // in all browsers (Firefox) so we set it to blank first el.setAttribute( 'src', 'about:blank' ); el.removeAttribute( 'src' ); } ); } } /** * Returns the number of past slides. This can be used as a global * flattened index for slides. */ function getSlidePastCount() { var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); // The number of past slides var pastCount = 0; // Step through all slides and count the past ones mainLoop: for( var i = 0; i < horizontalSlides.length; i++ ) { var horizontalSlide = horizontalSlides[i]; var verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) ); for( var j = 0; j < verticalSlides.length; j++ ) { // Stop as soon as we arrive at the present if( verticalSlides[j].classList.contains( 'present' ) ) { break mainLoop; } pastCount++; } // Stop as soon as we arrive at the present if( horizontalSlide.classList.contains( 'present' ) ) { break; } // Don't count the wrapping section for vertical slides if( horizontalSlide.classList.contains( 'stack' ) === false ) { pastCount++; } } return pastCount; } /** * Returns a value ranging from 0-1 that represents * how far into the presentation we have navigated. */ function getProgress() { // The number of past and total slides var totalCount = getTotalSlides(); var pastCount = getSlidePastCount(); if( currentSlide ) { var allFragments = currentSlide.querySelectorAll( '.fragment' ); // If there are fragments in the current slide those should be // accounted for in the progress. if( allFragments.length > 0 ) { var visibleFragments = currentSlide.querySelectorAll( '.fragment.visible' ); // This value represents how big a portion of the slide progress // that is made up by its fragments (0-1) var fragmentWeight = 0.9; // Add fragment progress to the past slide count pastCount += ( visibleFragments.length / allFragments.length ) * fragmentWeight; } } return pastCount / ( totalCount - 1 ); } /** * Checks if this presentation is running inside of the * speaker notes window. */ function isSpeakerNotes() { return !!window.location.search.match( /receiver/gi ); } /** * Reads the current URL (hash) and navigates accordingly. */ function readURL() { var hash = window.location.hash; // Attempt to parse the hash as either an index or name var bits = hash.slice( 2 ).split( '/' ), name = hash.replace( /#|\//gi, '' ); // If the first bit is invalid and there is a name we can // assume that this is a named link if( isNaN( parseInt( bits[0], 10 ) ) && name.length ) { var element; // Ensure the named link is a valid HTML ID attribute if( /^[a-zA-Z][\w:.-]*$/.test( name ) ) { // Find the slide with the specified ID element = document.getElementById( name ); } if( element ) { // Find the position of the named slide and navigate to it var indices = Reveal.getIndices( element ); slide( indices.h, indices.v ); } // If the slide doesn't exist, navigate to the current slide else { slide( indexh || 0, indexv || 0 ); } } else { // Read the index components of the hash var h = parseInt( bits[0], 10 ) || 0, v = parseInt( bits[1], 10 ) || 0; if( h !== indexh || v !== indexv ) { slide( h, v ); } } } /** * Updates the page URL (hash) to reflect the current * state. * * @param {Number} delay The time in ms to wait before * writing the hash */ function writeURL( delay ) { if( config.history ) { // Make sure there's never more than one timeout running clearTimeout( writeURLTimeout ); // If a delay is specified, timeout this call if( typeof delay === 'number' ) { writeURLTimeout = setTimeout( writeURL, delay ); } else if( currentSlide ) { var url = '/'; // Attempt to create a named link based on the slide's ID var id = currentSlide.getAttribute( 'id' ); if( id ) { id = id.replace( /[^a-zA-Z0-9\-\_\:\.]/g, '' ); } // If the current slide has an ID, use that as a named link if( typeof id === 'string' && id.length ) { url = '/' + id; } // Otherwise use the /h/v index else { if( indexh > 0 || indexv > 0 ) url += indexh; if( indexv > 0 ) url += '/' + indexv; } window.location.hash = url; } } } /** * Retrieves the h/v location of the current, or specified, * slide. * * @param {HTMLElement} slide If specified, the returned * index will be for this slide rather than the currently * active one * * @return {Object} { h: , v: , f: } */ function getIndices( slide ) { // By default, return the current indices var h = indexh, v = indexv, f; // If a slide is specified, return the indices of that slide if( slide ) { var isVertical = isVerticalSlide( slide ); var slideh = isVertical ? slide.parentNode : slide; // Select all horizontal slides var horizontalSlides = toArray( dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ) ); // Now that we know which the horizontal slide is, get its index h = Math.max( horizontalSlides.indexOf( slideh ), 0 ); // Assume we're not vertical v = undefined; // If this is a vertical slide, grab the vertical index if( isVertical ) { v = Math.max( toArray( slide.parentNode.querySelectorAll( 'section' ) ).indexOf( slide ), 0 ); } } if( !slide && currentSlide ) { var hasFragments = currentSlide.querySelectorAll( '.fragment' ).length > 0; if( hasFragments ) { var currentFragment = currentSlide.querySelector( '.current-fragment' ); if( currentFragment && currentFragment.hasAttribute( 'data-fragment-index' ) ) { f = parseInt( currentFragment.getAttribute( 'data-fragment-index' ), 10 ); } else { f = currentSlide.querySelectorAll( '.fragment.visible' ).length - 1; } } } return { h: h, v: v, f: f }; } /** * Retrieves the total number of slides in this presentation. */ function getTotalSlides() { return dom.wrapper.querySelectorAll( SLIDES_SELECTOR + ':not(.stack)' ).length; } /** * Returns the slide element matching the specified index. */ function getSlide( x, y ) { var horizontalSlide = dom.wrapper.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR )[ x ]; var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll( 'section' ); if( verticalSlides && verticalSlides.length && typeof y === 'number' ) { return verticalSlides ? verticalSlides[ y ] : undefined; } return horizontalSlide; } /** * Returns the background element for the given slide. * All slides, even the ones with no background properties * defined, have a background element so as long as the * index is valid an element will be returned. */ function getSlideBackground( x, y ) { // When printing to PDF the slide backgrounds are nested // inside of the slides if( isPrintingPDF() ) { var slide = getSlide( x, y ); if( slide ) { var background = slide.querySelector( '.slide-background' ); if( background && background.parentNode === slide ) { return background; } } return undefined; } var horizontalBackground = dom.wrapper.querySelectorAll( '.backgrounds>.slide-background' )[ x ]; var verticalBackgrounds = horizontalBackground && horizontalBackground.querySelectorAll( '.slide-background' ); if( verticalBackgrounds && verticalBackgrounds.length && typeof y === 'number' ) { return verticalBackgrounds ? verticalBackgrounds[ y ] : undefined; } return horizontalBackground; } /** * Retrieves the speaker notes from a slide. Notes can be * defined in two ways: * 1. As a data-notes attribute on the slide
* 2. As an