Minimal, multi-user Twitter OAuth blogging platform that can create living, powerful pages
{{#if !isAuthenticated }} | |
<p style="text-align: center"> | |
Please <a href="/auth/twitter">Sign In</a> to add Posts | |
</p> | |
{{return}} | |
{{/if}} |
```code | |
#if post == null | |
var error = `Post ${slug} does not exist` | |
else if post.CreatedBy != userName | |
var error = `You do not have permission to edit this post` | |
/if | |
'error' |> if(error) |> partial({ error }) | |
``` |
{{ var attrs = it.ownProps() }} | |
<div class="editor"> | |
<div class="editor-toolbar"> | |
<div class="btn" data-cmd="bold" title="Bold text (CTRL+B)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="italic" title="Italics (CTRL+I)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
<path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="link" title="Insert Link (CTRL+L)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
<path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="quote" title="Blockquote (CTRL+Q)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="image" title="Insert Image (CTRL+SHIFT+L)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="code" title="Insert Code (CTRL+<)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path fill="none" d="M0 0h24v24H0V0z"/> | |
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="heading" title="H2 Heading (CTRL+H)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
<path d="M9 4v3h5v12h3V7h5V4H9zm-6 8h3v7h3v-7h3V9H3v3z"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="ol" title="Numbered List (ALT+1)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="ul" title="Bulleted List (ALT+-)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/> | |
<path fill="none" d="M0 0h24v24H0V0z"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="strikethrough" title="Strike Through (ALT+S)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
<path d="M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="undo" title="undo (CTRL+Z)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
<path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="redo" title="redo (CTRL+SHIFT+Z)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
<path d="M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/> | |
</svg> | |
</div> | |
<div class="btn" data-cmd="save" title="Save (CTRL+S)"> | |
<svg width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/> | |
</svg> | |
</div> | |
</div> | |
<textarea {{ attrs |> htmlAttrs }}>{{content}}</textarea> | |
<a class="input-help" href="https://sharpscript.net//">template syntax</a> | |
{{#if error}}<div class="error">{{error}}</div>{{/if}} | |
</div> | |
<style> | |
.editor textarea { | |
padding: .5%; | |
width: 99%; | |
max-width: 99%; | |
min-width: 99%; | |
} | |
.editor-toolbar { | |
border: 1px solid #eee; | |
border-bottom: none; | |
margin: 0 -2px 0 0; | |
} | |
.editor-toolbar .btn { | |
margin: 5px; | |
padding: 2px 5px; | |
border-color: transparent; | |
background: none; | |
} | |
.editor-toolbar .btn:hover { | |
border-color: #ccc; | |
} | |
</style> | |
{{#raw}} | |
<script src="/editor.js"></script> | |
<script> | |
var editor = new Editor(document.querySelector(".editor"), { | |
save() { | |
let form = this.target.closest('form'); | |
form.querySelector('[type=submit]').click(); // form.submit() does not fire submit event | |
} | |
}) | |
</script> | |
{{/raw}} |
<div> | |
<div class="alert-error">{{error}}</div> | |
<a href="/">< home</a> | |
</div> | |
{{return}} |
<!-- | |
layout: none | |
--> | |
================================ | |
Create blog.sqlite if not exists | |
================================ | |
{{ `CREATE TABLE IF NOT EXISTS "Post" | |
( | |
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, | |
"Slug" VARCHAR(8000) NULL, | |
"Title" VARCHAR(8000) NULL, | |
"Content" VARCHAR(8000) NULL, | |
"Created" VARCHAR(8000) NOT NULL, | |
"CreatedBy" VARCHAR(8000) NOT NULL, | |
"Modified" VARCHAR(8000) NOT NULL, | |
"ModifiedBy" VARCHAR(8000) NOT NULL | |
); | |
CREATE TABLE IF NOT EXISTS "UserInfo" | |
( | |
"UserName" VARCHAR(8000) PRIMARY KEY, | |
"DisplayName" VARCHAR(8000) NULL, | |
"AvatarUrl" VARCHAR(8000) NULL, | |
"AvatarUrlLarge" VARCHAR(8000) NULL, | |
"Created" VARCHAR(8000) NOT NULL, | |
"Modified" VARCHAR(8000) NOT NULL | |
);` | |
|> dbExec | |
}} | |
{{ var postsCount = dbScalarSync(`SELECT COUNT(*) FROM Post`) }} | |
{{#if postsCount == 0 }} | |
======================================== | |
Seed with initial UserInfo and Post data | |
======================================== | |
{{ var sqlNow = `datetime(CURRENT_TIMESTAMP,'localtime')` }} | |
{{ var user = `ServiceStack` }} | |
======================== | |
Create ServiceStack User - Contains same info as if was @ServiceStack authenticated via Twitter | |
======================== | |
{{ `INSERT INTO UserInfo (UserName, DisplayName, AvatarUrl, AvatarUrlLarge, Created, Modified) | |
VALUES (@user, @user, @avatarUrl, @avatarUrlLarge, ${sqlNow}, ${sqlNow})` | |
|> dbExec({ | |
user: 'ServiceStack', | |
avatarUrl: 'https://pbs.twimg.com/profile_images/876249730078056448/JuTVEkWX_normal.jpg', | |
avatarUrlLarge: 'https://pbs.twimg.com/profile_images/876249730078056448/JuTVEkWX.jpg' | |
}) | |
}} | |
=============================================== | |
{{ var title = 'Live Document Example' }} | |
=============================================== | |
{{#raw content}}{{#markdown}} | |
All Blog posts have access to [ServiceStack Templates](https://sharpscript.net/) features which enables they to use a highly-productive, | |
easy to use [sandboxed](https://sharpscript.net/docs/sandbox) dynamic templating language which lets you easily create live documents | |
[like this one](/posts/live-document-example): | |
{{/markdown}} | |
<pre> | |
```code | |
11200 |> to => balance | |
3 |> to => projectedMonths | |
#keyvalues monthlyRevenues ':' | |
Salary: 4000 | |
App Royalties: 200 | |
/keyvalues | |
#keyvalues monthlyExpenses | |
Rent 1000 | |
Internet 50 | |
Mobile 50 | |
Food 400 | |
Misc 200 | |
/keyvalues | |
monthlyRevenues |> sum => it.Value |> to => totalRevenues | |
monthlyExpenses |> sum => it.Value |> to => totalExpenses | |
(totalRevenues - totalExpenses) |> to => totalSavings | |
``` | |
Current Balance: <b>{{ balance |> currency }}</b> | |
Monthly Revenues: | |
{{monthlyRevenues |> select: {it.Key.padRight(16)} {it.Value.currency()}\n }} | |
Total <b>{{ totalRevenues |> currency }}</b> | |
Monthly Expenses: | |
{{monthlyExpenses |> select: {it.Key.padRight(16)} {it.Value.currency()}\n }} | |
Total <b>{{ totalExpenses |> currency }}</b> | |
Monthly Savings: <b>{{ totalSavings |> currency }}</b> | |
Projected Cash Position: | |
{{projectedMonths.times() |> map => index + 1 |> map => | |
`${now.addMonths(it).dateFormat()} ${(it * totalSavings + balance).currency()}`|> joinln}} | |
</pre>{{/raw}} | |
{{ { title, content } |> addTo => initialPosts }} | |
========================================== | |
{{ var title = 'Markdown Example' }} | |
========================================== | |
{{#raw content}}{{#markdown}} | |
# Headings can start with 1-6 hashes | |
Markdown follows plain text conventions when rendering HTML. | |
So paragraphs separated by multiple lines are rendered as separate paragraphs. | |
### Use more hashes to create nested sub headings | |
> Text pre-fixed with '>' are treated as block quotes | |
Use a dash, asterisk or plus to create an ordered list: | |
- List Item | |
* List Item | |
+ List Item | |
Whilst you can use numbers for ordered lists: | |
1. Step 1 | |
2. Step 2 | |
3. Step 3 | |
Indent lines by 4 spaces to create pre-formatted code blocks in monospace font: | |
$ cd /Users/Guest | |
Follow [link to markdown block docs](https://sharpscript.net/docs/blocks#markdown) for more info. | |
{{/markdown}}{{/raw}} | |
{{ { title, content } |> addTo: initialPosts }} | |
================================================ | |
{{ var title = 'Web App Customizations' }} | |
================================================ | |
{{#raw content}}{{#markdown}} | |
## Init page | |
Just like `Global.asax.cs` can be used to run Startup logic in ASP.NET Web Applications and `Startup.cs` in .NET Core Apps, | |
you can add a `_init.html` to run logic once on Startup. | |
This is used in this Blog's [_init.html](https://github.com/sharp-apps/blog/blob/master/app/_init.html) where it will create a new | |
`blog.sqlite` database if it doesn't exist seeded with the UserInfo and Posts Tables and initial data, e.g: | |
``` | |
{{ `CREATE TABLE IF NOT EXISTS "UserInfo" | |
( | |
"UserName" VARCHAR(8000) PRIMARY KEY, | |
"DisplayName" VARCHAR(8000) NULL, | |
"AvatarUrl" VARCHAR(8000) NULL, | |
"AvatarUrlLarge" VARCHAR(8000) NULL, | |
"Created" VARCHAR(8000) NOT NULL, | |
"Modified" VARCHAR(8000) NOT NULL | |
);` | |
|> dbExec | |
}} | |
{{ var postsCount = dbScalarSync(`SELECT COUNT(*) FROM Post`) }} | |
{{#if postsCount == 0 }} | |
======================================== | |
Seed with initial UserInfo and Post data | |
======================================== | |
... | |
{{/if} | |
{{ htmlError }} | |
``` | |
The output of the `_init` page is captured in the `initout` argument which can be inspected as a template argument as done in | |
[log.html](https://github.com/sharp-apps/blog/blob/master/app/log.html): | |
``` | |
<div> | |
Output from init.html: | |
<pre>{{initout |> raw}}</pre> | |
</div> | |
``` | |
If there was an Exception with any of the SQL Statements it will be displayed in the `{{ htmlError }}` filter which can be later viewed | |
in the [/log](/log) page above. | |
## Customizable Auth Providers | |
Auth Providers can be configured in the same way | |
[Web App Plugins can be registered](https://sharpscript.net/docs/sharp-apps#registering-servicestack-plugins) by first specifying | |
you want to register the `AuthFeature` plugin with: | |
``` | |
features AuthFeature | |
``` | |
Then using `AuthFeature.AuthProviders` to specify which Auth Providers you want to have registered, e.g: | |
``` | |
AuthFeature.AuthProviders TwitterAuthProvider, GithubAuthProvider | |
``` | |
Each Auth Provider checks the Web Apps `web.settings` for its Auth Provider specific configuration it needs, e.g. to configure both | |
Twitter and GitHub Auth Providers you would populate it with your OAuth Apps details: | |
``` | |
oauth.RedirectUrl http://127.0.0.1:5000/ | |
oauth.CallbackUrl http://127.0.0.1:5000/auth/{0} | |
oauth.twitter.ConsumerKey {Twitter App Consumer Key} | |
oauth.twitter.ConsumerSecret {Twitter App Consumer Secret Key} | |
oauth.github.ClientId {GitHub Client Id} | |
oauth.github.ClientSecret {GitHub Client Secret} | |
oauth.github.Scopes {GitHub Auth Scopes} | |
``` | |
## Customizable Markdown Providers | |
By default Web Apps utilize [Markdig](https://github.com/lunet-io/markdig) implementation to render its Markdown. You can switch it back to | |
the built-in Markdown provider that ServiceStack uses with: | |
``` | |
markdownProvider MarkdownDeep | |
``` | |
## Rich Template Config Arguments | |
Any `web.settings` configs that are prefixed with `args.` are made available to Template Pages. Any arguments which start with | |
`{` or `[` are automatically converted into a JS object: | |
``` | |
args.blog { name:'blog.web-app.io', href:'/' } | |
args.tags ['technology','marketing'] | |
``` | |
Where they can be referenced as an `object` or an `array` directly: | |
``` | |
<a href="{{blog.href}}">{{blog.name}}</a> | |
{{#each tags}} <em>{{it}}</em> {{/each}} | |
``` | |
The alternative is to give each argument value a different name: | |
``` | |
args.blogName blog.web-app.io | |
args.blogHref / | |
``` | |
{{/markdown}}{{/raw}} | |
{{ { title, content } |> addTo => initialPosts }} | |
============================================ | |
{{ var title = 'Dynamic API Pages' }} | |
============================================ | |
{{#raw content}}{{#markdown}} | |
In addition to providing a productive dynamic language for generating HTML pages, Template Pages can also be used to rapidly develop Web APIs | |
which can utilize [dynamic page routing](/posts/page-based-routing) to easily create data-driven JSON APIs using optimal pretty URLs, in | |
real-time without any C# classes or compilation in sight! | |
The only difference between a Template Page that generates HTML or a Template Page that returns an API Response is that API pages use the | |
**return** filter to return a value. | |
E.g. To create a **Hello World** C# ServiceStack Service you would typically create a Request DTO, Response DTO and a Service implementation: | |
``` | |
[Route("/hello/{Name}")] | |
public class Hello : IReturn<HelloResponse> | |
{ | |
public string Name { get; set; } | |
} | |
public class HelloResponse | |
{ | |
public string Result { get; set; } | |
} | |
public class HelloService : Service | |
{ | |
public object Any(Hello request) => $"Hello, {request.Name}!"; | |
} | |
``` | |
### /hello API Page | |
> Usage: /hello/\{name} | |
An API which returns the same wire response as above can be implemented in API Pages by creating a page at | |
[/hello/_name/index.html](https://github.com/sharp-apps/blog/blob/master/app/hello/_name/index.html) | |
with the contents: | |
``` | |
{{ { result: `Hello, ${name}!` } |> return }} | |
``` | |
Which supports the same content negotiation as a ServiceStack Service where calling it in a browser will generate a | |
[human-friendly HTML Page](http://docs.servicestack.net/html5reportformat): | |
- [/hello/World](/hello/World) | |
Where as calling it with a JSON HTTP client containing `Accept: application/json` HTTP Header or with a `?format=json` query string will | |
return the API response in the JSON Format: | |
- [/hello/World?format=json](/hello/World?format=json) | |
Alternatively you can force a JSON Response by specifying it with: | |
``` | |
{{ { result: `Hello, ${name}!` } |> return({ format: 'json' }) }} | |
// Equivalent to: | |
{{ { result: `Hello, ${name}!` } |> return({ contentType: 'application/json' }) }} | |
``` | |
### /preview API Page | |
> Usage: /preview?content=\{templates} | |
The [/preview.html](https://github.com/sharp-apps/blog/blob/master/app/preview.html) page uses this to force a plain-text response with: | |
``` | |
{{ var response = content.evalTemplate({use:{plugins:'MarkdownScriptPlugin'}}) }} | |
{{ response |> return({ contentType:'text/plain' }) }} | |
``` | |
The preview API above is what provides this Blog's Live Preview feature where it will render any #Script provided in the | |
**content** Query String or HTTP Post Form Data, e.g: | |
- [/preview?content={{10|times|select:{pow(index,2)},}}](/preview?content={{10|times|select:{pow(index,2)},}}) | |
Which renders the plain text response: | |
0,1,4,9,16,25,36,49,64,81, | |
### /_user/api Page | |
> Usage: /\{user}/api | |
The [/_user/api.html](https://github.com/sharp-apps/blog/blob/master/app/_user/api.html) API page shows an example of how easy it is to | |
create data-driven APIs where you can literally return the response of an SQL query by calling the `dbSelect` filter and returning the results with: | |
``` | |
{{ `SELECT * | |
FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName | |
WHERE UserName = @user | |
ORDER BY p.Created DESC` | |
|> dbSelect({ user }) | |
|> return }} | |
``` | |
The **user** argument is populated as a result of dynamic route from the `_user` directory name which will let you view all | |
[@ServiceStack](/ServiceStack) posts with: | |
- [/ServiceStack/api](/ServiceStack/api) | |
Which also benefits from ServiceStack's multiple formats where the same API can be returned in: | |
- [/ServiceStack/api?format=json](/ServiceStack/api?format=json) | |
- [/ServiceStack/api?format=csv](/ServiceStack/api?format=csv) | |
- [/ServiceStack/api?format=xml](/ServiceStack/api?format=xml) | |
- [/ServiceStack/api?format=jsv](/ServiceStack/api?format=jsv) | |
Which thanks to the live development workflow provides the most productive development experience to rapidly develop Web APIs or perform common | |
tasks like viewing adhoc SQL queries in Excel which can be further manipulated using the | |
[LINQ-like expressiveness](https://sharpscript.net/linq/restriction-operators) and wrist-friendly filters available in #Script. | |
### /posts/_slug/api Page | |
> Usage: /posts/\{slug}/api | |
The [/posts/_slug/api.html](https://github.com/sharp-apps/blog/blob/master/app/posts/_slug/api.html) page shows an example of using the | |
`httpResult` filter to return a custom HTTP Response where if the post with the specified slug does not exist it will return a | |
`404 Post was not found` HTTP Response: | |
``` | |
{{ `SELECT * | |
FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName | |
WHERE Slug = @slug | |
ORDER BY p.Created DESC` | |
|> dbSingle({ slug }) | |
|> to => post | |
}} | |
{{ post ?? httpResult({ status:404, statusDescription:'Post was not found' }) | |
|> return }} | |
``` | |
The **httpResult** filter returns a ServiceStack `HttpResult` which allows for the following customization's: | |
```csharp | |
httpResult({ | |
status: 404, | |
status: 'NotFound' // can also use .NET HttpStatusCode enum name | |
statusDescription: 'Post was not found', | |
response: post, | |
format: 'json', | |
contentType: 'application/json', | |
'X-Powered-By': 'ServiceStack Templates', | |
}) | |
``` | |
Any other arguments like 'X-Powered-By' are returned as HTTP Response Headers. | |
This behaves similarly to customizing a response with return arguments: | |
``` | |
{{ post |> return({ format:'json', 'X-Powered-By':'ServiceStack #Script' }) }} | |
``` | |
Using the explicit httpResult filter is useful for returning a custom HTTP Response that doesn't have a Response Body, e.g. the **New Post** page | |
uses `httpFilter` to | |
[redirect back to the Users posts page](https://github.com/sharp-apps/blog/blob/e8bb7249192c5797348ced091ad5fd434db9a619/app/posts/new.html#L33) | |
after they've successfully created a new Post: | |
``` | |
{{#if success}} | |
{{ httpResult({ status:301, Location:`/${userName}` }) |> return }} | |
{{/if}} | |
``` | |
For more examples and info on API Pages checkout to the [API Pages docs](https://sharpscript.net/docs/api-pages). | |
{{/markdown}}{{/raw}} | |
{{ { title, content } |> addTo => initialPosts }} | |
============================================ | |
{{ var title = 'Page based routing' }} | |
============================================ | |
{{#raw content}}{{#markdown}} | |
Template Pages supports conventional page-based routes where the name of each page can be requested with or without its **.html** extension: | |
| path | page | | |
------ | ---- | | |
[/db](/db) | | | |
[/db.html](/db.html) | [/db.html](https://github.com/sharp-apps/blog/blob/master/app/db.html) | | |
[/posts/new](/posts/new) | | | |
[/posts/new.html](/posts/new.html) | [/posts/new.html](https://github.com/sharp-apps/blog/blob/master/app/posts/new.html) | |
and the default route **/** maps to the `index.html` in each directory if it exists, e.g: | |
| path | page | | |
------ | ---- | | |
[/](/) | [/index.html](https://github.com/sharp-apps/blog/blob/master/app/index.html) | | |
Nuxt-like [Dynamic Routes](https://nuxtjs.org/guide/routing#dynamic-routes) can also be used where any **file** or **directory** names | |
prefixed with an _ **underscore** allows for dynamic wildcard paths with the matching path component also assigned to the arguments name: | |
| path | page | arguments | | |
------ | ---- | --------- | | |
[/ServiceStack](/) | [/_user/index.html](https://github.com/sharp-apps/blog/blob/master/app/_user/index.html) | user=ServiceStack | | |
[/posts/markdown-example](/posts/markdown-example) | [/posts/_slug/index.html](https://github.com/sharp-apps/blog/blob/master/app/posts/_slug/index.html) | slug=markdown-example | | |
[/posts/markdown-example/edit](/posts/markdown-example/edit) | [/posts/_slug/edit.html](https://github.com/sharp-apps/blog/blob/master/app/posts/_slug/edit.html) | slug=markdown-example | | |
### Layout and partial recommended naming conventions | |
As the **_ underscore** prefix for declaring wildcard pages is also what is used to declare "hidden" pages, to disambiguate them from hidden | |
partials and layouts, the recommendation is to have them include `layout` and `partial` their name, i.e: | |
- _layout.html | |
- _alt-layout.html | |
- _menu-partial.html | |
Pages with `layout` or `partial` in their name are hidden and ignored in wildcard path resolution. | |
If you follow the recommended `_{name}-partial.html` naming convention you will also be able to reference partials using just their name, i.e: | |
``` | |
{{ 'menu' |> partial }} // Equivalent to: | |
{{ '_menu-partial' |> partial }} | |
``` | |
{{/markdown}}{{/raw}} | |
{{ { title, content } |> addTo => initialPosts }} | |
=============================== | |
{{ var title = 'About' }} | |
=============================== | |
{{#raw content}}{{#markdown}} | |
This Blog App demonstrates some of the capabilities in [ServiceStack Web Apps](https://sharpscript.net/docs/sharp-apps) - an exciting real-time | |
development model for developing .NET Core Apps where entire Web Apps can be developed within a live hot-reload experience without any compilation, | |
build tools, dependencies, IDEs or any C# source code necessary by using the powerful and user-friendly | |
[ServiceStack #Script language](https://sharpscript.net/) and its comprehensive built-in functionality. | |
### Ultimate Simplicity | |
This eliminates much of the complexity inherent in developing .NET Web Applications which by their nature results in highly customizable Web Apps | |
where their entire functionality can be modified in real-time whilst the App is running, which is simple enough to be enhanced by non-developers | |
like Designers and Content Creators courtesy of its approachable [Handlebars-like](https://sharpscript.net/docs/blocks) and familiar | |
[JavaScript syntax](https://sharpscript.net/docs/expression-viewer#expression=map(range(1%2Ccount)%2C%20x%20%3D%3E%20x%20*%20x)&count=5). | |
Compiled Apps can have a prohibitively large barrier to entry where any modification often requires downloading source code separately, setting | |
up a matching development environment with appropriate extensions and correct versions and non cursory level of experience with their chosen | |
language, frameworks, build tools and other platform idiosyncrasies. | |
By contrast Web Apps require no development environment, no IDE's or build tools with all source code already included as part of the App which | |
can be modified in real-time by any text editor to instantly view changes as they're made. So Apps like http://redis.web-app.io which provide a | |
rich Admin UI for searching, browsing and modifying Redis's core data structures, can be easily enhanced by modifying a single | |
[index.html](https://github.com/NetCoreWebApps/Redis/blob/master/app/index.html) at the same time as using the App. | |
## Blog App Features | |
This [/Blog](https://github.com/sharp-apps/blog/tree/master/app) Web App is another example of encapsulating useful functionality in a | |
highly customizable .NET Core Web App which to maximize approachability has no C# source code, plugins and uses no JavaScript or CSS frameworks. | |
The development of which ended up being one of the most enjoyable experiences we've had in recent memory where all the usual complexities of | |
developing a C# Server and modern JS App has been dispensed and you can just focus on the App you want to create, | |
using a plain-text editor on the left, a live updating browser on the right and nothing else to interrupt your flow. | |
Any infrastructure dependencies have also been avoided by using SQLite by default which is | |
[automatically created an populated on first run](/posts/web-app-customizations) if no database exists, or if preferred can be | |
[changed to use any other popular RDBMS](https://sharpscript.net/docs/sharp-apps#multi-platform-configurations) using just config. | |
### Multi User Blogging Platform | |
Any number of users can Sign In via Twitter and publish content under their Twitter Username where only they'll be able to modify their own Content. | |
Setting up Twitter is as easy as it can be which just requires modifying the | |
[Twitter Auth Config in web.settings](/posts/web-app-customizations#customizable-auth-providers) with the URL where the blog | |
is hosted and the OAuth Keys for the Twitter OAuth App created at https://apps.twitter.com | |
### Rich Content | |
Whilst most blogging platforms just edit static text, each Post content has access to the powerful and | |
[Sandboxed](https://sharpscript.net/docs/sandbox) features in https://sharpscript.net/ which can be used to create | |
[Live Documents](/posts/live-document-example) or [Render Markdown](/posts/markdown-example) which is itself just | |
[one of the available blocks](https://sharpscript.net/docs/blocks#markdown) where it will render to HTML any content between the `markdown` blocks: | |
{{/markdown}} | |
<pre><code>{{#markdown}} | |
## Markdown Content | |
{{/markdown}}</code></pre> | |
{{#markdown}} | |
By default the [Markdig](https://github.com/lunet-io/markdig) implementation is used to render Markdown but can also be configured to use an | |
[alternate Markdown provider](/posts/web-app-customizations#customizable-markdown-providers). | |
### Rich Markdown Editor | |
To make it easy to recall Markdown features, each Content is equipped with a Rich Text editor containing the most popular formatting controls | |
along with common short-cuts for each feature, discoverable by hovering over each button: | |
 | |
The Editor also adopts popular behavior in IDEs where `Tab` and `SHIFT+Tab` can be used to indent blocks of text and lines can be commented with | |
`Ctrl+/` or blocks with `CTRL+SHIFT+/`. | |
Another nice productivity win is being able to `CTRL+CLICK` on any Content you created to navigate to its Edit page. | |
### Auto saved drafts | |
The content in each Text `input` and `textarea` is saved to `localStorage` on each key-press so you can freely reload pages and navigate | |
around the site where all unpublished content will be preserved upon return. | |
When you want to revert back to the original published content you can clear the text boxes and reload the page which will load content from | |
the database instead of `localStorage`. | |
### Server Validation | |
The [new.html](https://github.com/sharp-apps/blog/blob/master/app/posts/new.html) and [edit.html](https://github.com/sharp-apps/blog/blob/master/app/posts/_slug/edit.html) pages shows examples of performing server validation with ServiceStack #Script: | |
``` | |
{{ assignErrorAndContinueExecuting: ex }} | |
{{ 'Title must be between 5 and 200 characters' | |
|> onlyIf(length(postTitle) < 5 || length(postTitle) > 200) |> to => titleError }} | |
{{ 'Content must be between 25 and 64000 characters' | |
|> onlyIf(length(content) < 25 || length(content) > 64000) |> to => contentError }} | |
{{ 'Potentially malicious characters detected' | |
|> ifNotExists(contentError) | onlyIf(containsXss(content)) |> to => contentError }} | |
``` | |
 | |
For more info see the docs on [Error Handling](https://sharpscript.net/docs/error-handling). | |
### Live Previews | |
Creating and Posting content benefit from Live Previews where its rendered output can be visualized in real-time before it's published. | |
Any textarea can easily be enhanced to enable Live Preview by including the `data-livepreview` attribute with the element where the output | |
should be rendered in, e.g: | |
<textarea data-livepreview=".preview"></textarea> | |
<div class="preview"></div> | |
The implementation of which is surprisingly simple where the JavaScript snippet in | |
[default.js](https://github.com/sharp-apps/blog/blob/master/app/default.js) below is used to send their content on each change: | |
``` | |
// Enable Live Preview of new Content | |
textAreas = document.querySelectorAll("textarea[data-livepreview]"); | |
for (let i = 0; i < textAreas.length; i++) { | |
textAreas[i].addEventListener("input", livepreview, false); | |
livepreview({ target: textAreas[i] }); | |
} | |
function livepreview(e) { | |
let el = e.target; | |
let sel = el.getAttribute("data-livepreview"); | |
if (el.value.trim() == "") { | |
document.querySelector(sel).innerHTML = "<div>Live Preview</div>"; | |
return; | |
} | |
let formData = new FormData(); | |
formData.append("content", el.value); | |
fetch("/preview", { | |
method: "post", | |
body: formData | |
}).then(function(r) { return r.text(); }) | |
.then(function(r) { document.querySelector(sel).innerHTML = r; }); | |
} | |
``` | |
To the [/preview.html](https://github.com/sharp-apps/blog/blob/master/app/preview.html) API Page which just renders and captures any | |
Template Content its sent and returns the output: | |
``` | |
{{ content | evalTemplate({use:{plugins:'MarkdownScriptPlugin'}}) |> to =>response }} | |
{{ response | return({ contentType:'text/plain' }) }} | |
``` | |
By default the `evalTemplate` filter renders ` in a new `ScriptContext` which can be customized to utilize any additional | |
`plugins`, `filters` and `blocks` available in the configured [SharpPagesFeature](https://sharpscript.net/docs/sharp-pages), | |
or for full access you can use `{use:{context:true}}` to evaluate any Template content under the same context that `evalTemplate` is run on. | |
{{/markdown}}{{/raw}} | |
{{ { title, content } |> addTo => initialPosts }} | |
====================== | |
Populate initial posts | |
====================== | |
{{#each initialPosts}} | |
{{ `INSERT INTO Post (Slug, Title, Content, Created, CreatedBy, Modified, ModifiedBy) | |
VALUES (@slug, @title, @content, ${sqlNow}, @user, ${sqlNow}, @user)` | |
| dbExec({ slug: generateSlug(title), title, content, user }) }} | |
{{/each}} | |
{{/if}} | |
{{ htmlError }} |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<link href="https://fonts.googleapis.com/css?family=Noticia+Text|Open+Sans|Special+Elite" rel="stylesheet"> | |
<title>{{title ?? 'simple.blog'}}</title> | |
<link rel="stylesheet" href="/default.css"> | |
</head> | |
<body> | |
<i hidden>{{ '/js/hot-fileloader.js' |> ifDebugIncludeScript }}</i> | |
<h3 id="title"><a href="{{ blog.href ?? '/' }}">{{ blog.name ?? 'blog' }}</a></h3> | |
{{#if isAuthenticated }} | |
{{ `SELECT * from UserInfo WHERE UserName = @userName` |> dbSingle({ userName }) |> to => userInfo }} | |
{{#if !userInfo}} | |
{{ var twitterAuth = userSession.providerOAuthAccess[0] }} | |
{{ var avatarUrl = twitterAuth.items.profileUrl.replace(`\\/`,'/') }} | |
{{ var avatarUrlLarge = avatarUrl.replace('_normal','') }} | |
{{ var sqlNow = `datetime(CURRENT_TIMESTAMP,'localtime')` }} | |
{{ `INSERT INTO UserInfo (UserName, DisplayName, AvatarUrl, AvatarUrlLarge, Created, Modified) | |
VALUES (@userName, @name, @avatarUrl, @avatarUrlLarge, ${sqlNow}, ${sqlNow})` | |
|> dbExec({ userName, name: userSession.displayName, avatarUrl, avatarUrlLarge }) }} | |
{{else}} | |
{{ var avatarUrl = userInfo.AvatarUrl}} | |
{{/if}} | |
<script> | |
var redirectedFromTwitter = location.hash === '#s=1'; | |
if (redirectedFromTwitter) location.href = '/{{username}}'; | |
</script> | |
<div class="profile auth"> | |
<div id="avatar"> | |
<div class="info"> | |
<div> | |
<a href="/auth/logout">sign out</a> | |
</div> | |
</div> | |
<div class="avatar-sm"> | |
<a href="/{{username}}"><img src="{{ avatarUrl }}" alt="avatar"></a> | |
</div> | |
</div> | |
<div class="links"> | |
<div><a href="/posts/new">new post</a></div> | |
<div><a href="/db">db</a></div> | |
</div> | |
</div> | |
{{else}} | |
<div class="profile no-auth"> | |
<div> | |
<img class="svg-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGLSURBVEhL7ZQ7SwNBFIXHnY0IdiKCVj5axcZC0EJBxM7GSvwNVtpZaGnhDwgWQsDsTDBgIVhJGhEESaWdCFYiWISdWYKoGc9ubkJ2M8maUsgHh33MPWeeDOvzTzkzQ9wLNh2p97n0t1jBDFILY7naML1ZKBjuCH3AjBmgP+1cVKe50E9catMUvtHZHrw3TFanqLKdjFRzdZM6jY2qBQTdxsKbUj9cqKwj1Q6VtpMRer5hQNCjK/wlaqpT1OPx0Lgcoe7YuT9K1RZKxsUoXhOmB4xq1xXBItpWW9uSygg1S0kdKKqxcIpYU2ULSFPop6QOeNXJejHWM2FOFQbV9XA0wJJcWQNShJlfUkQKsjKCTq5tId2EDrYpIR0s0TE29MUW1EHP4QEhezowbKCDz0SIXULVuKfXyfp33HywgI3LQx/W4IaEOiJLj+SDCYSfIODLGgxF10ovuAV/GYE5bPI9nt+20EjhXeSpFbL1iFeZwSYf4qooIeg9moFQbzglZTyz0Fp4KVJ1nz6M/QL269uAfmjPLQAAAABJRU5ErkJggg=="> | |
<a href="/auth/twitter">sign in</a> | |
<div><a href="/db">db</a></div> | |
</div> | |
</div> | |
{{/if}} | |
<div id="body"> | |
{{page}} | |
</div> | |
{{htmlError}} | |
{{initError}} | |
<script src="/default.js"></script> | |
</body> | |
</html> |
{{#each posts}} | |
<div class="user-post"> | |
{{#partial contentActions }} | |
{{#if isAuthenticated and userName = CreatedBy }} | |
<div class="content-actions"> | |
<a class="lnk" href="/posts/{{Slug}}/edit">edit post</a> | |
<form action="/posts/{{Slug}}/delete" method="POST"> | |
<button class="btn btn-danger" type="submit" onclick="return confirm('Are you sure you want to delete this post?')">delete post</button> | |
</form> | |
</div> | |
{{/if}} | |
{{/partial}} | |
{{#if !hidePostInfo}} | |
<div class="post-info"> | |
<div class="avatar-lg"><a href="/{{CreatedBy}}"><img src="{{ AvatarUrlLarge }}" alt="avatar"></a></div> | |
<div class="post-meta"> | |
<a href="/{{CreatedBy}}">@{{ CreatedBy }}</a> | |
</div> | |
</div> | |
{{'contentActions' |> partial({ Slug }) }} | |
{{/if}} | |
<h2 class="post-title"> | |
<a href="/posts/{{Slug}}">{{Title}}</a> | |
</h2> | |
<div class="post-date"> | |
{{ Created |> toDateTime |> dateFormat('dddd, dd MMMM') }} | |
</div> | |
{{ { 'data-edit-path': `/posts/${Slug}/edit`} |> if (isAuthenticated and userName == CreatedBy) |> to => attrs }} | |
<div class="post-content" {{ attrs | htmlAttrs }}> | |
{{ Content |> evalTemplate({use:{plugins:'MarkdownScriptPlugin'}}) |> raw }} | |
</div> | |
{{#if hidePostInfo}} | |
{{ 'contentActions' |> partial({ Slug }) }} | |
{{/if}} | |
</div> | |
{{/each}} |
API /{user}/api | |
* user : string - Return posts created by this Username | |
{{ `SELECT * | |
FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName | |
WHERE UserName = @user | |
ORDER BY p.Created DESC` | |
| dbSelect({ user }) | |
| return }} |
<div class="page"> | |
{{ `SELECT * from UserInfo WHERE UserName = @user` | dbSingle({ user }) |> to => userInfo }} | |
{{#with userInfo}} | |
<script>document.title = '{{DisplayName}} (@{{UserName}})'</script> | |
<div id="user-profile"> | |
<div class="avatar-lg"><img src="{{ AvatarUrlLarge }}" alt="avatar"></div> | |
<h3>@{{UserName}} posts</h3> | |
{{ var userPageIsAuthenticatedUser = UserName == userName }} | |
{{#if userPageIsAuthenticatedUser }} | |
<div class="links"> | |
<a href="/posts/new"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24"> | |
<path d="M0 0h24v24H0z" fill="none"/> | |
<path d="M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4V7zm-1-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/> | |
</svg> | |
<div>new post</div> | |
</a> | |
</div> | |
{{/if}} | |
</div> | |
<div> | |
{{ `SELECT * | |
FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName | |
WHERE UserName = @user | |
ORDER BY p.Created DESC, Id DESC` | |
|> dbSelect({ user }) | |
|> to => posts }} | |
{{ 'posts' |> partial({ posts, hidePostInfo: true }) }} | |
</div> | |
<style> | |
#body { | |
flex-direction: column; | |
} | |
#user-profile { | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
text-align: center; | |
} | |
#user-profile h3 { | |
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
font-weight: normal; | |
margin: .5em 0; | |
} | |
#user-profile .links { | |
margin: .5em 0 0 0; | |
display: flex; | |
justify-content: center; | |
} | |
</style> | |
{{else}} | |
{{ 'error' |> partial({ error: `User ${user} does not exist` }) }} | |
{{/with}} | |
</div> |
debug false | |
name Blog Sharp App | |
db sqlite | |
db.connection blog.sqlite | |
features AuthFeature | |
AuthFeature.AuthProviders TwitterAuthProvider | |
SharpPagesFeature { ScriptAdminRole: 'AllowAnon' } | |
oauth.RedirectUrl https://localhost:5001/ | |
oauth.CallbackUrl https://localhost:5001/auth/{0} | |
oauth.twitter.ConsumerKey JvWZokH73rdghDdCFCFkJtCEU | |
oauth.twitter.ConsumerSecret WNeOT6YalxXDR4iWZjc4jVjFaydoDcY8jgRrGc5FVLjsVlY2Y8 | |
args.blog { name:'blog.web-app.io', href:'/' } | |
CefConfig { width:1150, height:1050 } |
U1FMaXRlIGZvcm1hdCAzABAAAQEAQCAgAAAACgAAAAwAAAAAAAAAAAAAAAIAAAAEAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAC5DwQ0OEwAEDJEADm0OGwyRDeIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIJOAwcXHR0BhG90YWJsZVVzZXJJbmZvVXNlckluZm8EQ1JFQVRFIFRBQkxFICJVc2VySW5mbyIgDQogICAgKA0KICAgICAgICAiVXNlck5hbWUiIFZBUkNIQVIoODAwMCkgUFJJTUFSWSBLRVksIA0KICAgICAgICAiRGlzcGxheU5hbWUiIFZBUkNIQVIoODAwMCkgTlVMTCwgDQogICAgICAgICJBdmF0YXJVcmwiIFZBUkNIQVIoODAwMCkgTlVMTCwgDQogICAgICAgICJBdmF0YXJVcmxMYXJnZSIgVkFSQ0hBUig4MDAwKSBOVUxMLCANCiAgICAgICAgIkNyZWF0ZWQiIFZBUkNIQVIoODAwMCkgTk9UIE5VTEwsDQogICAgICAgICJNb2RpZmllZCIgVkFSQ0hBUig4MDAwKSBOT1QgTlVMTA0KICAgICkvBAYXQx0BAGluZGV4c3FsaXRlX2F1dG9pbmRleF9Vc2VySW5mb18xVXNlckluZm8FAAAACAAAAABQAgYXKysBWXRhYmxlc3FsaXRlX3NlcXVlbmNlc3FsaXRlX3NlcXVlbmNlA0NSRUFURSBUQUJMRSBzcWxpdGVfc2VxdWVuY2UobmFtZSxzZXEpgxABBxcVFQGGA3RhYmxlUG9zdFBvc3QCQ1JFQVRFIFRBQkxFICJQb3N0IiANCiAgICAoDQogICAgICAgICJJZCIgSU5URUdFUiBQUklNQVJZIEtFWSBBVVRPSU5DUkVNRU5ULCANCiAgICAgICAgIlNsdWciIFZBUkNIQVIoODAwMCkgTlVMTCwgDQogICAgICAgICJUaXRsZSIgVkFSQ0hBUig4MDAwKSBOVUxMLCANCiAgICAgICAgIkNvbnRlbnQiIFZBUkNIQVIoODAwMCkgTlVMTCwgDQogICAgICAgICJDcmVhdGVkIiBWQVJDSEFSKDgwMDApIE5PVCBOVUxMLCANCiAgICAgICAgIkNyZWF0ZWRCeSIgVkFSQ0hBUig4MDAwKSBOT1QgTlVMTCwgDQogICAgICAgICJNb2RpZmllZCIgVkFSQ0hBUig4MDAwKSBOT1QgTlVMTCwNCiAgICAgICAgIk1vZGlmaWVkQnkiIFZBUkNIQVIoODAwMCkgTk9UIE5VTEwgDQogICAgKQUAAAADD/EAAAAACg/7D/YP8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGOQIKAC0tiy8zJTMlbWFya2Rvd24tZXhhbXBsZU1hcmtkb3duIEV4YW1wbGV7eyNtYXJrZG93bn19DQojIEhlYWRpbmdzIGNhbiBzdGFydCB3aXRoIDEtNiBoYXNoZXMNCg0KTWFya2Rvd24gZm9sbG93cyBwbGFpbiB0ZXh0IGNvbnZlbnRpb25zIHdoZW4gcmVuZGVyaW5nIEhUTUwuDQoNClNvIHBhcmFncmFwaHMgc2VwYXJhdGVkIGJ5IG11bHRpcGxlIGxpbmVzIGFyZSByZW5kZXJlZCBhcyBzZXBhcmF0ZSBwYXJhZ3JhcGhzLg0KDQojIyMgVXNlIG1vcmUgaGFzaGVzIHRvIGNyZWF0ZSBuZXN0ZWQgc3ViIGhlYWRpbmdzDQoNCj4gVGV4dCBwcmUtZml4ZWQgd2l0aCAnPicgYXJlIHRyZWF0ZWQgYXMgYmxvY2sgcXVvdGVzDQoNClVzZSBhIGRhc2gsIGFzdGVyaXNrIG9yIHBsdXMgdG8gY3JlYXRlIGFuIG9yZGVyZWQgbGlzdDoNCg0KIC0gTGlzdCBJdGVtDQogKiBMaXN0IEl0ZW0NCiArIExpc3QgSXRlbQ0KDQpXaGlsc3QgeW91IGNhbiB1c2UgbnVtYmVycyBmb3Igb3JkZXJlZCBsaXN0czoNCg0KIDEuIFN0ZXAgMQ0KIDIuIFN0ZXAgMg0KIDMuIFN0ZXAgMw0KDQpJbmRlbnQgbGluZXMgYnkgNCBzcGFjZXMgdG8gY3JlYXRlIHByZS1mb3JtYXR0ZWQgY29kZSBibG9ja3MgaW4gbW9ub3NwYWNlIGZvbnQ6DQoNCiAgICAkIGNkIC9Vc2Vycy9HdWVzdA0KDQpGb2xsb3cgW2xpbmsgdG8gbWFya2Rvd24gYmxvY2sgZG9jc10oaHR0cHM6Ly9zaGFycHNjcmlwdC5uZXQvZG9jcy9ibG9ja3MjbWFya2Rvd24pIGZvciBtb3JlIGluZm8uDQp7ey9tYXJrZG93bn19MjAyMC0wNi0xNiAwNDozNzoxMlNlcnZpY2VTdGFjazIwMjAtMDYtMTYgMDQ6Mzc6MTJTZXJ2aWNlU3RhY2uMSwEKADc3lz8zJTMlbGl2ZS1kb2N1bWVudC1leGFtcGxlTGl2ZSBEb2N1bWVudCBFeGFtcGxle3sjbWFya2Rvd259fQ0KQWxsIEJsb2cgcG9zdHMgaGF2ZSBhY2Nlc3MgdG8gW1NlcnZpY2VTdGFjayBUZW1wbGF0ZXNdKGh0dHBzOi8vc2hhcnBzY3JpcHQubmV0LykgZmVhdHVyZXMgd2hpY2ggZW5hYmxlcyB0aGV5IHRvIHVzZSBhIGhpZ2hseS1wcm9kdWN0aXZlLCANCmVhc3kgdG8gdXNlIFtzYW5kYm94ZWRdKGh0dHBzOi8vc2hhcnBzY3JpcHQubmV0L2RvY3Mvc2FuZGJveCkgZHluYW1pYyB0ZW1wbGF0aW5nIGxhbmd1YWdlIHdoaWNoIGxldHMgeW91IGVhc2lseSBjcmVhdGUgbGl2ZSBkb2N1bWVudHMgDQpbbGlrZSB0aGlzIG9uZV0oL3Bvc3RzL2xpdmUtZG9jdW1lbnQtZXhhbXBsZSk6DQp7ey9tYXJrZG93bn19DQoNCjxwcmU+DQpgYGBjb2RlDQoxMTIwMCAgICAgICAgICAgICB8PiB0byA9PiBiYWxhbmNlDQozICAgICAgICAgICAgICAgICB8PiB0byA9PiBwcm9qZWN0ZWRNb250aHMNCg0KI2tleXZhbHVlcyBtb250aGx5UmV2ZW51ZXMgJzonDQogIFNhbGFyeTogICAgICAgICA0MDAwDQogIEFwcCBSb3lhbHRpZXM6ICAyMDANCi9rZXl2YWx1ZXMNCg0KI2tleXZhbHVlcyBtb250aGx5RXhwZW5zZXMNCiAgUmVudCAgICAgICAgICAgIDEwMDANCiAgSW50ZXJuZXQgICAgICAgIDUwDQogIE1vYmlsZSAgICAgICAgICA1MA0KICBGb29kICAgICAgICAgICAgNDAwDQogIE1pc2MgICAgICAgICAgICAyMDANCi9rZXl2YWx1ZXMNCg0KbW9udGhseVJldmVudWVzIHw+IHN1bSA9PiBpdC5WYWx1ZSB8PiB0byA9PiB0b3RhbFJldmVudWVzDQptb250aGx5RXhwZW5zZXMgfD4gc3VtID0+IGl0LlZhbHVlIHw+IHRvID0+IHRvdGFsRXhwZW5zZXMNCih0b3RhbFJldmVudWVzIC0gdG90YWxFeHBlbnNlcykgICAgfD4gdG8gPT4gdG90YWxTYXZpbmdzDQpgYGANCkN1cnJlbnQgQmFsYW5jZTogPGI+e3sgYmFsYW5jZSB8PiBjdXJyZW5jeSB9fTwvYj4NCg0KTW9udGhseSBSZXZlbnVlczoNCnt7bW9udGhseVJldmVudWVzIHw+IHNlbGVjdDoge2l0LktleS5wYWRSaWdodCgxNil9IHtpdC5WYWx1ZS5jdXJyZW5jeSgpfVxuIH19DQpUb3RhbCAgICAgICAgICAgIDxiPnt7IHRvdGFsUmV2ZW51ZXMgfD4gY3VycmVuY3kgfX08L2I+DQoNCk1vbnRobHkgRXhwZW5zZXM6DQp7e21vbnRobHlFeHBlbnNlcyB8PiBzZWxlY3Q6IHtpdC5LZXkucGFkUmlnaHQoMTYpfSB7aXQuVmFsdWUuY3VycmVuY3koKX1cbiB9fQ0KVG90YWwgICAgICAgICAgICA8Yj57eyB0b3RhbEV4cGVuc2VzIHw+IGN1cnJlbmN5IH19PC9iPg0KDQpNb250aGx5IFNhdmluZ3M6IDxiPnt7IHRvdGFsU2F2aW5ncyAgfD4gY3VycmVuY3kgfX08L2I+DQoNClByb2plY3RlZCBDYXNoIFBvc2l0aW9uOg0Ke3twcm9qZWN0ZWRNb250aHMudGltZXMoKSB8PiBtYXAgPT4gaW5kZXggKyAxIHw+IG1hcCA9PiANCmAke25vdy5hZGRNb250aHMoaXQpLmRhdGVGb3JtYXQoKX0gICAgICAgJHsoaXQgKiB0b3RhbFNhdmluZ3MgKyBiYWxhbmNlKS5jdXJyZW5jeSgpfWB8PiBqb2lubG59fQ0KPC9wcmU+MjAyMC0wNi0xNiAwNDozNzoxMlNlcnZpY2VTdGFjazIwMjAtMDYtMTYgMDQ6MzcAAAAJBAAAAAcDAAAABgINAAAAAQ/2AA/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAQMVAVBvc3QGDQAAAAIOTwAPJw5PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgVUCCSMlgSOBFTMzZGVtaXNiZWxsb3REZW1pcyBCZWxsb3RodHRwOi8vcGJzLnR3aW1nLmNvbS9wcm9maWxlX2ltYWdlcy8xMjQ0OTgzMDM4NTA2NzA4OTkyLzFGaXBKSWUzX25vcm1hbC5wbmdodHRwOi8vcGJzLnR3aW1nLmNvbS9wcm9maWxlX2ltYWdlcy8xMjQ0OTgzMDM4NTA2NzA4OTkyLzFGaXBKSWUzLnBuZzIwMjAtMDYtMTcgMTI6NDY6MzAyMDIwLTA2LTE3IDEyOjQ2OjMwgVYBCSUlgSOBFTMzU2VydmljZVN0YWNrU2VydmljZVN0YWNraHR0cHM6Ly9wYnMudHdpbWcuY29tL3Byb2ZpbGVfaW1hZ2VzLzg3NjI0OTczMDA3ODA1NjQ0OC9KdVRWRWtXWF9ub3JtYWwuanBnaHR0cHM6Ly9wYnMudHdpbWcuY29tL3Byb2ZpbGVfaW1hZ2VzLzg3NjI0OTczMDA3ODA1NjQ0OC9KdVRWRWtXWC5qcGcyMDIwLTA2LTE2IDA0OjM3OjEyMjAyMC0wNi0xNiAwNDozNzoxMgoAAAACD+AAD/AP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwMjAWRlbWlzYmVsbG90Ag8DJQlTZXJ2aWNlU3RhY2sNAAAAAgZ2AAmyBnYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhjkCCgAtLYsvMyUzJW1hcmtkb3duLWV4YW1wbGVNYXJrZG93biBFeGFtcGxle3sjbWFya2Rvd259fQ0KIyBIZWFkaW5ncyBjYW4gc3RhcnQgd2l0aCAxLTYgaGFzaGVzDQoNCk1hcmtkb3duIGZvbGxvd3MgcGxhaW4gdGV4dCBjb252ZW50aW9ucyB3aGVuIHJlbmRlcmluZyBIVE1MLg0KDQpTbyBwYXJhZ3JhcGhzIHNlcGFyYXRlZCBieSBtdWx0aXBsZSBsaW5lcyBhcmUgcmVuZGVyZWQgYXMgc2VwYXJhdGUgcGFyYWdyYXBocy4NCg0KIyMjIFVzZSBtb3JlIGhhc2hlcyB0byBjcmVhdGUgbmVzdGVkIHN1YiBoZWFkaW5ncw0KDQo+IFRleHQgcHJlLWZpeGVkIHdpdGggJz4nIGFyZSB0cmVhdGVkIGFzIGJsb2NrIHF1b3Rlcw0KDQpVc2UgYSBkYXNoLCBhc3RlcmlzayBvciBwbHVzIHRvIGNyZWF0ZSBhbiBvcmRlcmVkIGxpc3Q6DQoNCiAtIExpc3QgSXRlbQ0KICogTGlzdCBJdGVtDQogKyBMaXN0IEl0ZW0NCg0KV2hpbHN0IHlvdSBjYW4gdXNlIG51bWJlcnMgZm9yIG9yZGVyZWQgbGlzdHM6DQoNCiAxLiBTdGVwIDENCiAyLiBTdGVwIDINCiAzLiBTdGVwIDMNCg0KSW5kZW50IGxpbmVzIGJ5IDQgc3BhY2VzIHRvIGNyZWF0ZSBwcmUtZm9ybWF0dGVkIGNvZGUgYmxvY2tzIGluIG1vbm9zcGFjZSBmb250Og0KDQogICAgJCBjZCAvVXNlcnMvR3Vlc3QNCg0KRm9sbG93IFtsaW5rIHRvIG1hcmtkb3duIGJsb2NrIGRvY3NdKGh0dHBzOi8vc2hhcnBzY3JpcHQubmV0L2RvY3MvYmxvY2tzI21hcmtkb3duKSBmb3IgbW9yZSBpbmZvLg0Ke3svbWFya2Rvd259fTIwMjAtMDYtMTYgMDQ6Mzc6MTJTZXJ2aWNlU3RhY2syMDIwLTA2LTE2IDA0OjM3OjEyU2VydmljZVN0YWNrjEsBCgA3N5c/MyUzJWxpdmUtZG9jdW1lbnQtZXhhbXBsZUxpdmUgRG9jdW1lbnQgRXhhbXBsZXt7I21hcmtkb3dufX0NCkFsbCBCbG9nIHBvc3RzIGhhdmUgYWNjZXNzIHRvIFtTZXJ2aWNlU3RhY2sgVGVtcGxhdGVzXShodHRwczovL3NoYXJwc2NyaXB0Lm5ldC8pIGZlYXR1cmVzIHdoaWNoIGVuYWJsZXMgdGhleSB0byB1c2UgYSBoaWdobHktcHJvZHVjdGl2ZSwgDQplYXN5IHRvIHVzZSBbc2FuZGJveGVkXShodHRwczovL3NoYXJwc2NyaXB0Lm5ldC9kb2NzL3NhbmRib3gpIGR5bmFtaWMgdGVtcGxhdGluZyBsYW5ndWFnZSB3aGljaCBsZXRzIHlvdSBlYXNpbHkgY3JlYXRlIGxpdmUgZG9jdW1lbnRzIA0KW2xpa2UgdGhpcyBvbmVdKC9wb3N0cy9saXZlLWRvY3VtZW50LWV4YW1wbGUpOg0Ke3svbWFya2Rvd259fQ0KDQo8cHJlPg0KYGBgY29kZQ0KMTEyMDAgICAgICAgICAgICAgfD4gdG8gPT4gYmFsYW5jZQ0KMyAgICAgICAgICAgICAgICAgfD4gdG8gPT4gcHJvamVjdGVkTW9udGhzDQoNCiNrZXl2YWx1ZXMgbW9udGhseVJldmVudWVzICc6Jw0KICBTYWxhcnk6ICAgICAgICAgNDAwMA0KICBBcHAgUm95YWx0aWVzOiAgMjAwDQova2V5dmFsdWVzDQoNCiNrZXl2YWx1ZXMgbW9udGhseUV4cGVuc2VzDQogIFJlbnQgICAgICAgICAgICAxMDAwDQogIEludGVybmV0ICAgICAgICA1MA0KICBNb2JpbGUgICAgICAgICAgNTANCiAgRm9vZCAgICAgICAgICAgIDQwMA0KICBNaXNjICAgICAgICAgICAgMjAwDQova2V5dmFsdWVzDQoNCm1vbnRobHlSZXZlbnVlcyB8PiBzdW0gPT4gaXQuVmFsdWUgfD4gdG8gPT4gdG90YWxSZXZlbnVlcw0KbW9udGhseUV4cGVuc2VzIHw+IHN1bSA9PiBpdC5WYWx1ZSB8PiB0byA9PiB0b3RhbEV4cGVuc2VzDQoodG90YWxSZXZlbnVlcyAtIHRvdGFsRXhwZW5zZXMpICAgIHw+IHRvID0+IHRvdGFsU2F2aW5ncw0KYGBgDQpDdXJyZW50IEJhbGFuY2U6IDxiPnt7IGJhbGFuY2UgfD4gY3VycmVuY3kgfX08L2I+DQoNCk1vbnRobHkgUmV2ZW51ZXM6DQp7e21vbnRobHlSZXZlbnVlcyB8PiBzZWxlY3Q6IHtpdC5LZXkucGFkUmlnaHQoMTYpfSB7aXQuVmFsdWUuY3VycmVuY3koKX1cbiB9fQ0KVG90YWwgICAgICAgICAgICA8Yj57eyB0b3RhbFJldmVudWVzIHw+IGN1cnJlbmN5IH19PC9iPg0KDQpNb250aGx5IEV4cGVuc2VzOg0Ke3ttb250aGx5RXhwZW5zZXMgfD4gc2VsZWN0OiB7aXQuS2V5LnBhZFJpZ2h0KDE2KX0ge2l0LlZhbHVlLmN1cnJlbmN5KCl9XG4gfX0NClRvdGFsICAgICAgICAgICAgPGI+e3sgdG90YWxFeHBlbnNlcyB8PiBjdXJyZW5jeSB9fTwvYj4NCg0KTW9udGhseSBTYXZpbmdzOiA8Yj57eyB0b3RhbFNhdmluZ3MgIHw+IGN1cnJlbmN5IH19PC9iPg0KDQpQcm9qZWN0ZWQgQ2FzaCBQb3NpdGlvbjoNCnt7cHJvamVjdGVkTW9udGhzLnRpbWVzKCkgfD4gbWFwID0+IGluZGV4ICsgMSB8PiBtYXAgPT4gDQpgJHtub3cuYWRkTW9udGhzKGl0KS5kYXRlRm9ybWF0KCl9ICAgICAgICR7KGl0ICogdG90YWxTYXZpbmdzICsgYmFsYW5jZSkuY3VycmVuY3koKX1gfD4gam9pbmxufX0NCjwvcHJlPjIwMjAtMDYtMTYgMDQ6Mzc6MTJTZXJ2aWNlU3RhY2syMDIwLTA2LTE2IDA0OjM3OjEyU2VydmljZVN0YWNrDQAAAAEBmAABmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnGUDCgA5ObdvMyUzJXdlYi1hcHAtY3VzdG9taXphdGlvbnNXZWIgQXBwIEN1c3RvbWl6YXRpb25ze3sjbWFya2Rvd259fQ0KDQojIyBJbml0IHBhZ2UNCg0KSnVzdCBsaWtlIGBHbG9iYWwuYXNheC5jc2AgY2FuIGJlIHVzZWQgdG8gcnVuIFN0YXJ0dXAgbG9naWMgaW4gQVNQLk5FVCBXZWIgQXBwbGljYXRpb25zIGFuZCBgU3RhcnR1cC5jc2AgaW4gLk5FVCBDb3JlIEFwcHMsIA0KeW91IGNhbiBhZGQgYSBgX2luaXQuaHRtbGAgdG8gcnVuIGxvZ2ljIG9uY2Ugb24gU3RhcnR1cC4gDQoNClRoaXMgaXMgdXNlZCBpbiB0aGlzIEJsb2cncyBbX2luaXQuaHRtbF0oaHR0cHM6Ly9naXRodWIuY29tL3NoYXJwLWFwcHMvYmxvZy9ibG9iL21hc3Rlci9hcHAvX2luaXQuaHRtbCkgd2hlcmUgaXQgd2lsbCBjcmVhdGUgYSBuZXcgDQpgYmxvZy5zcWxpdGVgIGRhdGFiYXNlIGlmIGl0IGRvZXNuJ3QgZXhpc3Qgc2VlZGVkIHdpdGggdGhlIFVzZXJJbmZvIGFuZCBQb3N0cyBUYWJsZXMgYW5kIGluaXRpYWwgZGF0YSwgZS5nOg0KDQpgYGANCnt7ICBgQ1JFQVRFIFRBQkxFIElGIE5PVCBFWElTVFMgIlVzZXJJbmZvIiANCiAgICAoDQogICAgICAgICJVc2VyTmFtZSIgVkFSQ0hBUig4MDAwKSBQUklNQVJZIEtFWSwgDQogICAgICAgICJEaXNwbGF5TmFtZSIgVkFSQ0hBUig4MDAwKSBOVUxMLCANCiAgICAgICAgIkF2YXRhclVybCIgVkFSQ0hBUig4MDAwKSBOVUxMLCANCiAgICAgICAgIkF2YXRhclVybExhcmdlIiBWQVJDSEFSKDgwMDApIE5VTEwsIA0KICAgICAgICAiQ3JlYXRlZCIgVkFSQ0hBUig4MDAwKSBOT1QgTlVMTCwNCiAgICAgICAgIk1vZGlmaWVkIiBWQVJDSEFSKDgwMDApIE5PVCBOVUxMDQogICAgKTtgICAgIA0KICAgIHw+IGRiRXhlYw0KfX0NCg0Ke3sgdmFyIHBvc3RzQ291bnQgPSBkYlNjYWxhclN5bmMoYFNFTEVDVCBDT1VOVCgqKSBGUk9NIFBvc3RgKSB9fQ0KDQp7eyNpZiBwb3N0c0NvdW50ID09IDAgfX0NCg0KICAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiAgICBTZWVkIHdpdGggaW5pdGlhbCBVc2VySW5mbyBhbmQgUG9zdCBkYXRhDQogICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KDQogICAgLi4uDQoNCnt7L2lmfQ0KDQp7eyBodG1sRXJyb3IgfX0NCmBgYA0KDQpUaGUgb3V0cHV0IG9mIHRoZSBgX2luaXRgIHBhZ2UgaXMgY2FwdHVyZWQgaW4gdGhlIGBpbml0b3V0YCBhcmd1bWVudCB3aGljaCBjYW4gYmUgaW5zcGVjdGVkIGFzIGEgdGVtcGxhdGUgYXJndW1lbnQgYXMgZG9uZSBpbiANCltsb2cuaHRtbF0oaHR0cHM6Ly9naXRodWIuY29tL3NoYXJwLWFwcHMvYmxvZy9ibG9iL21hc3Rlci9hcHAvbG9nLmh0bWwpOg0KDQpgYGANCjxkaXY+DQogICAgT3V0cHV0IGZyb20gaW5pdC5odG1sOg0KDQogICAgPHByZT57e2luaXRvdXQgfD4gcmF3fX08L3ByZT4NCjwvZGl2Pg0KYGBgDQoNCklmIHRoZXJlIHdhcyBhbiBFeGNlcHRpb24gd2l0aCBhbnkgb2YgdGhlIFNRTCBTdGF0ZW1lbnRzIGl0IHdpbGwgYmUgZGlzcGxheWVkIGluIHRoZSBge3sgaHRtbEVycm9yIH19YCBmaWx0ZXIgd2hpY2ggY2FuIGJlIGxhdGVyIHZpZXdlZCANCmluIHRoZSBbL2xvZ10oL2xvZykgcGFnZSBhYm92ZS4NCg0KIyMgQ3VzdG9taXphYmxlIEF1dGggUHJvdmlkZXJzDQoNCkF1dGggUHJvdmlkZXJzIGNhbiBiZSBjb25maWd1cmVkIGluIHRoZSBzYW1lIHdheSANCltXZWIgQXBwIFBsdWdpbnMgY2FuIGJlIHJlZ2lzdGVyZWRdKGh0dHBzOi8vc2hhcnBzY3JpcHQubmV0L2RvY3Mvc2hhcnAtYXBwcyNyZWdpc3RlcmluZy1zZXJ2aWNlc3RhY2stcGx1Z2lucykgYnkgZmlyc3Qgc3BlY2lmeWluZyANCnlvdSB3YW50IHRvIHJlZ2lzdGVyIHRoZSBgQXV0aEZlYXR1cmVgIHBsdWdpbiB3aXRoOg0KDQpgYGANCmZlYXR1cmVzIEF1dGhGZWF0dXJlDQpgYGANCg0KVGhlbiB1c2luZyBgQXV0aEZlYXR1cmUuQXV0aFByb3ZpZGVyc2AgdG8gc3BlY2lmeSB3aGljaCBBdXRoIFByb3ZpZGVycyB5b3Ugd2FudCB0byBoYXZlIHJlZ2lzdGVyZWQsIGUuZzoNCg0KYGBgDQpBdXRoRmVhdHVyZS5BdXRoUHJvdmlkZXJzIFR3aXR0ZXJBdXRoUHJvdmlkZXIsIEdpdGh1YkF1dGhQcm92aWRlcg0KYGBgDQoNCkVhY2ggQXV0aCBQcm92aWRlciBjaGVja3MgdGhlIFdlYiBBcHBzIGB3ZWIuc2V0dGluZ3NgIGZvciBpdHMgQXV0aCBQcm92aWRlciBzcGVjaWZpYyBjb25maWd1cmF0aW9uIGl0IG5lZWRzLCBlLmcuIHRvIGNvbmZpZ3VyZSBib3RoIA0KVHdpdHRlciBhbmQgR2l0SHViIEF1dGggUHJvdmlkZXJzIHlvdSB3b3VsZCBwb3B1bGF0ZSBpdCB3aXRoIHlvdXIgT0F1dGggQXBwcyBkZXRhaWxzOg0KDQpgYGANCm9hdXRoLlJlZGlyZWN0VXJsIGh0dHA6Ly8xMjcuMC4wLjE6NTAwMC8NCm9hdXRoLkNhbGxiYWNrVXJsIGh0dHA6Ly8xMjcuMC4wLjE6NTAwMC9hdXRoL3swfQ0KDQpvYXV0aC50d2l0dGVyLkNvbnN1bWVyS2V5IHtUd2l0dGVyIEFwcCBDb25zdW1lciBLZXl9DQpvYXV0aC50d2l0dGVyLkNvbnN1bWVyU2VjcmV0IHtUd2l0dGVyIEFwcCBDb25zdW1lciBTZWNyZXQgS2V5fQ0KDQpvYXV0aC5naXRodWIuQ2xpZW50SWQge0dpdEh1YiBDbGllbnQgSWR9DQpvYXV0aC5naXRodWIuQ2xpZW50U2VjcmV0IHtHaXRIdWIgQ2xpZW50IFNlY3JldH0NCm9hdXRoLmdpdGh1Yi5TY29wZXMge0dpdEh1YiBBdXRoIFNjb3Blc30NCmBgYA0KDQojIyBDdXN0b21pemFibGUgTWFya2Rvd24gUHJvdmlkZXJzDQoNCkJ5IGRlZmF1bHQgV2ViIEFwcHMgdXRpbGl6ZSBbTWFya2RpZ10oaHR0cHM6Ly9naXRodWIuY29tL2x1bmV0LWlvL21hcmtkaWcpIGltcGxlbWVudGF0aW9uIHRvIHJlbmRlciBpdHMgTWFya2Rvd24uIFlvdSBjYW4gc3dpdGNoIGl0IGJhY2sgdG8gDQp0aGUgYnVpbHQtaW4gTWFya2Rvd24gcHJvdmlkZXIgdGhhdCBTZXJ2aWNlU3RhY2sgdXNlcyB3aXRoOg0KDQpgYGANCm1hcmtkb3duUHJvdmlkZXIgTWFya2Rvd25EZWVwDQpgYGANCg0KIyMgUmljaCBUZW1wbGF0ZSBDb25maWcgQXJndW1lbnRzDQoNCkFueSBgd2ViLnNldHRpbmdzYCBjb25maWdzIHRoYXQgYXJlIHByZWZpeGVkIHdpdGggYGFyZ3MuYCBhcmUgbWFkZSBhdmFpbGFibGUgdG8gVGVtcGxhdGUgUGFnZXMuIEFueSBhcmd1bWVudHMgd2hpY2ggc3RhcnQgd2l0aCANCmB7YCBvciBgW2AgYXJlIGF1dG9tYXRpY2FsbHkgY29udmVydGVkIGludG8gYSBKUyBvYmplY3Q6DQoNCmBgYA0KYXJncy5ibG9nIHsgbmFtZTonYmxvZy53ZWItYXBwLmlvJywgaHJlZjonLycgfQ0KYXJncy50YWdzIFsndGVjaG5vbG9neScsJ21hcmtldGluZyddDQpgYGANCg0KV2hlcmUgdGhleSBjYW4gYmUgcmVmZXJlbmNlZCBhcyBhbiBgb2JqZWN0YCBvciBhbiBgYXJyYXlgIGRpcmVjdGx5Og0KDQpgYGANCjxhIGhyZWY9Int7YmxvZy5ocmVmfX0iPnt7YmxvZy5uYW1lfX08L2E+DQoNCnt7I2VhY2ggdGFnc319IDxlbT57e2l0fX08L2VtPiB7ey9lYWNofX0NCmBgYA0KDQpUaGUgYWx0ZXJuYXRpdmUgaXMgdG8gZ2l2ZSBlYWNoIGFyZ3VtZW50IHZhbHVlIGEgZGlmZmVyZW50IG5hbWU6DQoNCmBgYA0KYXJncy5ibG9nTmFtZSBibG9nLndlYi1hcHAuaW8NCmFyZ3MuYmxvZ0hyZWYgLw0KYGBgDQoNCnt7L21hcmtkb3dufX0yMDIwLTA2LTE2IDA0OjM3OjEyU2VydmljZVN0YWNrMjAyMC0wNi0xNiAwNDozNzoxMlNlcnZpY2VTdGFjawAAAAAgdG8gZm9yY2UgYSBwbGFpbi10ZXh0IHJlc3BvbnNlIHdpdGg6DQoNCmBgYA0Ke3sgdmFyIHJlc3BvbnNlID0gY29udGVudC5ldmFsVGVtcGxhdGUoe3VzZTp7cGx1Z2luczonTWFya2Rvd25TY3JpcHRQbHVnaW4nfX0pIH19DQp7eyByZXNwb25zZSB8PiByZXR1cm4oeyBjb250ZW50VHlwZTondGV4dC9wbGFpbicgfSkgfX0NCmBgYA0KDQpUaGUgcHJldmlldyBBUEkgYWJvdmUgaXMgd2hhdCBwcm92aWRlcyB0aGlzIEJsb2cncyBMaXZlIFByZXZpZXcgZmVhdHVyZSB3aGVyZSBpdCB3aWxsIHJlbmRlciBhbnkgI1NjcmlwdCBwcm92aWRlZCBpbiB0aGUgDQoqKmNvbnRlbnQqKiBRdWVyeSBTdHJpbmcgb3IgSFRUUCBQb3N0IEZvcm0gRGF0YSwgZS5nOg0KDQogLSBbL3ByZXZpZXc/Y29udGVudD17ezEwfHRpbWVzfHNlbGVjdDp7cG93KGluZGV4LDIpfSx9fV0oL3ByZXZpZXc/Y29udGVudD17ezEwfHRpbWVzfHNlbGVjdDp7cG93KGluZGV4LDIpfSx9fSkNCg0KV2hpY2ggcmVuZGVycyB0aGUgcGxhaW4gdGV4dCByZXNwb25zZToNCg0KICAgIDAsMSw0LDksMTYsMjUsMzYsNDksNjQsODEsDQoNCiMjIyAvX3VzZXIvYXBpIFBhZ2UNCg0KPiBVc2FnZTogL1x7dXNlcn0vYXBpDQoNClRoZSBbL191c2VyL2FwaS5odG1sXShodHRwczovL2dpdGh1Yi5jb20vc2hhcnAtYXBwcy9ibG9nL2Jsb2IvbWFzdGVyL2FwcC9fdXNlci9hcGkuaHRtbCkgQVBJIHBhZ2Ugc2hvd3MgYW4gZXhhbXBsZSBvZiBob3cgZWFzeSBpdCBpcyB0byANCmNyZWF0ZSBkYXRhLWRyaXZlbiBBUElzIHdoZXJlIHlvdSBjYW4gbGl0ZXJhbGx5IHJldHVybiB0aGUgcmVzcG9uc2Ugb2YgYW4gU1FMIHF1ZXJ5IGJ5IGNhbGxpbmcgdGhlIGBkYlNlbGVjdGAgZmlsdGVyIGFuZCByZXR1cm5pbmcgdGhlIHJlc3VsdHMgd2l0aDoNCg0KYGBgDQp7eyBgU0VMRUNUICogDQogICAgICBGUk9NIFBvc3QgcCBJTk5FUiBKT0lOIFVzZXJJbmZvIHUgb24gcC5DcmVhdGVkQnkgPSB1LlVzZXJOYW1lIA0KICAgICBXSEVSRSBVc2VyTmFtZSA9IEB1c2VyIA0KICAgIE9SREVSIEJZIHAuQ3JlYXRlZCBERVNDYCANCiAgIHw+IGRiU2VsZWN0KHsgdXNlciB9KQ0KICAgfD4gcmV0dXJuIH19DQpgYGANCg0KVGhlICoqdXNlcioqIGFyZ3VtZW50IGlzIHBvcHVsYXRlZCBhcyBhIHJlc3VsdCBvZiBkeW5hbWljIHJvdXRlIGZyb20gdGhlIGBfdXNlcmAgZGlyZWN0b3J5IG5hbWUgd2hpY2ggd2lsbCBsZXQgeW91IHZpZXcgYWxsIA0KW0BTZXJ2aWNlU3RhY2tdKC9TZXJ2aWNlU3RhY2spIHBvc3RzIHdpdGg6DQoNCiAtIFsvU2VydmljZVN0YWNrL2FwaV0oL1NlcnZpY2VTdGFjay9hcGkpDQoNCldoaWNoIGFsc28gYmVuZWZpdHMgZnJvbSBTZXJ2aWNlU3RhY2sncyBtdWx0aXBsZSBmb3JtYXRzIHdoZXJlIHRoZSBzYW1lIEFQSSBjYW4gYmUgcmV0dXJuZWQgaW46DQoNCiAtIFsvU2VydmljZVN0YWNrL2FwaT9mb3JtYXQ9anNvbl0oL1NlcnZpY2VTdGFjay9hcGk/Zm9ybWF0PWpzb24pDQogLSBbL1NlcnZpY2VTdGFjay9hcGk/Zm9ybWF0PWNzdl0oL1NlcnZpY2VTdGFjay9hcGk/Zm9ybWF0PWNzdikNCiAtIFsvU2VydmljZVN0YWNrL2FwaT9mb3JtYXQ9eG1sXSgvU2VydmljZVN0YWNrL2FwaT9mb3JtYXQ9eG1sKQ0KIC0gWy9TZXJ2aWNlU3RhY2svYXBpP2Zvcm1hdD1qc3ZdKC9TZXJ2aWNlU3RhY2svYXBpP2Zvcm1hdD1qc3YpDQoNCldoaWNoIHRoYW5rcyB0byB0aGUgbGl2ZSBkZXZlbG9wbWVudCB3b3JrZmxvdyBwcm92aWRlcyB0aGUgbW9zdCBwcm9kdWN0aXZlIGRldmVsb3BtZW50IGV4cGVyaWVuY2UgdG8gcmFwaWRseSBkZXZlbG9wIFdlYiBBUElzIG9yIHBlcmZvcm0gY29tbW9uIA0KdGFza3MgbGlrZSB2aWV3aW5nIGFkaG9jIFNRTCBxdWVyaWVzIGluIEV4Y2VsIHdoaWNoIGNhbiBiZSBmdXJ0aGVyIG1hbmlwdWxhdGVkIHVzaW5nIHRoZSANCltMSU5RLWxpa2UgZXhwcmVzc2l2ZW5lc3NdKGh0dHBzOi8vc2hhcnBzY3JpcHQubmV0L2xpbnEvcmVzdHJpY3Rpb24tb3BlcmF0b3JzKSBhbmQgd3Jpc3QtZnJpZW5kbHkgZmlsdGVycyBhdmFpbGFibGUgaW4gI1NjcmlwdC4NCg0KIyMjIC9wb3N0cy9fc2x1Zy9hcGkgUGFnZQ0KDQo+IFVzYWdlOiAgL3Bvc3RzL1x7c2x1Z30vYXBpDQoNClRoZSBbL3Bvc3RzL19zbHVnL2FwaS5odG1sXShodHRwczovL2dpdGh1Yi5jb20vc2hhcnAtYXBwcy9ibG9nL2Jsb2IvbWFzdGVyL2FwcC9wb3N0cy9fc2x1Zy9hcGkuaHRtbCkgcGFnZSBzaG93cyBhbiBleGFtcGxlIG9mIHVzaW5nIHRoZSANCmBodHRwUmVzdWx0YCBmaWx0ZXIgdG8gcmV0dXJuIGEgY3VzdG9tIEhUVFAgUmVzcG9uc2Ugd2hlcmUgaWYgdGhlIHBvc3Qgd2l0aCB0aGUgc3BlY2lmaWVkIHNsdWcgZG9lcyBub3QgZXhpc3QgaXQgd2lsbCByZXR1cm4gYSANCmA0MDQgUG9zdCB3YXMgbm90IGZvdW5kYCBIVFRQIFJlc3BvbnNlOg0KDQpgYGANCnt7IGBTRUxFQ1QgKiANCiAgICAgIEZST00gUG9zdCBwIElOTkVSIEpPSU4gVXNlckluZm8gdSBvbiBwLkNyZWF0ZWRCeSA9IHUuVXNlck5hbWUgDQogICAgIFdIRVJFIFNsdWcgPSBAc2x1ZyANCiAgICAgT1JERVIgQlkgcC5DcmVhdGVkIERFU0NgIA0KICAgfD4gZGJTaW5nbGUoeyBzbHVnIH0pDQogICB8PiB0byA9PiBwb3N0IA0KfX0NCnt7IHBvc3QgPz8gaHR0cFJlc3VsdCh7IHN0YXR1czo0MDQsIHN0YXR1c0Rlc2NyaXB0aW9uOidQb3N0IHdhcyBub3QgZm91bmQnIH0pIA0KICAgfD4gcmV0dXJuIH19DQpgYGANCg0KVGhlICoqaHR0cFJlc3VsdCoqIGZpbHRlciByZXR1cm5zIGEgU2VydmljZVN0YWNrIGBIdHRwUmVzdWx0YCB3aGljaCBhbGxvd3MgZm9yIHRoZSBmb2xsb3dpbmcgY3VzdG9taXphdGlvbidzOg0KDQpgYGBjc2hhcnANCmh0dHBSZXN1bHQoeyANCiAgc3RhdHVzOiAgICAgICAgICAgIDQwNCwNCiAgc3RhdHVzOiAgICAgICAgICAgICdOb3RGb3VuZCcgLy8gY2FuIGFsc28gdXNlIC5ORVQgSHR0cFN0YXR1c0NvZGUgZW51bSBuYW1lDQogIHN0YXR1c0Rlc2NyaXB0aW9uOiAnUG9zdCB3YXMgbm90IGZvdW5kJywNCiAgcmVzcG9uc2U6ICAgICAgICAgIHBvc3QsDQogIGZvcm1hdDogICAgICAgICAgICAnanNvbicsDQogIGNvbnRlbnRUeXBlOiAgICAgICAnYXBwbGljYXRpb24vanNvbicsDQogICdYLVBvd2VyZWQtQnknOiAgICAnU2VydmljZVN0YWNrIFRlbXBsYXRlcycsDQp9KSANCmBgYA0KDQpBbnkgb3RoZXIgYXJndW1lbnRzIGxpa2UgJ1gtUG93ZXJlZC1CeScgYXJlIHJldHVybmVkIGFzIEhUVFAgUmVzcG9uc2UgSGVhZGVycy4gDQoNClRoaXMgYmVoYXZlcyBzaW1pbGFybHkgdG8gY3VzdG9taXppbmcgYSByZXNwb25zZSB3aXRoIHJldHVybiBhcmd1bWVudHM6DQoNCmBgYA0Ke3sgcG9zdCB8PiByZXR1cm4oeyBmb3JtYXQ6J2pzb24nLCAnWC1Qb3dlcmVkLUJ5JzonU2VydmljZVN0YWNrICNTY3JpcHQnIH0pIH19DQpgYGANCg0KVXNpbmcgdGhlIGV4cGxpY2l0IGh0dHBSZXN1bHQgZmlsdGVyIGlzIHVzZWZ1bCBmb3IgcmV0dXJuaW5nIGEgY3VzdG9tIEhUVFAgUmVzcG9uc2UgdGhhdCBkb2Vzbid0IGhhdmUgYSBSZXNwb25zZSBCb2R5LCBlLmcuIHRoZSAqKk5ldyBQb3N0KiogcGFnZSANCnVzZXMgYGh0dHBGaWx0ZXJgIHRvIA0KW3JlZGlyZWN0IGJhY2sgdG8gdGhlIFVzZXJzIHBvc3RzIHBhZ2VdKGh0dHBzOi8vZ2l0aHViLmNvbS9zaGFycC1hcHBzL2Jsb2cvYmxvYi9lOGJiNzI0OTE5MmM1Nzk3MzQ4Y2VkMDkxYWQ1ZmQ0MzRkYjlhNjE5L2FwcC9wb3N0cy9uZXcuaHRtbCNMMzMpIA0KYWZ0ZXIgdGhleSd2ZSBzdWNjZXNzZnVsbHkgY3JlYXRlZCBhIG5ldyBQb3N0Og0KDQpgYGANCnt7I2lmIHN1Y2Nlc3N9fQ0KICAgIHt7IGh0dHBSZXN1bHQoeyBzdGF0dXM6MzAxLCBMb2NhdGlvbjpgLyR7dXNlck5hbWV9YCB9KSB8PiByZXR1cm4gfX0NCnt7L2lmfX0NCmBgYA0KDQpGb3IgbW9yZSBleGFtcGxlcyBhbmQgaW5mbyBvbiBBUEkgUGFnZXMgY2hlY2tvdXQgdG8gdGhlIFtBUEkgUGFnZXMgZG9jc10oaHR0cHM6Ly9zaGFycHNjcmlwdC5uZXQvZG9jcy9hcGktcGFnZXMpLg0KDQp7ey9tYXJrZG93bn19MjAyMC0wNi0xNiAwNDozNzoxMlNlcnZpY2VTdGFjazIwMjAtMDYtMTYgMDQ6Mzc6MTJTZXJ2aWNlU3RhY2sNAAAAAQcPAAcPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsWYECgAvL+IFMyUzJWR5bmFtaWMtYXBpLXBhZ2VzRHluYW1pYyBBUEkgUGFnZXN7eyNtYXJrZG93bn19DQpJbiBhZGRpdGlvbiB0byBwcm92aWRpbmcgYSBwcm9kdWN0aXZlIGR5bmFtaWMgbGFuZ3VhZ2UgZm9yIGdlbmVyYXRpbmcgSFRNTCBwYWdlcywgVGVtcGxhdGUgUGFnZXMgY2FuIGFsc28gYmUgdXNlZCB0byByYXBpZGx5IGRldmVsb3AgV2ViIEFQSXMgDQp3aGljaCBjYW4gdXRpbGl6ZSBbZHluYW1pYyBwYWdlIHJvdXRpbmddKC9wb3N0cy9wYWdlLWJhc2VkLXJvdXRpbmcpIHRvIGVhc2lseSBjcmVhdGUgZGF0YS1kcml2ZW4gSlNPTiBBUElzIHVzaW5nIG9wdGltYWwgcHJldHR5IFVSTHMsIGluIA0KcmVhbC10aW1lIHdpdGhvdXQgYW55IEMjIGNsYXNzZXMgb3IgY29tcGlsYXRpb24gaW4gc2lnaHQhDQoNClRoZSBvbmx5IGRpZmZlcmVuY2UgYmV0d2VlbiBhIFRlbXBsYXRlIFBhZ2UgdGhhdCBnZW5lcmF0ZXMgSFRNTCBvciBhIFRlbXBsYXRlIFBhZ2UgdGhhdCByZXR1cm5zIGFuIEFQSSBSZXNwb25zZSBpcyB0aGF0IEFQSSBwYWdlcyB1c2UgdGhlIA0KKipyZXR1cm4qKiBmaWx0ZXIgdG8gcmV0dXJuIGEgdmFsdWUuIA0KDQpFLmcuIFRvIGNyZWF0ZSBhICoqSGVsbG8gV29ybGQqKiBDIyBTZXJ2aWNlU3RhY2sgU2VydmljZSB5b3Ugd291bGQgdHlwaWNhbGx5IGNyZWF0ZSBhIFJlcXVlc3QgRFRPLCBSZXNwb25zZSBEVE8gYW5kIGEgU2VydmljZSBpbXBsZW1lbnRhdGlvbjoNCg0KDQpgYGANCltSb3V0ZSgiL2hlbGxvL3tOYW1lfSIpXQ0KcHVibGljIGNsYXNzIEhlbGxvIDogSVJldHVybjxIZWxsb1Jlc3BvbnNlPg0Kew0KICAgIHB1YmxpYyBzdHJpbmcgTmFtZSB7IGdldDsgc2V0OyB9DQp9DQpwdWJsaWMgY2xhc3MgSGVsbG9SZXNwb25zZQ0Kew0KICAgIHB1YmxpYyBzdHJpbmcgUmVzdWx0IHsgZ2V0OyBzZXQ7IH0NCn0NCnB1YmxpYyBjbGFzcyBIZWxsb1NlcnZpY2UgOiBTZXJ2aWNlDQp7DQogICAgcHVibGljIG9iamVjdCBBbnkoSGVsbG8gcmVxdWVzdCkgPT4gJCJIZWxsbywge3JlcXVlc3QuTmFtZX0hIjsNCn0NCmBgYA0KDQojIyMgL2hlbGxvIEFQSSBQYWdlDQoNCj4gVXNhZ2U6IC9oZWxsby9ce25hbWV9DQoNCkFuIEFQSSB3aGljaCByZXR1cm5zIHRoZSBzYW1lIHdpcmUgcmVzcG9uc2UgYXMgYWJvdmUgY2FuIGJlIGltcGxlbWVudGVkIGluIEFQSSBQYWdlcyBieSBjcmVhdGluZyBhIHBhZ2UgYXQgDQpbL2hlbGxvL19uYW1lL2luZGV4Lmh0bWxdKGh0dHBzOi8vZ2l0aHViLmNvbS9zaGFycC1hcHBzL2Jsb2cvYmxvYi9tYXN0ZXIvYXBwL2hlbGxvL19uYW1lL2luZGV4Lmh0bWwpIA0Kd2l0aCB0aGUgY29udGVudHM6DQoNCmBgYA0Ke3sgeyByZXN1bHQ6IGBIZWxsbywgJHtuYW1lfSFgIH0gfD4gcmV0dXJuIH19DQpgYGANCg0KV2hpY2ggc3VwcG9ydHMgdGhlIHNhbWUgY29udGVudCBuZWdvdGlhdGlvbiBhcyBhIFNlcnZpY2VTdGFjayBTZXJ2aWNlIHdoZXJlIGNhbGxpbmcgaXQgaW4gYSBicm93c2VyIHdpbGwgZ2VuZXJhdGUgYSANCltodW1hbi1mcmllbmRseSBIVE1MIFBhZ2VdKGh0dHA6Ly9kb2NzLnNlcnZpY2VzdGFjay5uZXQvaHRtbDVyZXBvcnRmb3JtYXQpOg0KDQogLSBbL2hlbGxvL1dvcmxkXSgvaGVsbG8vV29ybGQpDQoNCldoZXJlIGFzIGNhbGxpbmcgaXQgd2l0aCBhIEpTT04gSFRUUCBjbGllbnQgY29udGFpbmluZyBgQWNjZXB0OiBhcHBsaWNhdGlvbi9qc29uYCBIVFRQIEhlYWRlciBvciB3aXRoIGEgYD9mb3JtYXQ9anNvbmAgcXVlcnkgc3RyaW5nIHdpbGwgDQpyZXR1cm4gdGhlIEFQSSByZXNwb25zZSBpbiB0aGUgSlNPTiBGb3JtYXQ6DQoNCiAtIFsvaGVsbG8vV29ybGQ/Zm9ybWF0PWpzb25dKC9oZWxsby9Xb3JsZD9mb3JtYXQ9anNvbikNCg0KQWx0ZXJuYXRpdmVseSB5b3UgY2FuIGZvcmNlIGEgSlNPTiBSZXNwb25zZSBieSBzcGVjaWZ5aW5nIGl0IHdpdGg6DQoNCmBgYA0Ke3sgeyByZXN1bHQ6IGBIZWxsbywgJHtuYW1lfSFgIH0gfD4gcmV0dXJuKHsgZm9ybWF0OiAnanNvbicgfSkgfX0gDQovLyBFcXVpdmFsZW50IHRvOg0Ke3sgeyByZXN1bHQ6IGBIZWxsbywgJHtuYW1lfSFgIH0gfD4gcmV0dXJuKHsgY29udGVudFR5cGU6ICdhcHBsaWNhdGlvbi9qc29uJyB9KSB9fQ0KYGBgDQoNCiMjIyAvcHJldmlldyBBUEkgUGFnZQ0KDQo+IFVzYWdlOiAvcHJldmlldz9jb250ZW50PVx7dGVtcGxhdGVzfQ0KDQpUaGUgWy9wcmV2aWV3Lmh0bWxdKGh0dHBzOi8vZ2l0aHViLmNvbS9zaGFycC1hcHBzL2Jsb2cvYmxvYi9tYXN0ZXIvYXBwL3ByZXZpZXcuaHRtbCkgcGFnZSB1c2VzIHRoaXMAAAAIDQAAAAIEnAAGxgScAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMQbBgsAFxeBhx0zJTMlYWJvdXRBYm91dHt7I21hcmtkb3dufX0NClRoaXMgQmxvZyBBcHAgZGVtb25zdHJhdGVzIHNvbWUgb2YgdGhlIGNhcGFiaWxpdGllcyBpbiBbU2VydmljZVN0YWNrIFdlYiBBcHBzXShodHRwczovL3NoYXJwc2NyaXB0Lm5ldC9kb2NzL3NoYXJwLWFwcHMpIC0gYW4gZXhjaXRpbmcgcmVhbC10aW1lIA0KZGV2ZWxvcG1lbnQgbW9kZWwgZm9yIGRldmVsb3BpbmcgLk5FVCBDb3JlIEFwcHMgd2hlcmUgZW50aXJlIFdlYiBBcHBzIGNhbiBiZSBkZXZlbG9wZWQgd2l0aGluIGEgbGl2ZSBob3QtcmVsb2FkIGV4cGVyaWVuY2Ugd2l0aG91dCBhbnkgY29tcGlsYXRpb24sIA0KYnVpbGQgdG9vbHMsIGRlcGVuZGVuY2llcywgSURFcyBvciBhbnkgQyMgc291cmNlIGNvZGUgbmVjZXNzYXJ5IGJ5IHVzaW5nIHRoZSBwb3dlcmZ1bCBhbmQgdXNlci1mcmllbmRseSANCltTZXJ2aWNlU3RhY2sgI1NjcmlwdCBsYW5ndWFnZV0oaHR0cHM6Ly9zaGFycHNjcmlwdC5uZXQvKSBhbmQgaXRzIGNvbXByZWhlbnNpdmUgYnVpbHQtaW4gZnVuY3Rpb25hbGl0eS4NCg0KIyMAAAALkjcFCgAxMaMjMyUzJXBhZ2UtYmFzZWQtcm91dGluZ1BhZ2UgYmFzZWQgcm91dGluZ3t7I21hcmtkb3dufX0NClRlbXBsYXRlIFBhZ2VzIHN1cHBvcnRzIGNvbnZlbnRpb25hbCBwYWdlLWJhc2VkIHJvdXRlcyB3aGVyZSB0aGUgbmFtZSBvZiBlYWNoIHBhZ2UgY2FuIGJlIHJlcXVlc3RlZCB3aXRoIG9yIHdpdGhvdXQgaXRzICoqLmh0bWwqKiBleHRlbnNpb246DQoNCnwgcGF0aCB8IHBhZ2UgfA0KLS0tLS0tIHwgLS0tLSB8DQpbL2RiXSgvZGIpIHwgIHwNClsvZGIuaHRtbF0oL2RiLmh0bWwpIHwgWy9kYi5odG1sXShodHRwczovL2dpdGh1Yi5jb20vc2hhcnAtYXBwcy9ibG9nL2Jsb2IvbWFzdGVyL2FwcC9kYi5odG1sKSB8DQpbL3Bvc3RzL25ld10oL3Bvc3RzL25ldykgfCB8DQpbL3Bvc3RzL25ldy5odG1sXSgvcG9zdHMvbmV3Lmh0bWwpIHwgWy9wb3N0cy9uZXcuaHRtbF0oaHR0cHM6Ly9naXRodWIuY29tL3NoYXJwLWFwcHMvYmxvZy9ibG9iL21hc3Rlci9hcHAvcG9zdHMvbmV3Lmh0bWwpDQoNCmFuZCB0aGUgZGVmYXVsdCByb3V0ZSAqKi8qKiBtYXBzIHRvIHRoZSBgaW5kZXguaHRtbGAgaW4gZWFjaCBkaXJlY3RvcnkgaWYgaXQgZXhpc3RzLCBlLmc6DQoNCnwgcGF0aCB8IHBhZ2UgfA0KLS0tLS0tIHwgLS0tLSB8DQpbL10oLykgfCBbL2luZGV4Lmh0bWxdKGh0dHBzOi8vZ2l0aHViLmNvbS9zaGFycC1hcHBzL2Jsb2cvYmxvYi9tYXN0ZXIvYXBwL2luZGV4Lmh0bWwpIHwNCg0KTnV4dC1saWtlIFtEeW5hbWljIFJvdXRlc10oaHR0cHM6Ly9udXh0anMub3JnL2d1aWRlL3JvdXRpbmcjZHluYW1pYy1yb3V0ZXMpIGNhbiBhbHNvIGJlIHVzZWQgd2hlcmUgYW55ICoqZmlsZSoqIG9yICoqZGlyZWN0b3J5KiogbmFtZXMgDQpwcmVmaXhlZCB3aXRoIGFuIF8gKip1bmRlcnNjb3JlKiogYWxsb3dzIGZvciBkeW5hbWljIHdpbGRjYXJkIHBhdGhzIHdpdGggdGhlIG1hdGNoaW5nIHBhdGggY29tcG9uZW50IGFsc28gYXNzaWduZWQgdG8gdGhlIGFyZ3VtZW50cyBuYW1lOg0KDQp8IHBhdGggfCBwYWdlIHwgYXJndW1lbnRzIHwNCi0tLS0tLSB8IC0tLS0gfCAtLS0tLS0tLS0gfA0KWy9TZXJ2aWNlU3RhY2tdKC8pIHwgWy9fdXNlci9pbmRleC5odG1sXShodHRwczovL2dpdGh1Yi5jb20vc2hhcnAtYXBwcy9ibG9nL2Jsb2IvbWFzdGVyL2FwcC9fdXNlci9pbmRleC5odG1sKSB8IHVzZXI9U2VydmljZVN0YWNrIHwNClsvcG9zdHMvbWFya2Rvd24tZXhhbXBsZV0oL3Bvc3RzL21hcmtkb3duLWV4YW1wbGUpIHwgWy9wb3N0cy9fc2x1Zy9pbmRleC5odG1sXShodHRwczovL2dpdGh1Yi5jb20vc2hhcnAtYXBwcy9ibG9nL2Jsb2IvbWFzdGVyL2FwcC9wb3N0cy9fc2x1Zy9pbmRleC5odG1sKSB8IHNsdWc9bWFya2Rvd24tZXhhbXBsZSB8DQpbL3Bvc3RzL21hcmtkb3duLWV4YW1wbGUvZWRpdF0oL3Bvc3RzL21hcmtkb3duLWV4YW1wbGUvZWRpdCkgfCBbL3Bvc3RzL19zbHVnL2VkaXQuaHRtbF0oaHR0cHM6Ly9naXRodWIuY29tL3NoYXJwLWFwcHMvYmxvZy9ibG9iL21hc3Rlci9hcHAvcG9zdHMvX3NsdWcvZWRpdC5odG1sKSB8IHNsdWc9bWFya2Rvd24tZXhhbXBsZSB8DQoNCiMjIyBMYXlvdXQgYW5kIHBhcnRpYWwgcmVjb21tZW5kZWQgbmFtaW5nIGNvbnZlbnRpb25zDQoNCkFzIHRoZSAqKl8gdW5kZXJzY29yZSoqIHByZWZpeCBmb3IgZGVjbGFyaW5nIHdpbGRjYXJkIHBhZ2VzIGlzIGFsc28gd2hhdCBpcyB1c2VkIHRvIGRlY2xhcmUgImhpZGRlbiIgcGFnZXMsIHRvIGRpc2FtYmlndWF0ZSB0aGVtIGZyb20gaGlkZGVuIA0KcGFydGlhbHMgYW5kIGxheW91dHMsIHRoZSByZWNvbW1lbmRhdGlvbiBpcyB0byBoYXZlIHRoZW0gaW5jbHVkZSBgbGF5b3V0YCBhbmQgYHBhcnRpYWxgIHRoZWlyIG5hbWUsIGkuZToNCg0KIC0gX2xheW91dC5odG1sDQogLSBfYWx0LWxheW91dC5odG1sDQogLSBfbWVudS1wYXJ0aWFsLmh0bWwNCg0KUGFnZXMgd2l0aCBgbGF5b3V0YCBvciBgcGFydGlhbGAgaW4gdGhlaXIgbmFtZSBhcmUgaGlkZGVuIGFuZCBpZ25vcmVkIGluIHdpbGRjYXJkIHBhdGggcmVzb2x1dGlvbi4NCg0KSWYgeW91IGZvbGxvdyB0aGUgcmVjb21tZW5kZWQgYF97bmFtZX0tcGFydGlhbC5odG1sYCBuYW1pbmcgY29udmVudGlvbiB5b3Ugd2lsbCBhbHNvIGJlIGFibGUgdG8gcmVmZXJlbmNlIHBhcnRpYWxzIHVzaW5nIGp1c3QgdGhlaXIgbmFtZSwgaS5lOg0KDQpgYGANCnt7ICdtZW51JyB8PiBwYXJ0aWFsIH19ICAgICAgICAgIC8vIEVxdWl2YWxlbnQgdG86DQp7eyAnX21lbnUtcGFydGlhbCcgfD4gcGFydGlhbCB9fQ0KYGBgDQoNCnt7L21hcmtkb3dufX0yMDIwLTA2LTE2IDA0OjM3OjEyU2VydmljZVN0YWNrMjAyMC0wNi0xNiAwNDozNzoxMlNlcnZpY2VTdGFjawAAAAwjIFVsdGltYXRlIFNpbXBsaWNpdHkNCg0KVGhpcyBlbGltaW5hdGVzIG11Y2ggb2YgdGhlIGNvbXBsZXhpdHkgaW5oZXJlbnQgaW4gZGV2ZWxvcGluZyAuTkVUIFdlYiBBcHBsaWNhdGlvbnMgd2hpY2ggYnkgdGhlaXIgbmF0dXJlIHJlc3VsdHMgaW4gaGlnaGx5IGN1c3RvbWl6YWJsZSBXZWIgQXBwcyANCndoZXJlIHRoZWlyIGVudGlyZSBmdW5jdGlvbmFsaXR5IGNhbiBiZSBtb2RpZmllZCBpbiByZWFsLXRpbWUgd2hpbHN0IHRoZSBBcHAgaXMgcnVubmluZywgd2hpY2ggaXMgc2ltcGxlIGVub3VnaCB0byBiZSBlbmhhbmNlZCBieSBub24tZGV2ZWxvcGVycyANCmxpa2UgRGVzaWduZXJzIGFuZCBDb250ZW50IENyZWF0b3JzIGNvdXJ0ZXN5IG9mIGl0cyBhcHByb2FjaGFibGUgW0hhbmRsZWJhcnMtbGlrZV0oaHR0cHM6Ly9zaGFycHNjcmlwdC5uZXQvZG9jcy9ibG9ja3MpIGFuZCBmYW1pbGlhciANCltKYXZhU2NyaXB0IHN5bnRheF0oaHR0cHM6Ly9zaGFycHNjcmlwdC5uZXQvZG9jcy9leHByZXNzaW9uLXZpZXdlciNleHByZXNzaW9uPW1hcChyYW5nZSgxJTJDY291bnQpJTJDJTIweCUyMCUzRCUzRSUyMHglMjAqJTIweCkmY291bnQ9NSkuDQoNCkNvbXBpbGVkIEFwcHMgY2FuIGhhdmUgYSBwcm9oaWJpdGl2ZWx5IGxhcmdlIGJhcnJpZXIgdG8gZW50cnkgd2hlcmUgYW55IG1vZGlmaWNhdGlvbiBvZnRlbiByZXF1aXJlcyBkb3dubG9hZGluZyBzb3VyY2UgY29kZSBzZXBhcmF0ZWx5LCBzZXR0aW5nIA0KdXAgYSBtYXRjaGluZyBkZXZlbG9wbWVudCBlbnZpcm9ubWVudCB3aXRoIGFwcHJvcHJpYXRlIGV4dGVuc2lvbnMgYW5kIGNvcnJlY3QgdmVyc2lvbnMgYW5kIG5vbiBjdXJzb3J5IGxldmVsIG9mIGV4cGVyaWVuY2Ugd2l0aCB0aGVpciBjaG9zZW4gDQpsYW5ndWFnZSwgZnJhbWV3b3JrcywgYnVpbGQgdG9vbHMgYW5kIG90aGVyIHBsYXRmb3JtIGlkaW9zeW5jcmFzaWVzLiANCg0KQnkgY29udHJhc3QgV2ViIEFwcHMgcmVxdWlyZSBubyBkZXZlbG9wbWVudCBlbnZpcm9ubWVudCwgbm8gSURFJ3Mgb3IgYnVpbGQgdG9vbHMgd2l0aCBhbGwgc291cmNlIGNvZGUgYWxyZWFkeSBpbmNsdWRlZCBhcyBwYXJ0IG9mIHRoZSBBcHAgd2hpY2ggDQpjYW4gYmUgbW9kaWZpZWQgaW4gcmVhbC10aW1lIGJ5IGFueSB0ZXh0IGVkaXRvciB0byBpbnN0YW50bHkgdmlldyBjaGFuZ2VzIGFzIHRoZXkncmUgbWFkZS4gU28gQXBwcyBsaWtlIGh0dHA6Ly9yZWRpcy53ZWItYXBwLmlvIHdoaWNoIHByb3ZpZGUgYSANCnJpY2ggQWRtaW4gVUkgZm9yIHNlYXJjaGluZywgYnJvd3NpbmcgYW5kIG1vZGlmeWluZyBSZWRpcydzIGNvcmUgZGF0YSBzdHJ1Y3R1cmVzLCBjYW4gYmUgZWFzaWx5IGVuaGFuY2VkIGJ5IG1vZGlmeWluZyBhIHNpbmdsZSANCltpbmRleC5odG1sXShodHRwczovL2dpdGh1Yi5jb20vTmV0Q29yZVdlYkFwcHMvUmVkaXMvYmxvYi9tYXN0ZXIvYXBwL2luZGV4Lmh0bWwpIGF0IHRoZSBzYW1lIHRpbWUgYXMgdXNpbmcgdGhlIEFwcC4NCg0KIyMgQmxvZyBBcHAgRmVhdHVyZXMNCg0KVGhpcyBbL0Jsb2ddKGh0dHBzOi8vZ2l0aHViLmNvbS9zaGFycC1hcHBzL2Jsb2cvdHJlZS9tYXN0ZXIvYXBwKSBXZWIgQXBwIGlzIGFub3RoZXIgZXhhbXBsZSBvZiBlbmNhcHN1bGF0aW5nIHVzZWZ1bCBmdW5jdGlvbmFsaXR5IGluIGEgDQpoaWdobHkgY3VzdG9taXphYmxlIC5ORVQgQ29yZSBXZWIgQXBwIHdoaWNoIHRvIG1heGltaXplIGFwcHJvYWNoYWJpbGl0eSBoYXMgbm8gQyMgc291cmNlIGNvZGUsIHBsdWdpbnMgYW5kIHVzZXMgbm8gSmF2YVNjcmlwdCBvciBDU1MgZnJhbWV3b3Jrcy4NClRoZSBkZXZlbG9wbWVudCBvZiB3aGljaCBlbmRlZCB1cCBiZWluZyBvbmUgb2YgdGhlIG1vc3QgZW5qb3lhYmxlIGV4cGVyaWVuY2VzIHdlJ3ZlIGhhZCBpbiByZWNlbnQgbWVtb3J5IHdoZXJlIGFsbCB0aGUgdXN1YWwgY29tcGxleGl0aWVzIG9mIA0KZGV2ZWxvcGluZyBhIEMjIFNlcnZlciBhbmQgbW9kZXJuIEpTIEFwcCBoYXMgYmVlbiBkaXNwZW5zZWQgYW5kIHlvdSBjYW4ganVzdCBmb2N1cyBvbiB0aGUgQXBwIHlvdSB3YW50IHRvIGNyZWF0ZSwgDQp1c2luZyBhIHBsYWluLXRleHQgZWRpdG9yIG9uIHRoZSBsZWZ0LCBhIGxpdmUgdXBkYXRpbmcgYnJvd3NlciBvbiB0aGUgcmlnaHQgYW5kIG5vdGhpbmcgZWxzZSB0byBpbnRlcnJ1cHQgeW91ciBmbG93Lg0KDQpBbnkgaW5mcmFzdHJ1Y3R1cmUgZGVwZW5kZW5jaWVzIGhhdmUgYWxzbyBiZWVuIGF2b2lkZWQgYnkgdXNpbmcgU1FMaXRlIGJ5IGRlZmF1bHQgd2hpY2ggaXMgDQpbYXV0b21hdGljYWxseSBjcmVhdGVkIGFuIHBvcHVsYXRlZCBvbiBmaXJzdCBydW5dKC9wb3N0cy93ZWItYXBwLWN1c3RvbWl6YXRpb25zKSBpZiBubyBkYXRhYmFzZSBleGlzdHMsIG9yIGlmIHByZWZlcnJlZCBjYW4gYmUgDQpbY2hhbmdlZCB0byB1c2UgYW55IG90aGVyIHBvcHVsYXIgUkRCTVNdKGh0dHBzOi8vc2hhcnBzY3JpcHQubmV0L2RvY3Mvc2hhcnAtYXBwcyNtdWx0aS1wbGF0Zm9ybS1jb25maWd1cmF0aW9ucykgdXNpbmcganVzdCBjb25maWcuDQoNCiMjIyBNdWx0aSBVc2VyIEJsb2dnaW5nIFBsYXRmb3JtDQoNCkFueSBudW1iZXIgb2YgdXNlcnMgY2FuIFNpZ24gSW4gdmlhIFR3aXR0ZXIgYW5kIHB1Ymxpc2ggY29udGVudCB1bmRlciB0aGVpciBUd2l0dGVyIFVzZXJuYW1lIHdoZXJlIG9ubHkgdGhleSdsbCBiZSBhYmxlIHRvIG1vZGlmeSB0aGVpciBvd24gQ29udGVudC4gDQpTZXR0aW5nIHVwIFR3aXR0ZXIgaXMgYXMgZWFzeSBhcyBpdCBjYW4gYmUgd2hpY2gganVzdCByZXF1aXJlcyBtb2RpZnlpbmcgdGhlIA0KW1R3aXR0ZXIgQXV0aCBDb25maWcgaW4gd2ViLnNldHRpbmdzXSgvcG9zdHMvd2ViLWFwcC1jdXN0b21pemF0aW9ucyNjdXN0b21pemFibGUtYXV0aC1wcm92aWRlcnMpIHdpdGggdGhlIFVSTCB3aGVyZSB0aGUgYmxvZyANCmlzIGhvc3RlZCBhbmQgdGhlIE9BdXRoIEtleXMgZm9yIHRoZSBUd2l0dGVyIE9BdXRoIEFwcCBjcmVhdGVkIGF0IGh0dHBzOi8vYXBwcy50d2l0dGVyLmNvbQ0KDQojIyMgUmljaCBDb250ZW50DQoNCldoaWxzdCBtb3N0IGJsb2dnaW5nIHBsYXRmb3JtcyBqdXN0IGVkaXQgc3RhdGljIHRleHQsIGVhY2ggUG9zdCBjb250ZW50IGhhcyBhY2Nlc3MgdG8gdGhlIHBvd2VyZnVsIGFuZCANCltTYW5kYm94ZWRdKGh0dHBzOi8vc2hhcnBzY3JpcHQubmV0L2RvY3Mvc2FuZGJveCkgZmVhdHVyZXMgaW4gaHR0cHM6Ly9zaGFycHNjcmlwdC5uZXQvIHdoaWNoIGNhbiBiZSB1c2VkIHRvIGNyZWF0ZSANCltMaXZlIERvY3VtZW50c10oL3Bvc3RzL2xpdmUtZG9jdW1lbnQtZXhhbXBsZSkgb3IgW1JlbmRlciBNYXJrZG93bl0oL3Bvc3RzL21hcmtkb3duLWV4YW1wbGUpIHdoaWNoIGlzIGl0c2VsZiBqdXN0IA0KW29uZSBvZiB0aGUgYXZhaWxhYmxlIGJsb2Nrc10oaHR0cHM6Ly9zaGFycHNjcmlwdC5uZXQvZG9jcy9ibG9ja3MjbWFya2Rvd24pIHdoZXJlIGl0IHdpbGwgcmVuZGVyIHRvIEhUTUwgYW55IGNvbnRlbnQgYmV0d2VlbiB0aGUgYG1hcmtkb3duYCBibG9ja3M6DQp7ey9tYXJrZG93bn19DQoNCjxwcmU+PGNvZGU+e+KAi3sjbWFya2Rvd259fQ0KIyMgTWFya2Rvd24gQ29udGVudA0Ke+KAi3svbWFya2Rvd259fTwvY29kZT48L3ByZT4NCg0KDQp7eyNtYXJrZG93bn19DQpCeSBkZWZhdWx0IHRoZSBbTWFya2RpZ10oaHR0cHM6Ly9naXRodWIuY29tL2x1bmV0LWlvL21hcmtkaWcpIGltcGxlbWVudGF0aW9uIGlzIHVzZWQgdG8gcmVuZGVyIE1hcmtkb3duIGJ1dCBjYW4gYWxzbyBiZSBjb25maWd1cmVkIHRvIHVzZSBhbiANClthbHRlcm5hdGUgTWFya2Rvd24gcHJvdmlkZXJdKC9wb3N0cy93ZWItYXBwLWN1c3RvbWl6YXRpb25zI2N1c3RvbWl6YWJsZS1tYXJrZG93bi1wcm92aWRlcnMpLg0KDQojIyMgUmljaCBNYXJrZG93biBFZGl0b3INCg0KVG8gbWFrZSBpdCBlYXN5IHRvIHJlY2FsbCBNYXJrZG93biBmZWF0dXJlcywgZWFjaCBDb250ZW50IGlzIGVxdWlwcGVkIHdpdGggYSBSaWNoIFRleHQgZWRpdG9yIGNvbnRhaW5pbmcgdGhlIG1vc3QgcG9wdWxhciBmb3JtYXR0aW5nIGNvbnRyb2xzIA0KYWxvbmcgd2l0aCBjb21tb24gc2hvcnQtY3V0cyAAAAAAZm9yIGVhY2ggZmVhdHVyZSwgZGlzY292ZXJhYmxlIGJ5IGhvdmVyaW5nIG92ZXIgZWFjaCBidXR0b246DQoNCiFbXShodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vU2VydmljZVN0YWNrL0Fzc2V0cy9tYXN0ZXIvaW1nL2xpdmVkZW1vcy9ibG9nL2VkaXRvci5wbmcpDQoNClRoZSBFZGl0b3IgYWxzbyBhZG9wdHMgcG9wdWxhciBiZWhhdmlvciBpbiBJREVzIHdoZXJlIGBUYWJgIGFuZCBgU0hJRlQrVGFiYCBjYW4gYmUgdXNlZCB0byBpbmRlbnQgYmxvY2tzIG9mIHRleHQgYW5kIGxpbmVzIGNhbiBiZSBjb21tZW50ZWQgd2l0aCANCmBDdHJsKy9gIG9yIGJsb2NrcyB3aXRoIGBDVFJMK1NISUZUKy9gLg0KDQpBbm90aGVyIG5pY2UgcHJvZHVjdGl2aXR5IHdpbiBpcyBiZWluZyBhYmxlIHRvIGBDVFJMK0NMSUNLYCBvbiBhbnkgQ29udGVudCB5b3UgY3JlYXRlZCB0byBuYXZpZ2F0ZSB0byBpdHMgRWRpdCBwYWdlLg0KDQojIyMgQXV0byBzYXZlZCBkcmFmdHMNCg0KVGhlIGNvbnRlbnQgaW4gZWFjaCBUZXh0IGBpbnB1dGAgYW5kIGB0ZXh0YXJlYWAgaXMgc2F2ZWQgdG8gYGxvY2FsU3RvcmFnZWAgb24gZWFjaCBrZXktcHJlc3Mgc28geW91IGNhbiBmcmVlbHkgcmVsb2FkIHBhZ2VzIGFuZCBuYXZpZ2F0ZSANCmFyb3VuZCB0aGUgc2l0ZSB3aGVyZSBhbGwgdW5wdWJsaXNoZWQgY29udGVudCB3aWxsIGJlIHByZXNlcnZlZCB1cG9uIHJldHVybi4gDQoNCldoZW4geW91IHdhbnQgdG8gcmV2ZXJ0IGJhY2sgdG8gdGhlIG9yaWdpbmFsIHB1Ymxpc2hlZCBjb250ZW50IHlvdSBjYW4gY2xlYXIgdGhlIHRleHQgYm94ZXMgYW5kIHJlbG9hZCB0aGUgcGFnZSB3aGljaCB3aWxsIGxvYWQgY29udGVudCBmcm9tIA0KdGhlIGRhdGFiYXNlIGluc3RlYWQgb2YgYGxvY2FsU3RvcmFnZWAuDQoNCiMjIyBTZXJ2ZXIgVmFsaWRhdGlvbg0KDQpUaGUgW25ldy5odG1sXShodHRwczovL2dpdGh1Yi5jb20vc2hhcnAtYXBwcy9ibG9nL2Jsb2IvbWFzdGVyL2FwcC9wb3N0cy9uZXcuaHRtbCkgYW5kIFtlZGl0Lmh0bWxdKGh0dHBzOi8vZ2l0aHViLmNvbS9zaGFycC1hcHBzL2Jsb2cvYmxvYi9tYXN0ZXIvYXBwL3Bvc3RzL19zbHVnL2VkaXQuaHRtbCkgcGFnZXMgc2hvd3MgZXhhbXBsZXMgb2YgcGVyZm9ybWluZyBzZXJ2ZXIgdmFsaWRhdGlvbiB3aXRoIFNlcnZpY2VTdGFjayAjU2NyaXB0Og0KDQpgYGANCnt7IGFzc2lnbkVycm9yQW5kQ29udGludWVFeGVjdXRpbmc6IGV4IH19DQoNCnt7ICdUaXRsZSBtdXN0IGJlIGJldHdlZW4gNSBhbmQgMjAwIGNoYXJhY3RlcnMnICAgICAgDQogICB8PiBvbmx5SWYobGVuZ3RoKHBvc3RUaXRsZSkgPCA1IHx8IGxlbmd0aChwb3N0VGl0bGUpID4gMjAwKSB8PiB0byA9PiB0aXRsZUVycm9yIH19DQp7eyAnQ29udGVudCBtdXN0IGJlIGJldHdlZW4gMjUgYW5kIDY0MDAwIGNoYXJhY3RlcnMnIA0KICAgfD4gb25seUlmKGxlbmd0aChjb250ZW50KSA8IDI1ICB8fCBsZW5ndGgoY29udGVudCkgPiA2NDAwMCkgfD4gdG8gPT4gY29udGVudEVycm9yIH19DQp7eyAnUG90ZW50aWFsbHkgbWFsaWNpb3VzIGNoYXJhY3RlcnMgZGV0ZWN0ZWQnICAgICAgIA0KICAgfD4gaWZOb3RFeGlzdHMoY29udGVudEVycm9yKSB8IG9ubHlJZihjb250YWluc1hzcyhjb250ZW50KSkgfD4gdG8gPT4gY29udGVudEVycm9yIH19DQpgYGANCg0KIVtdKGh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9TZXJ2aWNlU3RhY2svQXNzZXRzL21hc3Rlci9pbWcvbGl2ZWRlbW9zL2Jsb2cvc2VydmVyLXZhbGlkYXRpb24ucG5nKQ0KDQpGb3IgbW9yZSBpbmZvIHNlZSB0aGUgZG9jcyBvbiBbRXJyb3IgSGFuZGxpbmddKGh0dHBzOi8vc2hhcnBzY3JpcHQubmV0L2RvY3MvZXJyb3ItaGFuZGxpbmcpLg0KDQojIyMgTGl2ZSBQcmV2aWV3cw0KDQpDcmVhdGluZyBhbmQgUG9zdGluZyBjb250ZW50IGJlbmVmaXQgZnJvbSBMaXZlIFByZXZpZXdzIHdoZXJlIGl0cyByZW5kZXJlZCBvdXRwdXQgY2FuIGJlIHZpc3VhbGl6ZWQgaW4gcmVhbC10aW1lIGJlZm9yZSBpdCdzIHB1Ymxpc2hlZC4gDQoNCkFueSB0ZXh0YXJlYSBjYW4gZWFzaWx5IGJlIGVuaGFuY2VkIHRvIGVuYWJsZSBMaXZlIFByZXZpZXcgYnkgaW5jbHVkaW5nIHRoZSBgZGF0YS1saXZlcHJldmlld2AgYXR0cmlidXRlIHdpdGggdGhlIGVsZW1lbnQgd2hlcmUgdGhlIG91dHB1dCANCnNob3VsZCBiZSByZW5kZXJlZCBpbiwgZS5nOg0KDQogICAgPHRleHRhcmVhIGRhdGEtbGl2ZXByZXZpZXc9Ii5wcmV2aWV3Ij48L3RleHRhcmVhPg0KICAgIDxkaXYgY2xhc3M9InByZXZpZXciPjwvZGl2Pg0KDQpUaGUgaW1wbGVtZW50YXRpb24gb2Ygd2hpY2ggaXMgc3VycHJpc2luZ2x5IHNpbXBsZSB3aGVyZSB0aGUgSmF2YVNjcmlwdCBzbmlwcGV0IGluIA0KW2RlZmF1bHQuanNdKGh0dHBzOi8vZ2l0aHViLmNvbS9zaGFycC1hcHBzL2Jsb2cvYmxvYi9tYXN0ZXIvYXBwL2RlZmF1bHQuanMpIGJlbG93IGlzIHVzZWQgdG8gc2VuZCB0aGVpciBjb250ZW50IG9uIGVhY2ggY2hhbmdlOg0KDQpgYGANCi8vIEVuYWJsZSBMaXZlIFByZXZpZXcgb2YgbmV3IENvbnRlbnQNCnRleHRBcmVhcyA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoInRleHRhcmVhW2RhdGEtbGl2ZXByZXZpZXddIik7DQpmb3IgKGxldCBpID0gMDsgaSA8IHRleHRBcmVhcy5sZW5ndGg7IGkrKykgew0KICB0ZXh0QXJlYXNbaV0uYWRkRXZlbnRMaXN0ZW5lcigiaW5wdXQiLCBsaXZlcHJldmlldywgZmFsc2UpOw0KICBsaXZlcHJldmlldyh7IHRhcmdldDogdGV4dEFyZWFzW2ldIH0pOw0KfQ0KDQpmdW5jdGlvbiBsaXZlcHJldmlldyhlKSB7DQogIGxldCBlbCA9IGUudGFyZ2V0Ow0KICBsZXQgc2VsID0gZWwuZ2V0QXR0cmlidXRlKCJkYXRhLWxpdmVwcmV2aWV3Iik7DQoNCiAgaWYgKGVsLnZhbHVlLnRyaW0oKSA9PSAiIikgew0KICAgIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3Ioc2VsKS5pbm5lckhUTUwgPSAiPGRpdj5MaXZlIFByZXZpZXc8L2Rpdj4iOw0KICAgIHJldHVybjsNCiAgfQ0KDQogIGxldCBmb3JtRGF0YSA9IG5ldyBGb3JtRGF0YSgpOw0KICBmb3JtRGF0YS5hcHBlbmQoImNvbnRlbnQiLCBlbC52YWx1ZSk7DQoNCiAgZmV0Y2goIi9wcmV2aWV3Iiwgew0KICAgIG1ldGhvZDogInBvc3QiLA0KICAgIGJvZHk6IGZvcm1EYXRhDQogIH0pLnRoZW4oZnVuY3Rpb24ocikgeyByZXR1cm4gci50ZXh0KCk7IH0pDQogICAgLnRoZW4oZnVuY3Rpb24ocikgeyBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKHNlbCkuaW5uZXJIVE1MID0gcjsgfSk7DQp9DQpgYGANCg0KVG8gdGhlIFsvcHJldmlldy5odG1sXShodHRwczovL2dpdGh1Yi5jb20vc2hhcnAtYXBwcy9ibG9nL2Jsb2IvbWFzdGVyL2FwcC9wcmV2aWV3Lmh0bWwpIEFQSSBQYWdlIHdoaWNoIGp1c3QgcmVuZGVycyBhbmQgY2FwdHVyZXMgYW55IA0KVGVtcGxhdGUgQ29udGVudCBpdHMgc2VudCBhbmQgcmV0dXJucyB0aGUgb3V0cHV0Og0KDQpgYGANCnt7IGNvbnRlbnQgIHwgZXZhbFRlbXBsYXRlKHt1c2U6e3BsdWdpbnM6J01hcmtkb3duU2NyaXB0UGx1Z2luJ319KSB8PiB0byA9PnJlc3BvbnNlIH19DQp7eyByZXNwb25zZSB8IHJldHVybih7IGNvbnRlbnRUeXBlOid0ZXh0L3BsYWluJyB9KSB9fQ0KYGBgDQoNCkJ5IGRlZmF1bHQgdGhlIGBldmFsVGVtcGxhdGVgIGZpbHRlciByZW5kZXJzIGAgaW4gYSBuZXcgYFNjcmlwdENvbnRleHRgIHdoaWNoIGNhbiBiZSBjdXN0b21pemVkIHRvIHV0aWxpemUgYW55IGFkZGl0aW9uYWwgDQpgcGx1Z2luc2AsIGBmaWx0ZXJzYCBhbmQgYGJsb2Nrc2AgYXZhaWxhYmxlIGluIHRoZSBjb25maWd1cmVkIFtTaGFycFBhZ2VzRmVhdHVyZV0oaHR0cHM6Ly9zaGFycHNjcmlwdC5uZXQvZG9jcy9zaGFycC1wYWdlcyksIA0Kb3IgZm9yIGZ1bGwgYWNjZXNzIHlvdSBjYW4gdXNlIGB7dXNlOntjb250ZXh0OnRydWV9fWAgdG8gZXZhbHVhdGUgYW55IFRlbXBsYXRlIGNvbnRlbnQgdW5kZXIgdGhlIHNhbWUgY29udGV4dCB0aGF0IGBldmFsVGVtcGxhdGVgIGlzIHJ1biBvbi4NCg0Ke3svbWFya2Rvd259fTIwMjAtMDYtMTYgMDQ6Mzc6MTNTZXJ2aWNlU3RhY2syMDIwLTA2LTE2IDA0OjM3OjEzU2VydmljZVN0YWNr |
<!-- | |
title: blog.sqlite database | |
--> | |
<div class="page-blank"> | |
{{ `SELECT * FROM Post ORDER BY Modified DESC` | dbSelect | htmlDump({ caption: 'Post table', className: 'table-data db-post' }) }} | |
{{#noop remove noop block to also display UserInfo table}} | |
{{ `SELECT * FROM UserInfo ORDER BY Modified DESC` | dbSelect | htmlDump({ caption: 'UserInfo table', className: 'table-data' }) }} | |
{{/noop}} | |
<style> | |
#body { justify-content: left; } | |
.profile { display: none; } | |
.db-post td:nth-child(4) { | |
white-space: pre; | |
} | |
</style> | |
</div> |
body { | |
color: #212121; | |
padding: 0; | |
margin: 0; | |
font-family: 'Open Sans', sans-serif; | |
font-size: 1.4rem; | |
} | |
h1,h2,h3,h4,h5,h6 { | |
font-family: 'Special Elite', cursive; | |
font-family: 'Noticia Text', serif; | |
} | |
#title { margin:0; padding:.5em .5em 0 0; font-size: 1.5rem; text-align: right; font-weight: normal } | |
#body { display: flex; align-items: center; justify-content: center; } | |
a { text-decoration: none; color: #428bca; } | |
a:hover { text-decoration: underline; opacity: .8; } | |
.page { min-width: 880px; max-width: 880px; } | |
.page-edit { | |
width: 85%; | |
max-width: 1200px; | |
padding: 1em; | |
} | |
.profile { | |
position: absolute; | |
top: 50px; | |
right: 10px; | |
text-align: right; | |
margin: .3em .8em 0 0; | |
font-size: .8em; | |
} | |
#avatar { | |
display: flex; | |
} | |
#avatar img { | |
margin-left: 10px; | |
} | |
.profile a { | |
font-size: .8em; | |
} | |
.profile .info { | |
margin: .5em 0 0 0; | |
} | |
.profile strong { | |
padding: .3em; | |
} | |
.profile .links { | |
margin: 0 0 0 0; | |
} | |
.svg-icon { | |
vertical-align: bottom; | |
margin: 5px 0 0 0; | |
} | |
.avatar-sm img { | |
width: 42px; | |
height: 42px; | |
border-radius: 50%; | |
} | |
.avatar-lg img { | |
height: 128px; | |
width: 128px; | |
margin: 4px; | |
border-radius: 50%; | |
border: 1px solid #eee; | |
padding: 4px; | |
} | |
.error { | |
font-size: 16px; | |
color: red; | |
margin: .2em; | |
} | |
.error-summary { | |
background: #f2dede; | |
color: #a94442; | |
border-color: #ebccd1; | |
padding: .5em; | |
margin: .5em 0px; | |
} | |
.alert-danger, .alert-error { | |
color: #a94442; | |
margin: .5em 0px; | |
} | |
form .row { | |
margin: 0 0 1em 0; | |
} | |
input, textarea { | |
width: 99%; | |
padding: .5%; | |
font-size:16px; | |
border: 1px solid #eee; | |
} | |
button { | |
font-size: 1.5rem; | |
padding: .5em 1em; | |
} | |
.input-help { | |
float: right; | |
font-size: 14px; | |
} | |
.lnk { | |
padding: 8px 16px; | |
font-size: 16px; | |
} | |
.btn { | |
color: #24292e; | |
background-color: #eff3f6; | |
background-image: linear-gradient(-180deg, #fafbfc 0%, #eff3f6 90%); | |
} | |
.btn { | |
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; | |
position: relative; | |
display: inline-block; | |
padding: 8px 16px; | |
font-size: 16px; | |
font-weight: 600; | |
line-height: 20px; | |
white-space: nowrap; | |
vertical-align: middle; | |
cursor: pointer; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
background-repeat: repeat-x; | |
background-position: -1px -1px; | |
background-size: 110% 110%; | |
border: 1px solid rgba(27,31,35,0.2); | |
border-radius: 0.25em; | |
-webkit-appearance: none; | |
-moz-appearance: none; | |
appearance: none; | |
} | |
.btn:hover, .btn.hover { | |
background-color: #e6ebf1; | |
background-image: linear-gradient(-180deg, #f0f3f6 0%, #e6ebf1 90%); | |
background-position: -.5em; | |
border-color: rgba(27,31,35,0.35); | |
} | |
.btn:active { | |
background-image: none; | |
border-color: rgba(27,31,35,0.5); | |
box-shadow: inset 0 0.15em 0.3em rgba(27,31,35,0.15); | |
} | |
.btn-danger:active { | |
color: #fff; | |
background-color: #b5202c; | |
} | |
.btn-danger { | |
color: #cb2431; | |
background-color: #fafbfc; | |
background-image: linear-gradient(-180deg, #fafbfc 0%, #eff3f6 90%); | |
} | |
.btn-danger:hover { | |
color: #fff; | |
background-color: #cb2431; | |
background-image: linear-gradient(-180deg, #de4450 0%, #cb2431 90%); | |
border-color: rgba(27,31,35,0.5); | |
} | |
table.table-data { | |
width: 100%; | |
margin: 1em; | |
border-collapse: collapse; | |
font-size: 14px; | |
} | |
table.table-data caption { | |
font-size: 20px; | |
padding: .5em; | |
border: 1px solid #f1f1f1; | |
} | |
.table-data th { | |
white-space: nowrap; | |
padding: .2em; | |
} | |
.table-data td { | |
max-width: 500px; | |
text-overflow: ellipsis; | |
overflow: hidden; | |
vertical-align: top; | |
border: 1px solid #f1f1f1; | |
padding: .2em; | |
} | |
.table-data thead th { | |
background: #f1f1f1; | |
} | |
.post-info { | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
text-align: center; | |
} | |
.post-info .avatar-lg img { | |
width: 96px; | |
height: 96px; | |
} | |
.post-info .post-meta { | |
font-size: .7em; | |
} | |
.post-content { | |
margin-bottom: 2em; | |
} | |
.post-title { | |
text-align: center; | |
} | |
.post-title a { | |
color: #212121; | |
border-bottom: 2px dotted #eee; | |
} | |
.post-title a:hover { | |
color: #428bca; | |
text-decoration: none; | |
} | |
.post-date { | |
text-align: center; | |
font-size: 16px; | |
margin: -1em 0 0 0; | |
color: #999; | |
} | |
.content-actions { | |
text-align: center; | |
padding: 1em; | |
} | |
.content-actions form { | |
display: inline; | |
} | |
.preview { | |
border: 1px solid #eee; | |
border-radius: 2px; | |
padding: 1em; | |
user-select: none; | |
overflow-x: auto; | |
} | |
.preview:first-child, .preview:last-child { | |
margin-top:0; | |
padding-top:0; | |
} | |
pre { | |
word-spacing: normal; | |
} | |
code, kbd, pre, samp { | |
font-family: monospace,monospace; | |
} | |
p>code { | |
font-style: normal; | |
line-height: 1.5em; | |
padding: 2px 5px; | |
margin: 0; | |
background-color: rgba(0,0,0,0.04); | |
color: #c7254e; | |
border-radius: 3px; | |
} | |
pre>code { | |
display: block; | |
padding: 1em; | |
margin-bottom: 1em; | |
color: #333; | |
font-weight: 400; | |
} | |
code { | |
background-color: #f5f5f5; | |
white-space: pre-wrap; | |
font-size: .75em; | |
} | |
blockquote { | |
margin: 0 0 10px 0; | |
padding: 10px; | |
background-color: #FFF8DC; | |
border-left: 2px solid #ffeb8e; | |
} | |
blockquote>p { | |
margin: .5em; | |
} | |
.post-content img { | |
max-width: 880px; | |
} | |
.post-content table, .preview table { | |
border-collapse: collapse; | |
min-width: 50%; | |
font-size: .9em; | |
} | |
.post-content table th, .preview table th { | |
text-align: left; | |
padding: .2em .5em; | |
background: #f1f1f1; | |
} | |
.post-content table td, .preview table td { | |
text-align: left; | |
padding: .2em .5em; | |
} | |
.post-content h2[id], .post-content h3[id], .post-content h4[id], .post-content h5[id] { | |
cursor: pointer; | |
} | |
.post-content h2[id]:hover:after, .post-content h3[id]:hover:after, .post-content h4[id]:hover:after, .post-content h5[id]:hover:after { | |
content: " \00B6"; | |
} |
// Save content to local storage to preserve state across page reloads | |
let forms = document.querySelectorAll('form[data-save-drafts]'); | |
for (let i = 0; i < forms.length; i++) { | |
let form = forms[i]; | |
let monitorInputs = form.querySelectorAll('input[type=text],textarea'); | |
let keys = []; | |
for (let j=0; j < monitorInputs.length; j++) { | |
let input = monitorInputs[j]; | |
let key = 'drafts.' + location.pathname + '.' + (input.getAttribute('name') || input.id); | |
keys.push(key); | |
let draftValue = localStorage.getItem(key); | |
if (draftValue) { | |
input.value = draftValue; | |
} | |
input.addEventListener('input', function(e){ | |
localStorage.setItem(key, this.value); | |
}, false); | |
} | |
form.addEventListener('submit', function(e) { | |
keys.forEach(key => localStorage.removeItem(key)); | |
}); | |
} | |
// Enable autogrowing textareas | |
let textAreas = document.querySelectorAll("textarea[data-autogrow]"); | |
for (let i = 0; i < textAreas.length; i++) { | |
textAreas[i].addEventListener("input", autogrow); | |
autogrow({ target: textAreas[i] }); | |
} | |
function autogrow(e) { | |
let el = e.target; | |
let minHeignt = 150; | |
el.style.height = "5px"; | |
el.style.height = Math.max(el.scrollHeight, minHeignt) + "px"; | |
} | |
// Enable Live Preview of new Content | |
textAreas = document.querySelectorAll("textarea[data-livepreview]"); | |
for (let i = 0; i < textAreas.length; i++) { | |
textAreas[i].addEventListener("input", livepreview, false); | |
livepreview({ target: textAreas[i] }); | |
} | |
function livepreview(e) { | |
let el = e.target; | |
let sel = el.getAttribute("data-livepreview"); | |
if (el.value.trim() == "") { | |
document.querySelector(sel).innerHTML = "<div style='text-align:center;padding-top:20px;color:#999'>Live Preview</div>"; | |
return; | |
} | |
let formData = new FormData(); | |
formData.append("content", el.value); | |
fetch("/preview", { | |
method: "post", | |
body: formData | |
}).then(function(r) { return r.text(); }) | |
.then(function(r) { document.querySelector(sel).innerHTML = r; }); | |
} | |
// Ctrl + Click on page to edit | |
let posts = document.querySelectorAll("[data-edit-path]"); | |
for (let i = 0; i < posts.length; i++) { | |
let el = posts[i]; | |
let url = el.getAttribute("data-edit-path"); | |
el.addEventListener("click", function(e) { | |
if (e.ctrlKey) { | |
location.href = url; | |
} | |
}); | |
} | |
// Auto Link Headings with id attributes | |
var headings = document.querySelectorAll(".post-content h2[id],.post-content h3[id],.post-content h4[id],.post-content h5[id]"); | |
for (let i = 0; i < headings.length; i++) { | |
let el = headings[i]; | |
el.addEventListener("click", function(e) { | |
location.href = "#" + this.id; | |
}); | |
} |
function Editor($editor, opt) { | |
let history = []; | |
let redo = []; | |
let ops = { | |
lang: opt.lang || "", | |
target: $editor.querySelector("textarea"), | |
$emit(evt, arg) { | |
// input or save | |
if (evt === "input") { | |
this.target.value = arg; | |
var event = new Event("input", { | |
bubbles: true, | |
cancelable: true | |
}); | |
this.target.dispatchEvent(event); | |
} | |
if (opt[evt]) { | |
opt[evt].call(this,arg); | |
} | |
}, | |
$nextTick(fn) { | |
setTimeout(fn, 0); | |
}, | |
input() { | |
return this.target; | |
}, | |
hasSelection() { | |
return this.input().selectionStart !== this.input().selectionEnd; | |
}, | |
selection() { | |
let $txt = this.input(); | |
return $txt.value.substring($txt.selectionStart, $txt.selectionEnd) || ""; | |
}, | |
selectionInfo() { | |
let $txt = this.input(); | |
let value = $txt.value, | |
selPos = $txt.selectionStart, | |
sel = value.substring(selPos, $txt.selectionEnd) || "", | |
beforeSel = value.substring(0, selPos), | |
prevCRPos = beforeSel.lastIndexOf("\n"); | |
return { | |
value, | |
sel, | |
selPos, | |
beforeSel, | |
afterSel: value.substring(selPos), | |
prevCRPos, | |
beforeCR: prevCRPos >= 0 ? beforeSel.substring(0, prevCRPos + 1) : "", | |
afterCR: prevCRPos >= 0 ? beforeSel.substring(prevCRPos + 1) : "" | |
}; | |
}, | |
replace({ value, selectionStart, selectionEnd }) { | |
if (selectionEnd == null) { | |
selectionEnd = selectionStart; | |
} | |
let $txt = this.input(); | |
this.$emit("input", value); | |
this.$nextTick(() => { | |
$txt.focus(); | |
$txt.setSelectionRange(selectionStart, selectionEnd); | |
}); | |
}, | |
insert( | |
prefix, | |
suffix, | |
placeholder, | |
{ | |
selectionAtEnd, | |
offsetStart, | |
offsetEnd, | |
filterValue, | |
filterSelection | |
} = {} | |
) { | |
let $txt = this.input(); | |
let value = $txt.value; | |
let pos = $txt.selectionEnd; | |
history.push({ | |
value, | |
selectionStart: $txt.selectionStart, | |
selectionEnd: $txt.selectionEnd | |
}); | |
redo = []; | |
let from = $txt.selectionStart, | |
to = $txt.selectionEnd, | |
len = to - from; | |
let beforeRange = value.substring(0, from); | |
let afterRange = value.substring(to); | |
let toggleOff = | |
prefix && beforeRange.endsWith(prefix) && afterRange.startsWith(suffix); | |
let originalPos = pos; | |
let noSelection = from == to; | |
if (noSelection) { | |
if (!toggleOff) { | |
value = beforeRange + prefix + placeholder + suffix + afterRange; | |
pos += prefix.length; | |
offsetStart = 0; | |
offsetEnd = placeholder.length; | |
if (selectionAtEnd) { | |
pos += offsetEnd; | |
offsetEnd = 0; | |
} | |
} else { | |
value = | |
beforeRange.substring(0, beforeRange.length - prefix.length) + | |
afterRange.substring(suffix.length); | |
pos += -suffix.length; | |
} | |
if (filterValue) { | |
var opt = { pos }; | |
value = filterValue(value, opt); | |
pos = opt.pos; | |
} | |
} else { | |
var selectedText = value.substring(from, to); | |
if (filterSelection) { | |
selectedText = filterSelection(selectedText); | |
} | |
if (!toggleOff) { | |
value = beforeRange + prefix + selectedText + suffix + afterRange; | |
if (offsetStart) { | |
pos += (prefix + suffix).length; | |
} else { | |
pos = from; | |
offsetStart = prefix.length; | |
offsetEnd = selectedText.length; | |
} | |
} else { | |
value = | |
beforeRange.substring(0, beforeRange.length - prefix.length) + | |
selectedText + | |
afterRange.substring(suffix.length); | |
offsetStart = -selectedText.length - prefix.length; | |
offsetEnd = selectedText.length; | |
} | |
} | |
this.$emit("input", value); | |
this.$nextTick(() => { | |
$txt.focus(); | |
offsetStart = pos + (offsetStart || 0); | |
offsetEnd = offsetStart + (offsetEnd || 0); | |
$txt.setSelectionRange(offsetStart, offsetEnd); | |
}); | |
}, | |
bold() { | |
this.insert("**", "**", "bold"); | |
}, | |
italic() { | |
this.insert("_", "_", "italics"); | |
}, | |
strikethrough() { | |
this.insert("~~", "~~", "strikethrough"); | |
}, | |
link() { | |
this.insert("[", "](http://)", "", { offsetStart: -8, offsetEnd: 7 }); | |
}, | |
quote() { | |
this.insert("\n> ", "\n", "Blockquote", {}); | |
}, | |
image() { | |
this.insert("", "alt text", { | |
offsetStart: -8, | |
offsetEnd: 7 | |
}); | |
}, | |
code(e) { | |
let sel = this.selection(); | |
if (sel && !e.shiftKey) { | |
this.insert("`", "`", "code"); | |
} else { | |
let lang = this.lang || ""; | |
let partialSel = sel.indexOf("\n") === -1; | |
if (partialSel) { | |
this.insert("\n```" + lang + "\n", "\n```\n", "// code"); | |
} else { | |
this.insert("```" + lang + "\n", "```\n", ""); | |
} | |
} | |
}, | |
ol() { | |
if (this.hasSelection()) { | |
let { | |
sel, | |
selPos, | |
beforeSel, | |
afterSel, | |
prevCRPos, | |
beforeCR, | |
afterCR | |
} = this.selectionInfo(); | |
let partialSel = sel.indexOf("\n") === -1; | |
if (!partialSel) { | |
let indent = !sel.startsWith(" 1. "); | |
if (indent) { | |
let index = 1; | |
this.insert("", "", " - ", { | |
selectionAtEnd: true, | |
filterSelection: v => | |
" 1. " + | |
v.replace(/\n$/, "").replace(/\n/g, x => `\n ${++index}. `) + | |
"\n" | |
}); | |
} else { | |
this.insert("", "", "", { | |
filterValue: (v, opt) => { | |
if (prevCRPos >= 0) { | |
let afterCRTrim = afterCR.replace(/^ - /, ""); | |
beforeSel = beforeCR + afterCRTrim; | |
opt.pos -= afterCR.length - afterCRTrim.length; | |
} | |
return beforeSel + afterSel; | |
}, | |
filterSelection: v => | |
v.replace(/^ 1. /g, "").replace(/\n \d+. /g, "\n") | |
}); | |
} | |
} else { | |
this.insert("\n 1. ", "\n"); | |
} | |
} else { | |
this.insert("\n 1. ", "\n", "List Item", { | |
offsetStart: -10, | |
offsetEnd: 9 | |
}); | |
} | |
}, | |
ul() { | |
if (this.hasSelection()) { | |
let { | |
sel, | |
selPos, | |
beforeSel, | |
afterSel, | |
prevCRPos, | |
beforeCR, | |
afterCR | |
} = this.selectionInfo(); | |
let partialSel = sel.indexOf("\n") === -1; | |
if (!partialSel) { | |
let indent = !sel.startsWith(" - "); | |
if (indent) { | |
this.insert("", "", " - ", { | |
selectionAtEnd: true, | |
filterSelection: v => | |
" - " + v.replace(/\n$/, "").replace(/\n/g, "\n - ") + "\n" | |
}); | |
} else { | |
this.insert("", "", "", { | |
filterValue: (v, opt) => { | |
if (prevCRPos >= 0) { | |
let afterCRTrim = afterCR.replace(/^ - /, ""); | |
beforeSel = beforeCR + afterCRTrim; | |
opt.pos -= afterCR.length - afterCRTrim.length; | |
} | |
return beforeSel + afterSel; | |
}, | |
filterSelection: v => | |
v.replace(/^ - /g, "").replace(/\n - /g, "\n") | |
}); | |
} | |
} else { | |
this.insert("\n - ", "\n"); | |
} | |
} else { | |
this.insert("\n - ", "\n", "List Item", { | |
offsetStart: -10, | |
offsetEnd: 9 | |
}); | |
} | |
}, | |
heading() { | |
let sel = this.selection(), | |
partialSel = sel.indexOf("\n") === -1; | |
if (sel) { | |
if (partialSel) { | |
this.insert("\n## ", "\n", ""); | |
} else { | |
this.insert("## ", "", ""); | |
} | |
} else { | |
this.insert("\n## ", "\n", "Heading", { | |
offsetStart: -8, | |
offsetEnd: 7 | |
}); | |
} | |
}, | |
comment() { | |
let { | |
sel, | |
selPos, | |
beforeSel, | |
afterSel, | |
prevCRPos, | |
beforeCR, | |
afterCR | |
} = this.selectionInfo(); | |
let comment = !sel.startsWith("//") && !afterCR.startsWith("//"); | |
if (comment) { | |
if (!sel) { | |
this.replace({ | |
value: beforeCR + "//" + afterCR + afterSel, | |
selectionStart: selPos + "//".length | |
}); | |
} else { | |
this.insert("", "", "//", { | |
selectionAtEnd: true, | |
filterSelection: v => | |
"//" + v.replace(/\n$/, "").replace(/\n/g, "\n//") + "\n" | |
}); | |
} | |
} else { | |
this.insert("", "", "", { | |
filterValue: (v, opt) => { | |
if (prevCRPos >= 0) { | |
let afterCRTrim = afterCR.replace(/^\/\//, ""); | |
beforeSel = beforeCR + afterCRTrim; | |
opt.pos -= afterCR.length - afterCRTrim.length; | |
} | |
return beforeSel + afterSel; | |
}, | |
filterSelection: v => v.replace(/^\/\//g, "").replace(/\n\/\//g, "\n") | |
}); | |
} | |
}, | |
blockComment() { | |
this.insert("/*\n", "*/\n", ""); | |
}, | |
undo() { | |
if (history.length === 0) return false; | |
let $txt = this.input(); | |
let lastState = history.pop(); | |
redo.push({ | |
value: $txt.value, | |
selectionStart: $txt.selectionStart, | |
selectionEnd: $txt.selectionEnd | |
}); | |
this.replace(lastState); | |
return true; | |
}, | |
redo() { | |
if (redo.length === 0) return false; | |
let $txt = this.input(); | |
let lastState = redo.pop(); | |
history.push({ | |
value: $txt.value, | |
selectionStart: $txt.selectionStart, | |
selectionEnd: $txt.selectionEnd | |
}); | |
this.replace(lastState); | |
return true; | |
}, | |
save() { | |
this.$emit("save"); | |
}, | |
onkeydown(e) { | |
if (e.key === "Escape" || e.keyCode === 27) { | |
this.$emit('close'); | |
return; | |
} | |
let c = String.fromCharCode(e.keyCode).toLowerCase(); | |
if (c === '\t') { //tab: indent/unindent | |
let indent = !e.shiftKey; | |
if (indent) { | |
this.insert('','',' ', { | |
selectionAtEnd: true, | |
filterSelection: v => " " + v.replace(/\n$/,'').replace(/\n/g,"\n ") + "\n" | |
}); | |
} else { | |
this.insert('','','', { | |
filterValue:(v,opt) => { | |
let { selPos, beforeSel, afterSel, prevCRPos, beforeCR, afterCR } = this.selectionInfo(); | |
if (prevCRPos >= 0) { | |
let afterCRTrim = afterCR.replace(/\t/g,' ').replace(/^ ? ? ? ?/,''); | |
beforeSel = beforeCR + afterCRTrim; | |
opt.pos -= afterCR.length - afterCRTrim.length; | |
} | |
return beforeSel + afterSel; | |
}, | |
filterSelection: v => v.replace(/\t/g,' ').replace(/^ ? ? ? ?/g,'').replace(/\n /g,"\n") | |
}); | |
} | |
e.preventDefault(); | |
} | |
else if (e.ctrlKey) | |
{ | |
if (c === 'z') { //z: undo/redo | |
if (!e.shiftKey) { | |
if (this.undo()) { | |
e.preventDefault(); | |
} | |
} else { | |
if (this.redo()) { | |
e.preventDefault(); | |
} | |
} | |
} else if (c === 'b' && !e.shiftKey) { //b: bold | |
this.bold(); | |
e.preventDefault(); | |
} else if (c === 'h' && !e.shiftKey) { //h: heading | |
this.heading(); | |
e.preventDefault(); | |
} else if (c === 'i' && !e.shiftKey) { //i: italic | |
this.italic(); | |
e.preventDefault(); | |
} else if (c === 'q' && !e.shiftKey) { //q: blockquote | |
this.quote(); | |
e.preventDefault(); | |
} else if (c === 'l') { //l: link/image | |
if (!e.shiftKey) { | |
this.link(); | |
e.preventDefault(); | |
} else { | |
this.image(); | |
e.preventDefault(); | |
} | |
} else if ((c === 'k' || c === ',' || e.key === '<' || e.key === '>' || e.keyCode === 188)) { //<>: code | |
this.code(e); | |
e.preventDefault(); | |
} else if (c === 's' && !e.shiftKey) { //s: save | |
this.save(); | |
e.preventDefault(); | |
} else if (c === '/' || e.key === '/') { | |
this.comment(); | |
e.preventDefault(); | |
} else if ((c === '?' || e.key === '?') && e.shiftKey) { | |
this.blockComment(); | |
e.preventDefault(); | |
} | |
} | |
else if (e.altKey) { | |
if (e.key === '1' || e.key === '0') { | |
this.ol(); | |
e.preventDefault(); | |
} else if (e.key === '-') { | |
this.ul(); | |
e.preventDefault(); | |
} else if (e.key === 's') { | |
this.strikethrough(); | |
e.preventDefault(); | |
} | |
} | |
}, | |
ensureMarkdownBlock() { | |
if (this.target.value.indexOf("{{#markdown") == -1) { | |
let prefix = "{{#markdown}}\n"; | |
var selection = { | |
start: this.target.selectionStart, | |
end: this.target.selectionEnd | |
}; | |
this.target.value = prefix + this.target.value + "\n{{/markdown}}"; | |
this.target.setSelectionRange( | |
selection.start + prefix.length, | |
selection.end + prefix.length | |
); | |
} | |
} | |
}; | |
let ACTIVE_KEYS = '\t,b,n,h,i,q,l,k,<,>,/,?,1,0'.split(','); | |
ops.target.addEventListener('keydown', function(e){ | |
var isActiveKey = ACTIVE_KEYS.indexOf(e.key) >= 0; | |
if (isActiveKey && (e.ctrlKey || e.altKey)) { | |
ops.ensureMarkdownBlock(); | |
} | |
ops.onkeydown(e); | |
}); | |
let btns = $editor.querySelectorAll(".editor-toolbar [data-cmd]"); | |
for (let i = 0; i < btns.length; i++) { | |
let el = btns[i]; | |
let cmd = el.getAttribute("data-cmd"); | |
el.addEventListener("click", function(e) { | |
if (ops[cmd]) { | |
if (['save','undo','redo'].indexOf(cmd) === -1) { | |
ops.ensureMarkdownBlock(); | |
} | |
ops[cmd](e); | |
} | |
}); | |
} | |
} |
API /hello/{name} | |
* name : string - Name of Person to greet | |
{{ { result: `Hello, ${name}!` } | return }} |
<div class="page"> | |
{{ `SELECT * | |
FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName | |
ORDER BY p.Created DESC, Id DESC` | |
|> dbSelect |> to => posts }} | |
{{ 'posts' |> partial({ posts } )}} | |
</div> |
API /posts/{slug}/api | |
* slug : string - Return post with this slug name | |
{{ `SELECT * | |
FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName | |
WHERE Slug = @slug | |
ORDER BY p.Created DESC` | |
|> dbSingle({ slug }) | |
|> to => post }} | |
{{ post ?? httpResult({ status:404, statusDescription:'Post was not found' }) | |
|> return }} |
{{#if isHttpPost}} | |
{{ 'assert-auth' |> partial }} | |
{{ `SELECT * from Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName WHERE Slug = @slug ORDER BY p.Created DESC` | |
|> dbSingle({ slug }) | |
|> to => post }} | |
{{ 'assert-post' |> partial({ post }) }} | |
{{ `DELETE FROM Post WHERE Slug=@slug AND CreatedBy=@userName` |> dbExec({ slug, userName }) |> end }} | |
{{/if}} | |
{{ httpResult({ status:301, Location: `/${userName}` }) |> return }} |
<!-- | |
title: Edit post | |
--> | |
<div class="page-edit"> | |
{{ 'assert-auth' |> partial }} | |
{{ 'postTitle,content' |> importRequestParams }} | |
{{ `SELECT * from Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName WHERE Slug = @slug ORDER BY p.Created DESC` | |
|> dbSingle({ slug }) | |
|> to => post }} | |
{{ 'assert-post' |> partial({ post }) }} | |
{{#if isHttpGet }} | |
{{ var postTitle = post.Title}} | |
{{ var content = post.Content}} | |
{{else if isHttpPost }} | |
{{ assignErrorAndContinueExecuting: ex }} | |
{{ 'Title must be between 5 and 200 characters' | onlyIf(length(postTitle) < 5 || length(postTitle) > 200) |> to => titleError }} | |
{{ 'Content must be between 25 and 64000 characters' | onlyIf(length(content) < 25 || length(content) > 64000) |> to => contentError }} | |
{{ 'Potentially malicious characters detected' | ifNotExists(contentError) | onlyIf(containsXss(content)) |> to => contentError }} | |
{{#if !(ex || titleError || contentError) }} | |
{{ var sqlNow = `datetime(CURRENT_TIMESTAMP,'localtime')` }} | |
{{ `UPDATE Post SET Title=@title, Content=@content, Modified=${sqlNow}, ModifiedBy=@userName WHERE Slug=@slug` | |
|> dbExec({ slug:post.Slug, title: postTitle, content, userName }) |> end }} | |
{{ var success = ex == null }} | |
{{/if}} | |
{{/if}} | |
{{#if success}} | |
{{ httpResult({ status:301, Location: `/${userName}` }) |> return }} | |
{{else}} | |
<form method="POST" data-save-drafts> | |
{{#if ex}}<div class="error-summary">{{ex.message}}</div>{{/if}} | |
<div class="row"> | |
<input name="postTitle" type="text" placeholder="title" autocomplete="off" value="{{postTitle}}"> | |
{{#if titleError}}<div class="error">{{titleError}}</div>{{/if}} | |
</div> | |
<div class="row"> | |
{{ 'editor' |> partial({ name:"content", placeholder:"content (templates)", 'data-autogrow':true, 'data-livepreview':'.preview', error:contentError }) }} | |
</div> | |
<div class="row"> | |
<button class="btn" type="submit">Update post</button> | |
</div> | |
</form> | |
<div> | |
<div class="preview"></div> | |
</div> | |
{{/if}} | |
<style> | |
#body { | |
justify-content: left; | |
} | |
textarea { | |
height: 20em; | |
} | |
</style> | |
</div> |
<div class="page"> | |
{{ `SELECT * | |
FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName | |
WHERE Slug = @slug | |
ORDER BY p.Created DESC` | |
|> dbSingle({ slug }) | |
|> to => post }} | |
{{ 'error' |> if(!post) |> partial({ error:'Post does not exist' }) }} | |
{{ 'posts' |> partial({ posts: [post] } )}} | |
<div class="content-src"> | |
<h3>Content Source</h3> | |
<textarea data-autogrow onfocus="this.select()" onmouseup="return false">{{ post.Content }}</textarea> | |
</div> | |
<style> | |
.content-src { | |
border-top: 1px solid #eee; | |
position: absolute; | |
left: 10%; | |
width: 80%; | |
padding: 0 0 2em 0; | |
} | |
.content-src h3 { | |
text-align: center; | |
} | |
.content-src textarea { | |
min-height: 20em; | |
font-size: 16px; | |
} | |
</style> | |
</div> |
<!-- | |
title: New post | |
--> | |
<div class="page-edit"> | |
{{ 'assert-auth' |> partial }} | |
{{ 'postTitle,content' |> importRequestParams }} | |
{{#if isHttpPost }} | |
{{ assignErrorAndContinueExecuting: ex }} | |
{{ 'Title must be between 5 and 200 characters' | onlyIf(length(postTitle) < 5 || length(postTitle) > 200) |> to => titleError }} | |
{{ 'Content must be between 25 and 64000 characters' | onlyIf(length(content) < 25 || length(content) > 64000) |> to => contentError }} | |
{{ 'Potentially malicious characters detected' | ifNotExists(contentError) | onlyIf(containsXss(content)) |> to => contentError }} | |
{{#if !(titleError || contentError) }} | |
{{#if dbSingle(`SELECT Slug FROM Post WHERE slug = @slug`, { slug: generateSlug(postTitle) }) }} | |
{{ var titleError = 'Title already exists' }} | |
{{/if}} | |
{{/if}} | |
{{#if !(ex || titleError || contentError) }} | |
{{ var sqlNow = `datetime(CURRENT_TIMESTAMP,'localtime')` }} | |
{{ `INSERT INTO Post (Slug, Title, Content, Created, CreatedBy, Modified, ModifiedBy) VALUES (@slug, @title, @content, ${sqlNow}, @user, ${sqlNow}, @user)` | |
|> dbExec({ slug: generateSlug(postTitle), title: postTitle, content, user: userName }) }} | |
{{ var success = ex == null }} | |
{{/if}} | |
{{/if}} | |
{{#if success}} | |
{{ httpResult({ status:301, Location: `/${userName}` }) |> return }} | |
{{else}} | |
<form method="POST" data-save-drafts> | |
{{#if ex}}<div class="error-summary">{{ex.message}}</div>{{/if}} | |
<div class="row"> | |
<input name="postTitle" type="text" placeholder="title" autocomplete="off" value="{{postTitle}}"> | |
{{#if titleError}}<div class="error">{{titleError}}</div>{{/if}} | |
</div> | |
<div class="row"> | |
{{ 'editor' |> partial({ name:"content", placeholder:"content (templates)", 'data-autogrow':true, 'data-livepreview':'.preview', error:contentError }) }} | |
</div> | |
<div class="row"> | |
<button class="btn" type="submit">create post</button> | |
</div> | |
</form> | |
<div> | |
<div class="preview"></div> | |
</div> | |
{{/if}} | |
<style> | |
#body { | |
justify-content: left; | |
} | |
</style> | |
</div> |
API /preview | |
* content : string - #Script to evaluate | |
{{ var response = qs.content.evalTemplate({use:{plugins:'MarkdownScriptPlugin'}}) }} | |
{{ response |> return({ contentType:'text/plain' }) }} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment