<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Types - Warpgrapher Book</title>
<!-- Custom HTML head -->
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded "><a href="../warpgrapher/intro.html"><strong aria-hidden="true">1.</strong> Warpgrapher</a></li><li class="chapter-item expanded "><a href="../warpgrapher/quickstart.html"><strong aria-hidden="true">2.</strong> Quickstart</a></li><li class="chapter-item expanded "><a href="../integrations/intro.html"><strong aria-hidden="true">3.</strong> Server Integration</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../integrations/actix.html"><strong aria-hidden="true">3.1.</strong> Actix Web</a></li><li class="chapter-item expanded "><a href="../integrations/lambda.html"><strong aria-hidden="true">3.2.</strong> AWS Lambda</a></li></ol></li><li class="chapter-item expanded "><a href="../configuration/intro.html"><strong aria-hidden="true">4.</strong> Configuration</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../configuration/databases.html"><strong aria-hidden="true">4.1.</strong> Databases</a></li><li class="chapter-item expanded "><a href="../configuration/formats.html"><strong aria-hidden="true">4.2.</strong> Formats</a></li><li class="chapter-item expanded "><a href="../configuration/types.html" class="active"><strong aria-hidden="true">4.3.</strong> Types</a></li><li class="chapter-item expanded "><a href="../configuration/relationships.html"><strong aria-hidden="true">4.4.</strong> Relationships</a></li></ol></li><li class="chapter-item expanded "><a href="../api/intro.html"><strong aria-hidden="true">5.</strong> CRUD API Usage Examples</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../api/node_create.html"><strong aria-hidden="true">5.1.</strong> Node Create</a></li><li class="chapter-item expanded "><a href="../api/node_read.html"><strong aria-hidden="true">5.2.</strong> Node Read</a></li><li class="chapter-item expanded "><a href="../api/node_update.html"><strong aria-hidden="true">5.3.</strong> Node Update</a></li><li class="chapter-item expanded "><a href="../api/node_delete.html"><strong aria-hidden="true">5.4.</strong> Node Delete</a></li><li class="chapter-item expanded "><a href="../api/rel_create.html"><strong aria-hidden="true">5.5.</strong> Relationship Create</a></li><li class="chapter-item expanded "><a href="../api/rel_read.html"><strong aria-hidden="true">5.6.</strong> Relationship Read</a></li><li class="chapter-item expanded "><a href="../api/rel_update.html"><strong aria-hidden="true">5.7.</strong> Relationship Update</a></li><li class="chapter-item expanded "><a href="../api/rel_delete.html"><strong aria-hidden="true">5.8.</strong> Relationship Delete</a></li></ol></li><li class="chapter-item expanded "><a href="../engine/intro.html"><strong aria-hidden="true">6.</strong> Engine Features</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../engine/endpoints_static.html"><strong aria-hidden="true">6.1.</strong> Static Endpoints</a></li><li class="chapter-item expanded "><a href="../engine/endpoints_defined.html"><strong aria-hidden="true">6.2.</strong> Defined Endpoints</a></li><li class="chapter-item expanded "><a href="../engine/dynamic_props.html"><strong aria-hidden="true">6.3.</strong> Dynamic Props</a></li><li class="chapter-item expanded "><a href="../engine/dynamic_rels.html"><strong aria-hidden="true">6.4.</strong> Dynamic Relationships</a></li><li class="chapter-item expanded "><a href="../engine/context_request.html"><strong aria-hidden="true">6.5.</strong> Request Context</a></li><li class="chapter-item expanded "><a href="../engine/input_validation.html"><strong aria-hidden="true">6.6.</strong> Input Validation</a></li><li class="chapter-item expanded "><a href="../engine/event_handlers.html"><strong aria-hidden="true">6.7.</strong> Event Handlers</a></li></ol></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Warpgrapher Book</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="types"><a class="header" href="#types">Types</a></h1>
<p>The <a href="../warpgrapher/quickstart.html">Quickstart</a> presented a first example of a Warpgrapher configuration, shown again here.</p>
<pre><code class="language-rust no_run noplayground">static CONFIG: &str = "
version: 1
model:
- name: User
props:
- name: email
type: String
required: false
";
</code></pre>
<h2 id="type-configuration"><a class="header" href="#type-configuration">Type Configuration</a></h2>
<p>Recall that the <code>version</code> value is used to indicate the configuration file format version to be used. Right now, the only valid value is 1. The next element in the configuration a a data model. The <code>model</code> object is a list of types. The example shown in the <a href="../warpgrapher/quickstart.html">Quickstart</a> uses many defaults for simplicity. The definition below shows the full range of options for property definitions. Don't worry about relationships between types for the moment. Those are covered in the <a href="./relationships.html">next section</a>.</p>
<pre><code class="language-yaml">model:
- name: String
props:
- name: String
uses:
create: Boolean
query: Boolean
update: Boolean
output: Boolean
type: String # Boolean | Float | ID | Int | String
required: Boolean
list: Boolean
resolver: String
validator: String
endpoints:
read: Boolean
create: Boolean
update: Boolean
delete: Boolean
</code></pre>
<p>Right under the model object is a list of types. The first attribute describing a type is a name. In the example from the <a href="../warpgrapher/quickstart.html">Quickstart</a>, the name of the type is <code>User</code>.</p>
<p>The second attribute describing a type is <code>props</code>. The <code>props</code> attribute is a list of properties that are stored on nodes of that type. Each property is described the several configuration attributes, as follows.</p>
<p>The <code>name</code> attribute is a string that identifies the property. It must be unique within the scope of the type. In the <a href="../warpgrapher/quickstart.html">Quickstart</a> example, the sole property on the User type is named email.</p>
<p>The <code>uses</code> attribute is an object that contains four fields within it, <code>create</code>, <code>query</code>, <code>update</code>, and <code>output</code>, each a boolean value. The fields within the <code>uses</code> attribute control whether the property is present in various parts of the GraphQL schema. If the <code>create</code> attribute is true, then the property will be included in the GraphQL input for creation operations. If false, the property will be omitted from creation operations. If the <code>query</code> attribute is true, the property will be included in the GraphQL schema for search query input. If false, the property will be omitted from search query operations. If the <code>update</code> attribute is true, the property will be included in the GraphQL schema input for updating existing nodes. If false, the property will be omitted from the update schema. Lastly, if the <code>output</code> attribute is true, the property will be included in the GraphQL schema for nodes returned to the client. If false, the property will be omitted from the output.</p>
<p>By default, all <code>uses</code> boolean attributes are true, meaning that the property is included in all relevant areas of the GraphQL schema. Selectively setting some of the <code>uses</code> attributes handles uses cases where a property should not be available for some operations. For example, one might set the <code>create</code> attribute to false if a property is a calculated value that should never be set directly. One might set <code>update</code> to false to make an attribute immutable -- for example, the <code>email</code> property of the <code>User</code> type might have <code>update</code> set to false if GraphQL clients should not be able to tamper with the identities of users. One might set <code>output</code> to false for properties that should never be read through the GraphQL interface, such as for keeping people from reading out a password property.</p>
<p>The <code>type</code> attribute of the property definition is a String value that must take on a value of <code>Boolean</code>, <code>Float</code>, <code>ID</code>, <code>Int</code>, or <code>String</code>, defining type of the property.</p>
<p>If the <code>required</code> attribute of the property definition is false, the property is not required (it is optional). By default this attribute is true, which means it must be provided when nodes of this type are created (unless hidden from the <code>create</code> use) and it must be present (non-null) when retrieving the node from Warpgrapher (again, unless hidden from the <code>output</code> use).</p>
<p>If the <code>list</code> attribute of the property definition is true, the property is a list of scalar values of <code>type</code>. If <code>list</code> is false, the property is only a single value of that scalar type.</p>
<p>The <code>resolver</code> attribute is a text key that is used to identify a custom-written resolver function. Warpgrapher allows applications to define custom resolvers that do more or different things than the default CRUD operations automatically provided by Warpgrapher itself. For example, a custom resolver might dynamically calculate a value, such as a total or average, rather than just returning a value from the database. Custom resolvers for <a href="../engine/dynamic_props.html">dynamic properties</a> are covered in greater detail later in the book.</p>
<p>The <code>validator</code> attribute is a text key that is used to identify a fuction that validates an input. For example, a validation function might check an email against and email validation regex. <a href="../engine/validators.html">Validation functions</a> are covered in greater detail later in the book.</p>
<p>Note that the <code>endpoints</code> attribute is on the <code>type</code> definition, not the <code>property</code> definition, as indicated by the indentation in the YAML example above. The <code>endpoints</code> attribute is somewhat similar to the <code>uses</code> boolean, but at the level of the whole type rather than a single property. If the <code>read</code> attribute is true, Warpgrapher will generate a query in the GraphQL schema so that node of this type can be retrieved. If false, no query will be generated. If the <code>create</code> attribute is true, Warpgrapher will generate a node creation mutation in the GraphQL schema. If false, no creation mutation will be generated. If the <code>update</code> attribute is true, Warpgrapher will generate a node update mutation in the GraphQL schema. If false, no update mutation will be generated. Lastly, if the <code>delete</code> attribute is true, Warpgrapher will generate a node deletion mutation in the GraphQL schema. If false, no delete mutation will be generated.</p>
<h2 id="generated-schema"><a class="header" href="#generated-schema">Generated Schema</a></h2>
<p>Warpgrapher uses the configuration described above to automatically generate a GraphQL schema and default resolver to create, read, update, and delete nodes of the types defined in the configuration's model section. The remainder of this section walks through the contents of the schema in detail.</p>
<p>The top level GraphQL Query has two queries within it, as shown below. The <code>_version</code> query returns a scalar <code>String</code> with the version of the GraphQL service. The value returned is set with the <a href="https://docs.rs/warpgrapher/latest/warpgrapher/engine/struct.EngineBuilder.html#method.with_version">with_version</a> method on the <code>EngineBuilder</code>.</p>
<pre><code>type Query {
User(input: UserQueryInput, options: UserOptions): [User!]
_version: String
}
</code></pre>
<p>The <code>User</code> query, above, is generated by Warpgrapher for the retrieval of User nodes. The query takes two parameters, an <code>input</code> parameter that provides any search parameters that narrow down the set of Users to be retrieved, and an <code>options</code> object. The query returns a <code>User</code> type.</p>
<p>The <code>UserQueryInput</code>, defined in the schema snippet below, is use to provide search parameters to identify the <code>User</code> nodes to return to the client. The <code>User</code> node configuration had only one property, <code>email</code>. Warpgrapher automatically adds an <code>id</code> property that contains a unique identifier for nodes. In the GraphQL schema, the id is always represented as a string. However, in some Gremlin back-ends, the id may be required to be an integer, in which case the id field in the GraphQL schema will be a String that can be successfully parsed into an integer value. </p>
<pre><code>input UserQueryInput {
email: StringQueryInput
id: StringQueryInput
}
</code></pre>
<p>Note that the types of both <code>email</code> and <code>id</code> are <code>StringQueryInput</code>, not a simple <code>String</code> scalar. This is because the query input allows for more than just an exact match.</p>
<pre><code>input StringQueryInput {
CONTAINS: String
EQ: String
GT: String
GTE: String
IN: [String!]
LT: String
LTE: String
NOTCONTAINS: String
NOTEQ: String
NOTIN: [String!]
}
</code></pre>
<p>The <code>StringQueryInput</code> has various options for matching a String more flexibly than an exact match. The <code>CONTAINS</code> operator looks for the associated String value anywhere in the target property (e.g. the <code>email</code> or <code>id</code> properties of a <code>User</code> node). <code>EQ</code> looks for an exact match. <code>GT</code> and <code>GTE</code> are greater-than and great-than-or-equals, which are useful for searching for ranges based on alphabetization, as do <code>LT</code> and <code>LTE</code>. The <code>IN</code> operators allows for searching for any string that is within a given set of Strings. <code>NOTCONTAINS</code> is the opposite of <code>CONTAINS</code>, looking for property values that do not contain the provided String. <code>NOTEQ</code> looks for non-matching Strings. And finally, <code>NOTIN</code> matches property values that do not appear in the provided set of Strings.</p>
<p>The <code>options</code> argument, described back above as an argument for the <code>User</code> query as a whole, is of type <code>UserOptions</code>. The <code>UserOptions</code> type has a single property, called <code>sort</code>, which is a list of zero or more <code>UserSort</code> objects. Each <code>UserSort</code> object has two enumeration properties, <code>direction</code> and <code>orderBy</code>.</p>
<pre><code>type UserOptions {
sort: [UserSort!]
}
type UserSort {
direction: DirectionEnum
orderBy: UserOrderByEnum!
}
enum DirectionEnum {
ascending
descending
}
enum UserOrderByEnum {
id
email
}
</code></pre>
<p>The <code>UserOrderByEnum</code> has variant values for each of the properties (but not relationships) on a User. By including one or more values in the <code>sort</code> array provided to <code>UserOptions</code>, it is possible to sort results coming back from Warpgrapher. The <code>direction</code> property determines whether the results are returned in ascending or descending sort order. The <code>orderBy</code> field determines on which property the results are sorted. If the <code>sort</code> array contains more than one value, then resorts groups of results with the same first sort key are further sorted by the second key, and so on. For example, a <code>sort</code> array might have entries for <code>joinDate</code> and then <code>name</code> to sort first by the date someone joined, and alphabetically for all people who joined on the same date.</p>
<p>The results of the query are returned in a <code>User</code> type, shown below.</p>
<p>type User {
email: String
id: ID!
}</p>
<pre><code>
The `User` type is the definition of the output type for the `User` GraphQL query. The names are the same, but these are two distinct things in the GraphQL schema -- the `User` query returns an array of zero or more `User` types. The `User` type is two fields, and `id` and an `email`. The id is a unique identifier for that node, which may be an integer or a UUID, depending on the graph database used. The `email` string is the single property that was defined on the example schema.
</code></pre>
<p>type Mutation {
UserCreate(input: UserCreateMutationInput!, options: UserOptions): User
UserDelete(input: UserDeleteInput!, options: UserOptions): Int
UserUpdate(input: UserUpdateInput!, options: UserOptions): [User!]
}</p>
<pre><code>
In addition to providing queries to retrieve existing nodes, Warpgrapher also automatically generates GraphQL schema elements and resolvers for create, update, and delete operations. The schema snippet above shows the mutations that are generated for the `User` node in the example configuration. All three of the mutations take an `options` argument, which was described in the section on queries, above. Additionally, all three mutations take an `input` value, that provides the information necessary to complete the create, update, or delete operation, respectively. Creation operations return the created node. Update operations return all the nodes that were matched and updated. Lastly, the delete operation returns the number of nodes that were deleted. The input arguments are detailed below.
</code></pre>
<p>input UserCreateMutationInput {
email: String
id: ID
}</p>
<pre><code>
The `UserCreateMutationInput` mutation input includes the email property defined in the example configuration. It also includes an `id` property. Note that the `id` property is optional. If not provided by the client, it will be set to a unique identifier by the Warpgrapher server. The reason that clients are permitted to set the `id` when creating nodes is to allow for offline mode support, which may require the creation of identifiers within local caches that should remain the same after synchronization with the server.
</code></pre>
<p>input UserDeleteInput {
DELETE: UserDeleteMutationInput
MATCH: UserQueryInput
}</p>
<p>input UserDeleteMutationInput</p>
<pre><code>
The `UserDeleteInput` input is used to identify which nodes to delete. Note that the `MATCH` part of the argument is the very same `UserQueryInput` type used in the `User` query schema element above. So searching for which nodes to delete is the same input format used to search for nodes to return in a read query. The `UserDeleteMutationInput` is empty right now, and may be omitted. It will become relevant later, in the discussion on relationships between nodes.
</code></pre>
<p>input UserUpdateInput {
MATCH: UserQueryInput
SET: UserUpdateMutationInput
}</p>
<p>input UserUpdateMutationInput {
email: String
}</p>
<pre><code>
Lastly, the `UserUpdateInput` input is provided to the udpate mutation in order to select the nodes that need to be updated and describe the update to be applied. The `MATCH` attribute is used to identify what nodes require the update. Note that the type of the `MATCH` attribute is `UserQueryInput`, which is the same type used for searching for nodes in the GraphQL query above. The `SET` attribute is used to provide the new values to which the matching nodes should be set. In this example, it is a single String value for the `email` of the `User`. Note that `id`s are set only at creation. They cannot be updated later.
## Full Schema Listing
The full schema, described in pieces above, is included below:
</code></pre>
<p>input UserDeleteInput {
DELETE: UserDeleteMutationInput
MATCH: UserQueryInput
}</p>
<p>input UserQueryInput {
email: StringQueryInput
id: StringQueryInput
}</p>
<p>type Mutation {
UserCreate(input: UserCreateMutationInput!, options: UserOptions): User
UserDelete(input: UserDeleteInput!, options: UserOptions): Int
UserUpdate(input: UserUpdateInput!, options: UserOptions): [User!]
}</p>
<p>input UserUpdateMutationInput {
email: String
}</p>
<p>type Subscription</p>
<p>input UserUpdateInput {
MATCH: UserQueryInput
SET: UserUpdateMutationInput
}</p>
<p>type Query {
User(input: UserQueryInput, options: UserOptions): [User!]
_version: String
}</p>
<p>input UserDeleteMutationInput</p>
<p>type User {
email: String
id: ID!
}</p>
<p>input UserCreateMutationInput {
email: String
id: ID
}</p>
<p>input StringQueryInput {
CONTAINS: String
EQ: String
GT: String
GTE: String
IN: [String!]
LT: String
LTE: String
NOTCONTAINS: String
NOTEQ: String
NOTIN: [String!]
}</p>
<pre><code></code></pre>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../configuration/formats.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../configuration/relationships.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../configuration/formats.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../configuration/relationships.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script type="text/javascript">
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="../clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="../highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="../book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
</body>
</html>