diff --git a/.deploy/docker-compose-template.yml b/.deploy/docker-compose-template.yml new file mode 100644 index 0000000..03bd7d3 --- /dev/null +++ b/.deploy/docker-compose-template.yml @@ -0,0 +1,21 @@ +version: "3.9" +services: + ${APP_NAME}: + image: ghcr.io/${IMAGE_REPO}:${RELEASE_VERSION} + restart: always + ports: + - "5000" + environment: + VIRTUAL_HOST: ${HOST_DOMAIN} + LETSENCRYPT_HOST: ${HOST_DOMAIN} + LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL} + volumes: + - ${APP_NAME}-mydb:/app/App_Data + +networks: + default: + name: nginx + external: true + +volumes: + ${APP_NAME}-mydb: diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..1f3ff5a --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,144 @@ +# ServiceStack mix GitHub Actions +`release.yml` generated from `x mix release-ecr-aws`, this template in designed to help with automating CI deployments to AWS ECS and dedicated AWS ECS cluster. +This is a cheap way to start without an AWS Application Load Balancer (ALB) and also be in a situation that will easier to add one once the web service needs additional scale or high availability. + +## Overview +`release.yml` is designed to work with a ServiceStack app templates deploying directly to a single server in a dedicated ECS cluster via templated GitHub Actions. + +## Setup +### Create unique ECS cluster +For this setup, it is best to create a separate cluster as cluster will only have the single instance in it running. +This pattern is to start from a good base with AWS ECS and automated CI deployment while avoiding the higher costs of needing to run an application load balancer (ALB). + +If/when you can justify the cost of an ALB for easier scaling and zero downtime deployment, the GitHub Action `release.yml` can be slightly modified to be used with a re-created or different ECS Service that is configured to be used with an Application Load Balancer and Target Group. + +### Elastic IP (optional) +The reason you might want to register this first is because we are only running one EC2 instance and hosting our own `nginx-proxy` on the same instance as the applications. +Since an `A` record will be pointing there, one advantage of not using an auto-assigned IP is that we can reassign the elastic IP if for what ever reason the instance goes down or is lost. + +## Launch to EC2 Instance +When launching the EC2 instance, you'll need to select an 'ECS optimized' AMI as the image used for your instance. +### Choose AMI +The easiest way to find the latest Amazon Linux 2 image for this is to go to the [AWS documentation for ECS-optimized AMIs and look up your region here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html#ecs-optimized-ami-linux). + +Using the AMI ID (starts with `ami-`) at the bottom, search in the 'Community AMIs' tab on the first step of the `Launch EC2 Instance` wizard. + +### Choose Instance Type +A t2.micro or larger will work fine, this pattern can be used to host multiple applications on the 1 server so if the number of applications gets larger, you might need a larger instance type. +> Note this pattern is suitable for testing prototypes or low traffic applications as it is cost effective and makes it easy to bundle multiple apps onto 1 EC2 instance. + +### Configure Instance +Under `IAM role`, use the `ecsInstanceRole`, if this is not available, see [AWS documentation for the process of checking if it exists and creating it if needed](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html). + +You will also want to add the following Userdata script (in the `Configure` step of the launch wizard) with your own `ECS_CLUSTER` value. This tells the ecs-agent running on the instance which ECS cluster the instance should join. + +```bash +#!/bin/bash +cat </etc/ecs/ecs.config +ECS_CLUSTER=sharpscripts +ECS_AVAILABLE_LOGGING_DRIVERS=["awslogs", "syslog"] +ECS_ENABLE_CONTAINER_METADATA=true +EOS +``` + +Note down your cluster name as it will need to be used to create the cluster in ECS before it is visible. +See [`ECS Container Agent Configuration`](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-agent-config.html) for more information. + +### Add Storage +The default of 30gb is fine but take into account how large/how many applications you'll have running. + +### Configure Security Groups +You'll want to expose at least ports 80 and 443. + +### Setup Docker-compose and nginx-proxy +To let your server handle multiple ServiceStack applications and automate the generation and management of TLS certificates, an additional docker-compose file is provided via the `x mix` template, `nginx-proxy-compose.yml`. This docker-compose file is ready to run and can be copied to the deployment server. +> This is done via docker-compose rather than via ECS for simplicity. + +For example, once copied to remote `~/nginx-proxy-compose.yml`, the following command can be run on the remote server. + +``` +docker-compose -f ~/nginx-proxy-compose.yml up -d +``` + +This will run an nginx reverse proxy along with a companion container that will watch for additional containers in the same docker network and attempt to initialize them with valid TLS certificates. + +## GitHub Repository setup +The `release.yml` assumes 6 secrets have been setup. + +- AWS_ACCESS_KEY_ID - AWS access key for programmatic access to AWS APIs. +- AWS_SECRET_ACCESS_KEY - AWS access secrets for programmatic access to AWS APIs. +- AWS_REGION - default region for AWS API calls. +- AWS_ECS_CLUSTER - Cluster name in ECS, this should match the value in your Userdata. +- HOST_DOMAIN - Domain/submain of your application, eg `sharpscript.example.com` . +- LETSENCRYPT_EMAIL - Email address, required for Let's Encrypt automated TLS certificates. + +These secrets are used to populate variables within GitHub Actions and other configuration files. + +For the AWS access, a separate user specifically for deploying via GitHub Actions should be used. + +The policies required for the complete initial setup will be: +- `AmazonEC2ContainerRegistryFullAccess` +- `AmazonECS_FullAccess` + +Once the application is successfully deployed the first time, reduced access for both ECR and ECS can be used instead. For application updates, the GitHub Action can use the following policy. + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ecr:GetRegistryPolicy", + "ecr:PutImageTagMutability", + "ecr:GetDownloadUrlForLayer", + "ecr:DescribeRegistry", + "ecr:GetAuthorizationToken", + "ecr:ListTagsForResource", + "ecr:UploadLayerPart", + "ecr:ListImages", + "ecr:PutImage", + "ecr:UntagResource", + "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:DescribeImages", + "ecr:TagResource", + "ecr:DescribeRepositories", + "ecr:InitiateLayerUpload", + "ecr:BatchCheckLayerAvailability", + "ecr:ReplicateImage", + "ecr:GetRepositoryPolicy", + "ecs:SubmitTaskStateChange", + "ecs:UpdateContainerInstancesState", + "ecs:RegisterContainerInstance", + "ecs:DescribeTaskDefinition", + "ecs:DescribeClusters", + "ecs:ListServices", + "ecs:UpdateService", + "ecs:ListTasks", + "ecs:ListTaskDefinitionFamilies", + "ecs:RegisterTaskDefinition", + "ecs:SubmitContainerStateChange", + "ecs:StopTask", + "ecs:DescribeServices", + "ecs:ListContainerInstances", + "ecs:DescribeContainerInstances", + "ecs:DeregisterContainerInstance", + "ecs:TagResource", + "ecs:DescribeTasks", + "ecs:UntagResource", + "ecs:ListTaskDefinitions", + "ecs:ListClusters" + ], + "Resource": "*" + } + ] +} +``` +> Further permission reduction can be done by reducing what resources can be accessed. +> Application permissions can be controlled via `taskRoleArn`, see [AWS docs for details](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html). + +## What's the process of the `release.yml`? + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/release-ecr-aws-diagram.png) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..7d9b802 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: Build + +on: + pull_request: {} + push: + branches: + - '**' # matches every branch + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.*' + + - name: build + run: dotnet build + working-directory: . + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..698a67f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +name: Build container +permissions: + packages: write + contents: write +on: + # Triggered on new GitHub Release + release: + types: [published] + # Triggered on every successful Build action + workflow_run: + workflows: ["Build"] + branches: [main,master] + types: + - completed + # Manual trigger for rollback to specific release or redeploy latest + workflow_dispatch: + inputs: + version: + default: latest + description: Tag you want to release. + required: true + +jobs: + push_to_registry: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion != 'failure' }} + steps: + # Checkout latest or specific tag + - name: checkout + if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} + uses: actions/checkout@v3 + - name: checkout tag + if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }} + uses: actions/checkout@v3 + with: + ref: refs/tags/${{ github.event.inputs.version }} + + # Assign environment variables used in subsequent steps + - name: Env variable assignment + run: echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + # TAG_NAME defaults to 'latest' if not a release or manual deployment + - name: Assign version + run: | + echo "TAG_NAME=latest" >> $GITHUB_ENV + if [ "${{ github.event.release.tag_name }}" != "" ]; then + echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV + fi; + if [ "${{ github.event.inputs.version }}" != "" ]; then + echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV + fi; + + # Authenticate, build and push to GitHub Container Registry (ghcr.io) + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Build and push new docker image, skip for manual redeploy other than 'latest' + - name: Build and push Docker images + uses: docker/build-push-action@v6 + if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} + with: + file: Dockerfile + context: . + push: true + tags: ghcr.io/${{ env.image_repository_name }}:${{ env.TAG_NAME }} + diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000..2fb07d7 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000..75efafc --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000..1435a67 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000..f87d811 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000..18e61d7 --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000..1b280c7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end +end + + +$stdout.sync = true + +puts "Checking build status..." +attempts = 0 +checks = GithubStatusChecks.new + +begin + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000..061f805 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000..690aa96 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,18 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Option 1: Read secrets from the environment +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD +KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME + +# Option 2: Read secrets via a command +# RAILS_MASTER_KEY=$(cat config/master.key) + +# Option 3: Read secrets via kamal secrets helpers +# These will handle logging in and fetching the secrets in as few calls as possible +# There are adapters for 1Password, LastPass + Bitwarden +# +# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fe0d675..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: csharp -os: linux -dist: trusty -sudo: required -dotnet: 2.1.3 -mono: none -env: DOTNETCORE=1 -services: - - docker -addons: - apt: - packages: - - docker-ce -script: - - chmod +x ./deploy-envs.sh - - chmod +x ./scripts/build.sh - - chmod +x ./scripts/deploy.sh - - cd scripts && ./build.sh - - if [ "$TRAVIS_BRANCH" == "master" ]; then ./deploy.sh; fi \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index e4b2363..ed180eb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/src/bin/Debug/netcoreapp2.1/TemplatePages.dll", + "program": "${workspaceRoot}/src/bin/Debug/netcoreapp2.1/SharpScript.dll", "args": [], "cwd": "${workspaceRoot}/src", "stopAtEntry": false, @@ -17,7 +17,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/src/bin/Debug/netcoreapp2.1/TemplatePages.dll", + "program": "${workspaceRoot}/src/bin/Debug/netcoreapp2.1/SharpScript.dll", "args": [], "cwd": "${workspaceRoot}/src", "stopAtEntry": false, diff --git a/AppHost.cs b/AppHost.cs new file mode 100644 index 0000000..a4e65f7 --- /dev/null +++ b/AppHost.cs @@ -0,0 +1,183 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Collections; +using ServiceStack; +using ServiceStack.IO; +using ServiceStack.Script; +using ServiceStack.Text; +using ServiceStack.Data; +using ServiceStack.OrmLite; +using ServiceStack.Configuration; +using Funq; + +namespace SharpScript; + +public class AppHost : AppHostBase +{ + public AppHost() + : base("#Script Pages", typeof(ScriptServices).Assembly) { } + + public ScriptContext LinqContext; + + public override void Configure(Container container) + { + SetConfig(new HostConfig { + DebugMode = AppSettings.Get("DebugMode", Env.IsWindows), + }); + + Plugins.Add(new ServiceStack.Api.OpenApi.OpenApiFeature()); + + var path = MapProjectPath("~/wwwroot/assets/js/customers.json"); + var json = File.ReadAllText(path); + TemplateQueryData.Customers = json.FromJson>(); + + container.Register(c => new Customers(TemplateQueryData.Customers)); + container.Register(c => new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider)); + + using (var db = container.Resolve().Open()) + { + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + TemplateQueryData.Customers.Each(x => db.Save(x, references:true)); + db.InsertAll(TemplateQueryData.Products); + + db.CreateTable(); + db.Insert(new Quote { Id = 1, Url = "https://gist.githubusercontent.com/gistlyn/0ab7494dfbff78466ef622d501662027/raw/b3dd1e9f8e82c169a32829071aa7f761c6494843/quote.md" }); + } + + Plugins.Add(new AutoQueryFeature { MaxLimit = 100 }); + + Plugins.Add(new AutoQueryDataFeature { MaxLimit = 100 } + .AddDataSource(ctx => ctx.ServiceSource(ctx.Dto.ConvertTo(), + HostContext.Cache, TimeSpan.FromMinutes(10))) + ); + + var customFilters = new CustomScriptMethods(); + Plugins.Add(new SharpPagesFeature { + ScriptLanguages = { ScriptLisp.Language }, + ScriptMethods = { + customFilters, + new DbScriptsAsync() + }, + FilterTransformers = { + ["convertScriptToCodeBlocks"] = GitHubMarkdownScripts.convertScriptToCodeBlocks, + ["convertScriptToLispBlocks"] = GitHubMarkdownScripts.convertScriptToLispBlocks, + }, + Args = { + ["products"] = TemplateQueryData.Products + }, + ScriptTypes = { + typeof(Ints), + typeof(Adder), + typeof(StaticLog), + typeof(InstanceLog), + typeof(GenericStaticLog<>), + }, + RenderExpressionExceptions = true, + MetadataDebugAdminRole = RoleNames.AllowAnon, + ExcludeFiltersNamed = { "dbExec" } + }); + + AfterInitCallbacks.Add(host => { + var feature = GetPlugin(); + + var files = GetVirtualFileSources().First(x => x is FileSystemVirtualFiles); + foreach (var file in files.GetDirectory("docs").GetAllMatchingFiles("*.html")) + { + var page = feature.GetPage(file.VirtualPath).Init().Result; + if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) + { + customFilters.DocsIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); + } + } + + foreach (var file in files.GetDirectory("sharp-apps").GetAllMatchingFiles("*.html")) + { + var page = feature.GetPage(file.VirtualPath).Init().Result; + if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) + { + customFilters.AppsIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); + } + } + + foreach (var file in files.GetDirectory("scode").GetAllMatchingFiles("*.html")) + { + var page = feature.GetPage(file.VirtualPath).Init().Result; + if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) + { + customFilters.CodeIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); + } + } + + foreach (var file in files.GetDirectory("lisp").GetAllMatchingFiles("*.html")) + { + var page = feature.GetPage(file.VirtualPath).Init().Result; + if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) + { + customFilters.LispIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); + } + } + + foreach (var file in files.GetDirectory("usecases").GetAllMatchingFiles("*.html")) + { + var page = feature.GetPage(file.VirtualPath).Init().Result; + if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) + { + customFilters.UseCasesIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); + } + } + + foreach (var file in files.GetDirectory("linq").GetAllMatchingFiles("*.html")) + { + var page = feature.GetPage(file.VirtualPath).Init().Result; + if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) + { + customFilters.LinqIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); + } + } + + var protectedScriptNames = new HashSet(ScriptMethodInfo.GetMethodsAvailable(typeof(ProtectedScripts)).Map(x => x.Name)); + + LinqContext = new ScriptContext { + ScriptLanguages = { ScriptLisp.Language }, + Args = { + [ScriptConstants.DefaultDateFormat] = "yyyy/MM/dd", + ["products"] = TemplateQueryData.Products, + ["products-list"] = Lisp.ToCons(TemplateQueryData.Products), + ["customers"] = TemplateQueryData.Customers, + ["customers-list"] = Lisp.ToCons(TemplateQueryData.Customers), + ["comparer"] = new CaseInsensitiveComparer(), + ["anagramComparer"] = new AnagramEqualityComparer(), + }, + ScriptTypes = { + typeof(DateTime), + typeof(CaseInsensitiveComparer), + typeof(AnagramEqualityComparer), + }, + ScriptMethods = { + new ProtectedScripts() + }, + }; + protectedScriptNames.Each(x => LinqContext.ExcludeFiltersNamed.Add(x)); + LinqContext.Init(); + }); + } + + public string GetPath(string virtualPath) + { + var path = "/" + virtualPath.LastLeftPart('.'); + if (path.EndsWith("/index")) + path = path.Substring(0, path.Length - "index".Length); + + return path; + } +} + +public class Quote +{ + public int Id { get; set; } + public string Url { get; set; } +} diff --git a/Dockerfile b/Dockerfile index a0e853a..48dad51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ -FROM microsoft/dotnet:2.1-sdk AS build-env -COPY src /app -WORKDIR /app +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /source -RUN dotnet restore --configfile NuGet.Config -RUN dotnet publish -c Release -o out +COPY . . +RUN dotnet restore SharpScript.csproj +RUN dotnet publish SharpScript.csproj -c release -o /app --no-restore -# Build runtime image -FROM microsoft/dotnet:2.1-aspnetcore-runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +LABEL service="sharpscript" WORKDIR /app -COPY --from=build-env /app/out . -ENV ASPNETCORE_URLS http://*:5000 -ENTRYPOINT ["dotnet", "TemplatePages.dll"] \ No newline at end of file +ENV ASPNETCORE_URLS=http://+:8080 +COPY --from=build /app ./ +ENTRYPOINT ["dotnet", "SharpScript.dll"] diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..ce78a68 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..f816e5b --- /dev/null +++ b/Program.cs @@ -0,0 +1,25 @@ +var builder = WebApplication.CreateBuilder(args); + +var cultureInfo = new System.Globalization.CultureInfo("en-US"); +System.Globalization.CultureInfo.DefaultThreadCurrentCulture = cultureInfo; +System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ +} +else +{ + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseServiceStack(new AppHost()); + +app.Run(); diff --git a/README.md b/README.md index e7c7e31..b6debde 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# ServiceStack Templates +# SharpScript -Comprehensive Documentation complete with Live Interactive Examples on using ServiceStack Templates. +Comprehensive Documentation complete with Live Interactive Examples on using SharpScript. -> Live Demo: [templates.servicestack.net](http://templates.servicestack.net) +> Live Demo: [sharpscript.net](http://sharpscript.net) -[![](https://raw.githubusercontent.com/NetCoreApps/TemplatePages/master/src/wwwroot/assets/img/screenshot.png)](https://github.com/NetCoreApps/TemplatePages/tree/master/src) +[![](https://raw.githubusercontent.com/ServiceStack/sharpscript/master/src/wwwroot/assets/img/screenshot.png)](https://github.com/ServiceStack/sharpscript/tree/master/src) diff --git a/src/AutoDataQueryServices.cs b/ServiceInterface/AutoDataQueryServices.cs similarity index 96% rename from src/AutoDataQueryServices.cs rename to ServiceInterface/AutoDataQueryServices.cs index 490b3d6..39d3021 100644 --- a/src/AutoDataQueryServices.cs +++ b/ServiceInterface/AutoDataQueryServices.cs @@ -5,7 +5,7 @@ using System.Text; using ServiceStack; -namespace TemplatePages +namespace SharpScript { public class QueryGitHubRepos : QueryData { @@ -115,7 +115,7 @@ public T GetJson(string route) { try { return "https://api.github.com".CombineWith(route) - .GetJsonFromUrl(requestFilter: req => req.UserAgent = nameof(AutoDataQueryServices)) + .GetJsonFromUrl(requestFilter: req => req.With(c => c.UserAgent = nameof(AutoDataQueryServices))) .FromJson(); } catch(Exception) { return default(T); } } diff --git a/src/AutoQueryDbServices.cs b/ServiceInterface/AutoQueryDbServices.cs similarity index 93% rename from src/AutoQueryDbServices.cs rename to ServiceInterface/AutoQueryDbServices.cs index c1ba56f..dd022ed 100644 --- a/src/AutoQueryDbServices.cs +++ b/ServiceInterface/AutoQueryDbServices.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using ServiceStack; -namespace TemplatePages +namespace SharpScript { // The entire QueryCustomers AutoQuery Service (no implementation required) public class QueryCustomers : QueryDb diff --git a/src/CodePagePartials.cs b/ServiceInterface/CodePagePartials.cs similarity index 89% rename from src/CodePagePartials.cs rename to ServiceInterface/CodePagePartials.cs index 2f58260..96813b3 100644 --- a/src/CodePagePartials.cs +++ b/ServiceInterface/CodePagePartials.cs @@ -1,12 +1,12 @@ using System.Linq; using System.Collections.Generic; using ServiceStack; -using ServiceStack.Templates; +using ServiceStack.Script; -namespace TemplatePages +namespace SharpScript { [Page("navLinks")] - public class NavLinksPartial : TemplateCodePage + public class NavLinksPartial : SharpCodePage { string render(string PathInfo, Dictionary links) => $@"
    @@ -19,7 +19,7 @@ string render(string PathInfo, Dictionary links) => $@" } [Page("customerCard")] - public class CustomerCardPartial : TemplateCodePage + public class CustomerCardPartial : SharpCodePage { public ICustomers Customers { get; set; } diff --git a/ServiceInterface/Configure.Nuglify.cs b/ServiceInterface/Configure.Nuglify.cs new file mode 100644 index 0000000..988fd8e --- /dev/null +++ b/ServiceInterface/Configure.Nuglify.cs @@ -0,0 +1,41 @@ +using System; +using ServiceStack; +using ServiceStack.Html; +using NUglify; + +namespace SharpScript +{ + public class NUglifyJsMinifier : ICompressor + { + public string Compress(string js) + { + try + { + return Uglify.Js(js).Code; + } + catch (Exception e) + { + Console.WriteLine(e); + return js; + } + } + } + public class NUglifyCssMinifier : ICompressor + { + public string Compress(string css) => Uglify.Css(css).Code; + } + public class NUglifyHtmlMinifier : ICompressor + { + public string Compress(string html) => Uglify.Html(html).Code; + } + + public class ConfigureNUglify : IConfigureAppHost + { + public void Configure(IAppHost appHost) + { + Minifiers.JavaScript = new NUglifyJsMinifier(); + Minifiers.Css = new NUglifyCssMinifier(); + Minifiers.Html = new NUglifyHtmlMinifier(); + } + } +} \ No newline at end of file diff --git a/ServiceInterface/CustomScriptMethods.cs b/ServiceInterface/CustomScriptMethods.cs new file mode 100644 index 0000000..98a97cb --- /dev/null +++ b/ServiceInterface/CustomScriptMethods.cs @@ -0,0 +1,255 @@ +using System.Threading.Tasks; +using ServiceStack; +using ServiceStack.Script; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Diagnostics; +using System; +using ServiceStack.Redis; +using ServiceStack.OrmLite; +using System.Reflection; + +namespace SharpScript +{ + public class CustomScriptMethods : ScriptMethods + { + public Dictionary> DocsIndex { get; } = new Dictionary>(); + public Dictionary> AppsIndex { get; } = new Dictionary>(); + public Dictionary> CodeIndex { get; } = new Dictionary>(); + public Dictionary> LispIndex { get; } = new Dictionary>(); + public Dictionary> UseCasesIndex { get; } = new Dictionary>(); + public Dictionary> LinqIndex { get; } = new Dictionary>(); + + public object prevDocLink(int order) + { + if (DocsIndex.TryGetValue(order - 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object nextDocLink(int order) + { + if (DocsIndex.TryGetValue(order + 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object prevAppsLink(int order) + { + if (AppsIndex.TryGetValue(order - 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object nextAppsLink(int order) + { + if (AppsIndex.TryGetValue(order + 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object prevCodeLink(int order) + { + if (CodeIndex.TryGetValue(order - 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object nextCodeLink(int order) + { + if (CodeIndex.TryGetValue(order + 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object prevLispLink(int order) + { + if (LispIndex.TryGetValue(order - 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object nextLispLink(int order) + { + if (LispIndex.TryGetValue(order + 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object prevUseCaseLink(int order) + { + if (UseCasesIndex.TryGetValue(order - 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object nextUseCaseLink(int order) + { + if (UseCasesIndex.TryGetValue(order + 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object prevLinqLink(int order) + { + if (LinqIndex.TryGetValue(order - 1, out KeyValuePair entry)) + return entry; + return null; + } + + public object nextLinqLink(int order) + { + if (LinqIndex.TryGetValue(order + 1, out KeyValuePair entry)) + return entry; + return null; + } + + List> sortedDocLinks; + public object docLinks() => sortedDocLinks ?? (sortedDocLinks = sortLinks(DocsIndex)); + + List> sortedAppsLinks; + public object appsLinks() => sortedAppsLinks ?? (sortedAppsLinks = sortLinks(AppsIndex)); + + List> sortedCodeLinks; + public object codeLinks() => sortedCodeLinks ?? (sortedCodeLinks = sortLinks(CodeIndex)); + List> sortedLispLinks; + public object lispLinks() => sortedLispLinks ?? (sortedLispLinks = sortLinks(LispIndex)); + + List> sortedUseCaseLinks; + public object useCaseLinks() => sortedUseCaseLinks ?? (sortedUseCaseLinks = sortLinks(UseCasesIndex)); + + List> sortedLinqLinks; + public object linqLinks() => sortedLinqLinks ?? (sortedLinqLinks = sortLinks(LinqIndex)); + + public List> sortLinks(Dictionary> links) + { + var sortedKeys = links.Keys.ToList(); + sortedKeys.Sort(); + + var to = new List>(); + + foreach (var key in sortedKeys) + { + var entry = links[key]; + to.Add(entry); + } + + return to; + } + + public async Task includeContentFile(ScriptScopeContext scope, string virtualPath) + { + var file = HostContext.VirtualFiles.GetFile(virtualPath); + if (file == null) + throw new FileNotFoundException($"includeContentFile '{virtualPath}' was not found"); + + using (var reader = file.OpenRead()) + { + await reader.CopyToAsync(scope.OutputStream); + } + } + + public List customers() => TemplateQueryData.Customers; + + public Process[] processes => Process.GetProcesses(); + public Process[] processesByName(string name) => Process.GetProcessesByName(name); + public Process processById(int processId) => Process.GetProcessById(processId); + public Process currentProcess() => Process.GetCurrentProcess(); + + Type GetFilterType(string name) => name switch { + nameof(DefaultScripts) => typeof(DefaultScripts), + nameof(HtmlScripts) => typeof(HtmlScripts), + nameof(ProtectedScripts) => typeof(ProtectedScripts), + nameof(InfoScripts) => typeof(InfoScripts), + nameof(RedisScripts) => typeof(RedisScripts), + nameof(DbScriptsAsync) => typeof(DbScriptsAsync), + nameof(ValidateScripts) => typeof(ValidateScripts), + nameof(AutoQueryScripts) => typeof(AutoQueryScripts), + nameof(ServiceStackScripts) => typeof(ServiceStackScripts), + _ => throw new NotSupportedException(name) + }; + + public IRawString methodLinkToSrc(string name) + { + const string prefix = "https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Script/Methods/"; + + var type = GetFilterType(name); + var url = type == typeof(DefaultScripts) + ? prefix + : type == typeof(HtmlScripts) || type == typeof(ProtectedScripts) + ? $"{prefix}{type.Name}.cs" + : type == typeof(InfoScripts) + ? "https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/InfoScripts.cs" + : type == typeof(RedisScripts) + ? "https://github.com/ServiceStack/ServiceStack.Redis/blob/master/src/ServiceStack.Redis/RedisScripts.cs" + : type == typeof(DbScriptsAsync) + ? "https://github.com/ServiceStack/ServiceStack.OrmLite/tree/master/src/ServiceStack.OrmLite/DbScriptsAsync.cs" + : type == typeof(ValidateScripts) + ? "https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/ValidateScripts.cs" + : type == typeof(AutoQueryScripts) + ? "https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Server/AutoQueryScripts.cs" + : type == typeof(ServiceStackScripts) + ? "https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/ServiceStackScripts.cs" + : prefix; + + return new RawString($"{type.Name}.cs"); + } + + public ScriptMethodInfo[] methodsAvailable(string name) => ScriptMethodInfo.GetMethodsAvailable(GetFilterType(name)); + } + + public class ScriptMethodInfo + { + public string Name { get; set; } + public string FirstParam { get; set; } + public string ReturnType { get; set; } + public int ParamCount { get; set; } + public string[] RemainingParams { get; set; } + + public static ScriptMethodInfo[] GetMethodsAvailable(Type filterType) + { + var filters = filterType.GetMethods(BindingFlags.Instance | BindingFlags.Public); + var to = filters + .OrderBy(x => x.Name) + .ThenBy(x => x.GetParameters().Count()) + .Where(x => x.DeclaringType != typeof(ScriptMethods) && x.DeclaringType != typeof(object)) + .Where(m => !m.IsSpecialName) + .Select(ScriptMethodInfo.Create); + + return to.ToArray(); + } + + public static ScriptMethodInfo Create(MethodInfo mi) + { + var paramNames = mi.GetParameters() + .Where(x => x.ParameterType != typeof(ScriptScopeContext)) + .Select(x => x.Name) + .ToArray(); + + var to = new ScriptMethodInfo { + Name = mi.Name, + FirstParam = paramNames.FirstOrDefault(), + ParamCount = paramNames.Length, + RemainingParams = paramNames.Length > 1 ? paramNames.Skip(1).ToArray() : new string[]{}, + ReturnType = mi.ReturnType?.Name, + }; + + return to; + } + + public string Return => ReturnType != null && ReturnType != nameof(StopExecution) ? " -> " + ReturnType : ""; + + public string Body => ParamCount == 0 + ? $"{Name}" + : ParamCount == 1 + ? $"|> {Name}" + : $"|> {Name}(" + string.Join(", ", RemainingParams) + $")"; + + public string Display => ParamCount == 0 + ? $"{Name}{Return}" + : ParamCount == 1 + ? $"{FirstParam} |> {Name}{Return}" + : $"{FirstParam} |> {Name}(" + string.Join(", ", RemainingParams) + $"){Return}"; + } +} diff --git a/src/CustomerServices.cs b/ServiceInterface/CustomerServices.cs similarity index 78% rename from src/CustomerServices.cs rename to ServiceInterface/CustomerServices.cs index fccc2b2..2ea394c 100644 --- a/src/CustomerServices.cs +++ b/ServiceInterface/CustomerServices.cs @@ -1,7 +1,7 @@ using ServiceStack; -using ServiceStack.Templates; +using ServiceStack.Script; -namespace TemplatePages +namespace SharpScript { [Route("/customers/{Id}")] public class ViewCustomer @@ -11,7 +11,7 @@ public class ViewCustomer public class CustomerServices : Service { - public ITemplatePages Pages { get; set; } + public ISharpPages Pages { get; set; } public object Any(ViewCustomer request) => new PageResult(Pages.GetPage("examples/customer")) { diff --git a/src/EmailTemplatesService.cs b/ServiceInterface/EmailTemplatesService.cs similarity index 92% rename from src/EmailTemplatesService.cs rename to ServiceInterface/EmailTemplatesService.cs index 0d9a778..c973de3 100644 --- a/src/EmailTemplatesService.cs +++ b/ServiceInterface/EmailTemplatesService.cs @@ -1,10 +1,9 @@ using System.Linq; -using System.Collections.Generic; using ServiceStack; -using ServiceStack.Templates; +using ServiceStack.Script; using ServiceStack.IO; -namespace TemplatePages +namespace SharpScript { [Route("/emails/order-confirmation/preview")] public class PreviewHtmlEmail : IReturn @@ -29,7 +28,7 @@ public object Any(PreviewHtmlEmail request) var customer = Customers.GetCustomer(request.PreviewCustomerId) ?? Customers.GetAllCustomers().First(); - var context = new TemplateContext { + var context = new ScriptContext { PageFormats = { new MarkdownPageFormat() }, Args = { ["customer"] = customer, diff --git a/ServiceInterface/GitHubMarkdownFilters.cs b/ServiceInterface/GitHubMarkdownFilters.cs new file mode 100644 index 0000000..030d1a5 --- /dev/null +++ b/ServiceInterface/GitHubMarkdownFilters.cs @@ -0,0 +1,159 @@ +using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; +using ServiceStack; +using ServiceStack.IO; +using ServiceStack.Script; +using ServiceStack.Text; + +namespace SharpScript +{ + public class GitHubMarkdownScripts : ScriptMethods + { + public string ApiBaseUrl { get; set; } = "https://api.github.com"; + + public bool UseMemoryCache { get; set; } = true; + + public string Mode { get; set; } = "gfm"; + + public string RepositoryContext { get; set; } + + public IRawString markdown(ScriptScopeContext scope, string markdown) + { + var html = MarkdownConfig.Transformer.Transform(markdown); + return html.ToRawString(); + } + + public static async Task TransformToHtml(Stream markdownStream) + { + var md = await markdownStream.ReadToEndAsync(); + var html = MarkdownConfig.Transformer.Transform(md); + return MemoryStreamFactory.GetStream(html.ToUtf8Bytes()); + } + + public static async Task convertScriptToCodeBlocks(Stream renderedHtmlMarkdownBlock) + { + var html = await renderedHtmlMarkdownBlock.ReadToEndAsync(); + html = html.Replace("<script>", + "```code") + .Replace("</script>", + "```") + .Replace("</script>", + "```") + .Replace("</script>", + "```"); + return MemoryStreamFactory.GetStream(html.ToUtf8Bytes()); + } + + public static async Task convertScriptToLispBlocks(Stream renderedHtmlMarkdownBlock) + { + var html = await renderedHtmlMarkdownBlock.ReadToEndAsync(); + html = html.Replace("<script>", + "```lisp") + .Replace("</script>", + "```") + .Replace("</script>", + "```") + .Replace("</script>", + "```"); + return MemoryStreamFactory.GetStream(html.ToUtf8Bytes()); + } + + static bool ReplaceUserContent = true; + + public async Task githubMarkdown(ScriptScopeContext scope, string markdownPath) + { + var file = Context.ProtectedMethods.ResolveFile(nameof(githubMarkdown), scope, markdownPath); + var htmlFilePath = file.VirtualPath.LastLeftPart('.') + ".html"; + var cacheKey = nameof(GitHubMarkdownScripts) + ">" + htmlFilePath; + + var htmlFile = Context.VirtualFiles.GetFile(htmlFilePath); + if (htmlFile != null && htmlFile.LastModified >= file.LastModified) + { + if (UseMemoryCache) + { + byte[] bytes; + if (!Context.Cache.TryGetValue(cacheKey, out object oBytes)) + { + using (var stream = htmlFile.OpenRead()) + { + var ms = MemoryStreamFactory.GetStream(); + using (ms) + { + await stream.CopyToAsync(ms); + ms.Position = 0; + bytes = ms.ToArray(); + Context.Cache[cacheKey] = bytes; + } + } + } + else + { + bytes = (byte[])oBytes; + } + scope.OutputStream.Write(bytes, 0, bytes.Length); + } + else + { + using (var htmlReader = htmlFile.OpenRead()) + { + await htmlReader.CopyToAsync(scope.OutputStream); + } + } + } + else + { + var ms = MemoryStreamFactory.GetStream(); + using (ms) + { + using (var stream = file.OpenRead()) + { + await stream.CopyToAsync(ms); + } + + ms.Position = 0; + var bytes = ms.ToArray(); + + var htmlBytes = RepositoryContext == null + ? await ApiBaseUrl.CombineWith("markdown", "raw") + .PostBytesToUrlAsync(bytes, contentType:MimeTypes.PlainText, requestFilter:x => x.With(c => c.UserAgent = "#Script")) + : await ApiBaseUrl.CombineWith("markdown") + .PostBytesToUrlAsync(new Dictionary { {"text", bytes.FromUtf8Bytes() }, {"mode", Mode}, {"context", RepositoryContext} }.ToJson().ToUtf8Bytes(), + contentType:MimeTypes.Json, requestFilter:x => x.With(c => c.UserAgent = "#Script")); + + byte[] wrappedBytes = null; + + if (ReplaceUserContent) + { + var html = htmlBytes.FromUtf8Bytes(); + html = html.Replace("user-content-",""); + wrappedBytes = ("
    " + html + "
    ").ToUtf8Bytes(); + } + else + { + var headerBytes = "
    ".ToUtf8Bytes(); + var footerBytes = "
    ".ToUtf8Bytes(); + + wrappedBytes = new byte[headerBytes.Length + htmlBytes.Length + footerBytes.Length]; + System.Buffer.BlockCopy(headerBytes, 0, wrappedBytes, 0, headerBytes.Length); + System.Buffer.BlockCopy(htmlBytes, 0, wrappedBytes, headerBytes.Length, htmlBytes.Length); + System.Buffer.BlockCopy(footerBytes, 0, wrappedBytes, headerBytes.Length + htmlBytes.Length, footerBytes.Length); + } + + if (Context.VirtualFiles is IVirtualFiles vfs) + { + var fs = vfs.GetFileSystemVirtualFiles(); + fs.DeleteFile(htmlFilePath); + fs.WriteFile(htmlFilePath, wrappedBytes); + } + + if (UseMemoryCache) + { + Context.Cache[cacheKey] = wrappedBytes; + } + await scope.OutputStream.WriteAsync(wrappedBytes, 0, wrappedBytes.Length); + } + } + } + } +} diff --git a/src/IntrospectStateServices.cs b/ServiceInterface/IntrospectStateServices.cs similarity index 81% rename from src/IntrospectStateServices.cs rename to ServiceInterface/IntrospectStateServices.cs index 1f1d2e9..0abb1bc 100644 --- a/src/IntrospectStateServices.cs +++ b/ServiceInterface/IntrospectStateServices.cs @@ -4,10 +4,9 @@ using System.Diagnostics; using System.Collections.Generic; using ServiceStack; -using ServiceStack.IO; -using ServiceStack.Templates; +using ServiceStack.Script; -namespace TemplatePages +namespace SharpScript { [Route("/introspect/state")] public class IntrospectState @@ -17,7 +16,7 @@ public class IntrospectState public string DriveInfo { get; set; } } - public class StateTemplateFilters : TemplateFilter + public class StateScriptMethods : ScriptMethods { bool HasAccess(Process process) { @@ -35,8 +34,8 @@ public class IntrospectStateServices : Service { public object Any(IntrospectState request) { - var context = new TemplateContext { - ScanTypes = { typeof(StateTemplateFilters) }, //Autowires (if needed) + var context = new ScriptContext { + ScanTypes = { typeof(StateScriptMethods) }, //Autowires (if needed) RenderExpressionExceptions = true }.Init(); diff --git a/src/LinqServices.cs b/ServiceInterface/LinqServices.cs similarity index 93% rename from src/LinqServices.cs rename to ServiceInterface/LinqServices.cs index ee09a5a..2422549 100644 --- a/src/LinqServices.cs +++ b/ServiceInterface/LinqServices.cs @@ -1,20 +1,19 @@ using System; using System.Linq; -using System.Collections; using System.Collections.Generic; -using System.IO; using ServiceStack; -using ServiceStack.Templates; +using ServiceStack.Script; using ServiceStack.IO; using ServiceStack.DataAnnotations; using System.Threading.Tasks; -namespace TemplatePages +namespace SharpScript { [Route("/linq/eval")] public class EvaluateLinq : IReturn { - public string Template { get; set; } + public string Code { get; set; } + public string Lang { get; set; } public Dictionary Files { get; set; } } @@ -30,12 +29,18 @@ public async Task Any(EvaluateLinq request) context.VirtualFiles.WriteFile(entry.Key, entry.Value); } - var pageResult = new PageResult(context.OneTimePage(request.Template)); + var page = request.Lang == "code" + ? context.CodeSharpPage(request.Code) + : request.Lang == "lisp" + ? context.LispSharpPage(request.Code) + : context.OneTimePage(request.Code); + + var pageResult = new PageResult(page); return await pageResult.RenderToStringAsync(); } } - public class AnagramEqualityComparer : IEqualityComparer + public class AnagramEqualityComparer : IEqualityComparer, IEqualityComparer { public bool Equals(string x, string y) => GetCanonicalString(x) == GetCanonicalString(y); public int GetHashCode(string obj) => GetCanonicalString(obj).GetHashCode(); @@ -45,6 +50,10 @@ private string GetCanonicalString(string word) Array.Sort(wordChars); return new string(wordChars); } + + public new bool Equals(object x, object y) => Equals((string) x, (string) y); + + public int GetHashCode(object obj) => GetHashCode((string)obj); } public class Customer diff --git a/src/ProductServices.cs b/ServiceInterface/ProductServices.cs similarity index 89% rename from src/ProductServices.cs rename to ServiceInterface/ProductServices.cs index df5cb07..3ee44d0 100644 --- a/src/ProductServices.cs +++ b/ServiceInterface/ProductServices.cs @@ -1,8 +1,8 @@ #if false //stay within free-quota limit using ServiceStack; -using ServiceStack.Templates; +using ServiceStack.Script; -namespace TemplatePages +namespace SharpScript { [Route("/products/view")] public class ViewProducts diff --git a/src/ProductsPage.cs b/ServiceInterface/ProductsPage.cs similarity index 87% rename from src/ProductsPage.cs rename to ServiceInterface/ProductsPage.cs index db7692d..0f8f667 100644 --- a/src/ProductsPage.cs +++ b/ServiceInterface/ProductsPage.cs @@ -1,13 +1,13 @@ using System.Linq; using System.Collections.Generic; using ServiceStack; -using ServiceStack.Templates; +using ServiceStack.Script; -namespace TemplatePages +namespace SharpScript { [Page("products")] [PageArg("title", "Products")] - public class ProductsPage : TemplateCodePage + public class ProductsPage : SharpCodePage { string render(Product[] products) => $@" diff --git a/ServiceInterface/ScriptServices.cs b/ServiceInterface/ScriptServices.cs new file mode 100644 index 0000000..f4c084e --- /dev/null +++ b/ServiceInterface/ScriptServices.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using ServiceStack; +using ServiceStack.Script; +using ServiceStack.IO; +using System.Threading.Tasks; +using System; +using ServiceStack.OrmLite; +using ServiceStack.Data; + +namespace SharpScript +{ + [Route("/pages/eval")] + public class EvaluateScripts : IReturn + { + public Dictionary Files { get; set; } + public Dictionary Args { get; set; } + + public string Page { get; set; } + } + + [Route("/template/eval")] + public class EvaluateScript + { + public string Template { get; set; } + } + + [Route("/expression/eval")] + public class EvalExpression : IReturn + { + public string Expression { get; set; } + } + + public class EvalExpressionResponse + { + public object Result { get; set; } + public string Tree { get; set; } + public ResponseStatus ResponseStatus { get; set; } + } + + [ReturnExceptionsInJson] + public class ScriptServices : Service + { + public async Task Any(EvaluateScripts request) + { + var context = new ScriptContext { + ScriptMethods = { + new ProtectedScripts(), + }, + ExcludeFiltersNamed = { "fileWrite","fileAppend","fileDelete","dirDelete" } + }.Init(); + + foreach (var entry in request.Files.Safe()) + { + context.VirtualFiles.WriteFile(entry.Key, entry.Value); + } + + var pageResult = new PageResult(context.GetPage(request.Page ?? "page")); + + foreach (var entry in request.Args.Safe()) + { + pageResult.Args[entry.Key] = entry.Value; + } + + return await pageResult.RenderToStringAsync(); // render to string so [ReturnExceptionsInJson] can detect Exceptions and return JSON + } + + public async Task Any(EvaluateScript request) + { + var context = new ScriptContext { + DebugMode = false, + ScriptLanguages = { ScriptLisp.Language }, + ScriptMethods = { + new DbScriptsAsync(), + new AutoQueryScripts(), + new ServiceStackScripts(), + new CustomScriptMethods(), + }, + Plugins = { + new ServiceStackScriptBlocks(), + new MarkdownScriptPlugin(), + } + }; + //Register any dependencies filters need: + context.Container.AddSingleton(() => base.GetResolver().TryResolve()); + context.Init(); + var pageResult = new PageResult(context.OneTimePage(request.Template)) + { + Args = base.Request.GetScriptRequestParams(importRequestParams:true) + }; + return await pageResult.RenderToStringAsync(); // render to string so [ReturnExceptionsInJson] can detect Exceptions and return JSON + } + + public object Any(EvalExpression request) + { + if (string.IsNullOrWhiteSpace(request.Expression)) + return new EvalExpressionResponse(); + + var args = new Dictionary(); + foreach (String name in Request.QueryString.AllKeys) + { + if (name.EqualsIgnoreCase("expression")) + continue; + + var argExpr = Request.QueryString[name]; + var argValue = JS.eval(argExpr); + args[name] = argValue; + } + + var scope = JS.CreateScope(args: args); + var expr = JS.expression(request.Expression.Trim()); + + var response = new EvalExpressionResponse { + Result = ScriptLanguage.UnwrapValue(expr.Evaluate(scope)), + Tree = expr.ToJsAstString(), + }; + return response; + } + } + +} \ No newline at end of file diff --git a/ServiceInterface/ScriptTypes.cs b/ServiceInterface/ScriptTypes.cs new file mode 100644 index 0000000..c839c32 --- /dev/null +++ b/ServiceInterface/ScriptTypes.cs @@ -0,0 +1,103 @@ +using System.Text; + +namespace SharpScript +{ + public class Ints + { + public Ints(int a, int b) + { + A = a; + B = b; + } + public int A { get; set; } + public int B { get; set; } + public int C { get; set; } + public int D { get; set; } + public int GetTotal() => A + B + C + D; + + public string GenericMethod() => typeof(T).Name + " " + GetTotal(); + + public string GenericMethod(T value) => typeof(T).Name + $" {value} " + GetTotal(); + } + + public class Adder + { + public string String { get; set; } + public double Double { get; set; } + + public Adder(string str) => String = str; + public Adder(double num) => Double = num; + + public string Add(string str) => String += str; + public double Add(double num) => Double += num; + + public override string ToString() => String != null ? $"string: {String}" : $"double: {Double}"; + } + + public class StaticLog + { + static StringBuilder sb = new StringBuilder(); + + public static void Log(string message) => sb.Append(message); + + public static void Log(string message) => sb.Append(typeof(T).Name + " " + message); + + public static string AllLogs() => sb.ToString(); + + public static void Clear() => sb.Clear(); + + public static string Prop { get; } = "StaticLog.Prop"; + public static string Field = "StaticLog.Field"; + public const string Const = "StaticLog.Const"; + + public string InstanceProp { get; } = "StaticLog.InstanceProp"; + public string InstanceField = "StaticLog.InstanceField"; + + public class Inner1 + { + public static string Prop1 { get; } = "StaticLog.Inner1.Prop1"; + public static string Field1 = "StaticLog.Inner1.Field1"; + public const string Const1 = "StaticLog.Inner1.Const1"; + + public string InstanceProp1 { get; } = "StaticLog.Inner1.InstanceProp1"; + public string InstanceField1 = "StaticLog.Inner1.InstanceField1"; + + public static class Inner2 + { + public static string Prop2 { get; } = "StaticLog.Inner1.Inner2.Prop2"; + public static string Field2 = "StaticLog.Inner1.Inner2.Field2"; + public const string Const2 = "StaticLog.Inner1.Inner2.Const2"; + } + } + } + + public class InstanceLog + { + private readonly string prefix; + public InstanceLog(string prefix) => this.prefix = prefix; + + StringBuilder sb = new StringBuilder(); + + public void Log(string message) => sb.Append(prefix + " " + message); + + public void Log(string message) => sb.Append(prefix + " " + typeof(T2).Name + " " + message); + + public string AllLogs() => sb.ToString(); + + public void Clear() => sb.Clear(); + } + + public class GenericStaticLog + { + static StringBuilder sb = new StringBuilder(); + + public static void Log(string message) => sb.Append(typeof(T).Name + " " + message); + + public static void Log(string message) => sb.Append(typeof(T).Name + " " + typeof(T2).Name + " " + message); + + public static string AllLogs() => sb.ToString(); + + public static void Clear() => sb.Clear(); + } + +} diff --git a/SharpScript.csproj b/SharpScript.csproj new file mode 100644 index 0000000..d14d377 --- /dev/null +++ b/SharpScript.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + SharpScript + SharpScript + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..ff69491 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,103 @@ +# Name of your application. Used to uniquely configure containers. +service: sharpscript + +# Name of the container image. +image: servicestack/sharpscript + +# Deploy to these servers. +servers: + web: + - 5.78.128.205 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: sharpscript.net + # Proxy connects to your container on port 80 by default. + app_port: 8080 + healthcheck: + interval: 3 + path: /metadata + timeout: 3 + +# Credentials for your image host. +registry: + # Specify the registry server, if you're not using Docker Hub + server: ghcr.io + username: + - KAMAL_REGISTRY_USERNAME + + # Always use an access token rather than real password (pulled from .kamal/secrets). + password: + - KAMAL_REGISTRY_PASSWORD + +# Configure builder setup. +builder: + arch: amd64 + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +# +# env: +# clear: +# DB_HOST: 192.168.0.2 +# secret: +# - RAILS_MASTER_KEY + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +# +# aliases: +# shell: app exec --interactive --reuse "bash" + +# Use a different ssh user than root +# +# ssh: +# user: app + +# Use a persistent storage volume. +# +volumes: + - "/opt/docker/sharpscript/App_Data:/app/App_Data" + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +# +# asset_path: /app/public/assets + +# Configure rolling deploys by setting a wait time between batches of restarts. +# +# boot: +# limit: 10 # Can also specify as a percentage of total hosts, such as "25%" +# wait: 2 + +# Use accessory services (secrets come from .kamal/secrets). +# +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# port: 3306 +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: valkey/valkey:8 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/deploy-envs.sh b/deploy-envs.sh deleted file mode 100644 index 6a7a007..0000000 --- a/deploy-envs.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# set environment variables used in deploy.sh and AWS task-definition.json: -export IMAGE_NAME=netcoreapps-templates -export IMAGE_VERSION=latest - -export AWS_DEFAULT_REGION=us-east-1 -export AWS_ECS_CLUSTER_NAME=default -export AWS_VIRTUAL_HOST=templates.servicestack.net - -# set any sensitive information in travis-ci encrypted project settings: -# required: AWS_ACCOUNT_NUMBER, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY -# optional: SERVICESTACK_LICENSE diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100644 index 0259d62..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -source ../deploy-envs.sh - -#AWS_ACCOUNT_NUMBER={} set in private variable -export AWS_ECS_REPO_DOMAIN=$AWS_ACCOUNT_NUMBER.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com - -# Build process -docker build -t $IMAGE_NAME ../ -docker tag $IMAGE_NAME $AWS_ECS_REPO_DOMAIN/$IMAGE_NAME:$IMAGE_VERSION diff --git a/scripts/deploy.sh b/scripts/deploy.sh deleted file mode 100644 index c8563b7..0000000 --- a/scripts/deploy.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -source ../deploy-envs.sh - -export AWS_ECS_REPO_DOMAIN=$AWS_ACCOUNT_NUMBER.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com -export ECS_SERVICE=$IMAGE_NAME-service -export ECS_TASK=$IMAGE_NAME-task - -# install dependencies -sudo apt-get install jq -y #install jq for json parsing -sudo apt-get install gettext -y -pip install --user awscli # install aws cli w/o sudo -export PATH=$PATH:$HOME/.local/bin # put aws in the path - -# replace environment variables in task-definition -envsubst < task-definition.json > new-task-definition.json - -eval $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email | sed 's|https://||') #needs AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY envvars - -if [ $(aws ecr describe-repositories | jq --arg x $IMAGE_NAME '[.repositories[] | .repositoryName == $x] | any') == "true" ]; then - echo "Found ECS Repository $IMAGE_NAME" -else - echo "ECS Repository doesn't exist, Creating $IMAGE_NAME ..." - aws ecr create-repository --repository-name $IMAGE_NAME -fi - -docker push $AWS_ECS_REPO_DOMAIN/$IMAGE_NAME:$IMAGE_VERSION - -aws ecs register-task-definition --cli-input-json file://new-task-definition.json --region $AWS_DEFAULT_REGION > /dev/null # Create a new task revision -TASK_REVISION=$(aws ecs describe-task-definition --task-definition $ECS_TASK --region $AWS_DEFAULT_REGION | jq '.taskDefinition.revision') #get latest revision -SERVICE_ARN="arn:aws:ecs:$AWS_DEFAULT_REGION:$AWS_ACCOUNT_NUMBER:service/$ECS_SERVICE" -ECS_SERVICE_EXISTS=$(aws ecs list-services --region $AWS_DEFAULT_REGION --cluster $AWS_ECS_CLUSTER_NAME | jq '.serviceArns' | jq 'contains(["'"$SERVICE_ARN"'"])') -if [ "$ECS_SERVICE_EXISTS" == "true" ]; then - echo "ECS Service already exists, Updating $ECS_SERVICE ..." - aws ecs update-service --cluster $AWS_ECS_CLUSTER_NAME --service $ECS_SERVICE --task-definition "$ECS_TASK:$TASK_REVISION" --desired-count 1 --region $AWS_DEFAULT_REGION > /dev/null #update service with latest task revision -else - echo "Creating ECS Service $ECS_SERVICE ..." - aws ecs create-service --cluster $AWS_ECS_CLUSTER_NAME --service-name $ECS_SERVICE --task-definition "$ECS_TASK:$TASK_REVISION" --desired-count 1 --region $AWS_DEFAULT_REGION > /dev/null #create service -fi -if [ "$(aws ecs list-tasks --service-name $ECS_SERVICE --region $AWS_DEFAULT_REGION | jq '.taskArns' | jq 'length')" -gt "0" ]; then - TEMP_ARN=$(aws ecs list-tasks --service-name $ECS_SERVICE --region $AWS_DEFAULT_REGION | jq '.taskArns[0]') # Get current running task ARN - TASK_ARN="${TEMP_ARN%\"}" # strip double quotes - TASK_ARN="${TASK_ARN#\"}" # strip double quotes - aws ecs stop-task --task $TASK_ARN --region $AWS_DEFAULT_REGION > /dev/null # Stop current task to force start of new task revision with new image -fi diff --git a/scripts/task-definition.json b/scripts/task-definition.json deleted file mode 100644 index c6f6ce6..0000000 --- a/scripts/task-definition.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "family": "${ECS_TASK}", - "networkMode": "bridge", - "containerDefinitions": [ - { - "image": "${AWS_ECS_REPO_DOMAIN}/${IMAGE_NAME}:${IMAGE_VERSION}", - "name": "${IMAGE_NAME}", - "cpu": 128, - "memory": 256, - "essential": true, - "portMappings": [ - { - "containerPort": 5000, - "hostPort": 0, - "protocol": "tcp" - } - ], - "environment": [ - { - "name": "VIRTUAL_HOST", - "value": "${AWS_VIRTUAL_HOST}" - } - ] - } - ] -} \ No newline at end of file diff --git a/src/AppHost.cs b/src/AppHost.cs deleted file mode 100644 index 76c7167..0000000 --- a/src/AppHost.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.IO; -using System.Collections.Generic; -using System.Linq; -using System.Collections; -using ServiceStack; -using ServiceStack.IO; -using ServiceStack.Templates; -using ServiceStack.Text; -using ServiceStack.Data; -using ServiceStack.OrmLite; -using ServiceStack.Configuration; -using Funq; - -namespace TemplatePages -{ - public class AppHost : AppHostBase - { - public AppHost() - : base("Template Pages", typeof(TemplateServices).Assembly) { } - - public TemplateContext LinqContext; - - public override void Configure(Container container) - { - ServiceStack.Memory.NetCoreMemory.Configure(); - - SetConfig(new HostConfig { - DebugMode = AppSettings.Get("DebugMode", Env.IsWindows), - }); - - Plugins.Add(new ServiceStack.Api.OpenApi.OpenApiFeature()); - - var path = MapProjectPath("~/wwwroot/assets/js/customers.json"); - var json = File.ReadAllText(path); - TemplateQueryData.Customers = json.FromJson>(); - - container.Register(c => new Customers(TemplateQueryData.Customers)); - container.Register(c => new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider)); - - using (var db = container.Resolve().Open()) - { - db.CreateTable(); - db.CreateTable(); - db.CreateTable(); - TemplateQueryData.Customers.Each(x => db.Save(x, references:true)); - db.InsertAll(TemplateQueryData.Products); - - db.CreateTable(); - db.Insert(new Quote { Id = 1, Url = "https://gist.githubusercontent.com/gistlyn/0ab7494dfbff78466ef622d501662027/raw/b3dd1e9f8e82c169a32829071aa7f761c6494843/quote.md" }); - } - - Plugins.Add(new AutoQueryFeature { MaxLimit = 100 }); - - Plugins.Add(new AutoQueryDataFeature { MaxLimit = 100 } - .AddDataSource(ctx => ctx.ServiceSource(ctx.Dto.ConvertTo(), - HostContext.Cache, TimeSpan.FromMinutes(10))) - ); - - var customFilters = new CustomTemplateFilters(); - Plugins.Add(new TemplatePagesFeature { - TemplateFilters = { - customFilters, - new TemplateDbFiltersAsync() - }, - Args = { - ["products"] = TemplateQueryData.Products - }, - RenderExpressionExceptions = true, - MetadataDebugAdminRole = RoleNames.AllowAnon, - ExcludeFiltersNamed = { "dbExec" } - }); - - AfterInitCallbacks.Add(host => { - var feature = GetPlugin(); - - var files = GetVirtualFileSources().First(x => x is FileSystemVirtualFiles); - foreach (var file in files.GetDirectory("docs").GetAllMatchingFiles("*.html")) - { - var page = feature.GetPage(file.VirtualPath).Init().Result; - if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) - { - customFilters.DocsIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); - } - } - - foreach (var file in files.GetDirectory("linq").GetAllMatchingFiles("*.html")) - { - var page = feature.GetPage(file.VirtualPath).Init().Result; - if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) - { - customFilters.LinqIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); - } - } - - foreach (var file in files.GetDirectory("usecases").GetAllMatchingFiles("*.html")) - { - var page = feature.GetPage(file.VirtualPath).Init().Result; - if (page.Args.TryGetValue("order", out object order) && page.Args.TryGetValue("title", out object title)) - { - customFilters.UseCasesIndex[int.Parse((string)order)] = new KeyValuePair(GetPath(file.VirtualPath), (string)title); - } - } - - LinqContext = new TemplateContext { - Args = { - [TemplateConstants.DefaultDateFormat] = "yyyy/MM/dd", - ["products"] = TemplateQueryData.Products, - ["customers"] = TemplateQueryData.Customers, - ["comparer"] = new CaseInsensitiveComparer(), - ["anagramComparer"] = new AnagramEqualityComparer(), - } - }.Init(); - }); - } - - public string GetPath(string virtualPath) - { - var path = "/" + virtualPath.LastLeftPart('.'); - if (path.EndsWith("/index")) - path = path.Substring(0, path.Length - "index".Length); - - return path; - } - } - - public class Quote - { - public int Id { get; set; } - public string Url { get; set; } - } -} diff --git a/src/CustomTemplateFilters.cs b/src/CustomTemplateFilters.cs deleted file mode 100644 index 14690af..0000000 --- a/src/CustomTemplateFilters.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System.Threading.Tasks; -using ServiceStack; -using ServiceStack.Templates; -using System.Collections.Generic; -using System.Linq; -using System.IO; -using System.Diagnostics; -using System; -using ServiceStack.Redis; -using ServiceStack.OrmLite; -using System.Reflection; - -namespace TemplatePages -{ - public class CustomTemplateFilters : TemplateFilter - { - public Dictionary> DocsIndex { get; } = new Dictionary>(); - public Dictionary> LinqIndex { get; } = new Dictionary>(); - public Dictionary> UseCasesIndex { get; } = new Dictionary>(); - - public object prevDocLink(int order) - { - if (DocsIndex.TryGetValue(order - 1, out KeyValuePair entry)) - return entry; - return null; - } - - public object nextDocLink(int order) - { - if (DocsIndex.TryGetValue(order + 1, out KeyValuePair entry)) - return entry; - return null; - } - - public object prevLinqLink(int order) - { - if (LinqIndex.TryGetValue(order - 1, out KeyValuePair entry)) - return entry; - return null; - } - - public object nextLinqLink(int order) - { - if (LinqIndex.TryGetValue(order + 1, out KeyValuePair entry)) - return entry; - return null; - } - - public object prevUseCaseLink(int order) - { - if (UseCasesIndex.TryGetValue(order - 1, out KeyValuePair entry)) - return entry; - return null; - } - - public object nextUseCaseLink(int order) - { - if (UseCasesIndex.TryGetValue(order + 1, out KeyValuePair entry)) - return entry; - return null; - } - - List> sortedDocLinks; - public object docLinks() => sortedDocLinks ?? (sortedDocLinks = sortLinks(DocsIndex)); - - List> sortedLinqLinks; - public object linqLinks() => sortedLinqLinks ?? (sortedLinqLinks = sortLinks(LinqIndex)); - - List> sorteUseCaseLinks; - public object useCaseLinks() => sorteUseCaseLinks ?? (sorteUseCaseLinks = sortLinks(UseCasesIndex)); - - public List> sortLinks(Dictionary> links) - { - var sortedKeys = links.Keys.ToList(); - sortedKeys.Sort(); - - var to = new List>(); - - foreach (var key in sortedKeys) - { - var entry = links[key]; - to.Add(entry); - } - - return to; - } - - public async Task includeContentFile(TemplateScopeContext scope, string virtualPath) - { - var file = HostContext.VirtualFiles.GetFile(virtualPath); - if (file == null) - throw new FileNotFoundException($"includeContentFile '{virtualPath}' was not found"); - - using (var reader = file.OpenRead()) - { - await reader.CopyToAsync(scope.OutputStream); - } - } - - public List customers() => TemplateQueryData.Customers; - - public Process[] processes => Process.GetProcesses(); - public Process[] processesByName(string name) => Process.GetProcessesByName(name); - public Process processById(int processId) => Process.GetProcessById(processId); - public Process currentProcess() => Process.GetCurrentProcess(); - - Type GetFilterType(string name) - { - switch(name) - { - case nameof(TemplateDefaultFilters): - return typeof(TemplateDefaultFilters); - case nameof(TemplateHtmlFilters): - return typeof(TemplateHtmlFilters); - case nameof(TemplateProtectedFilters): - return typeof(TemplateProtectedFilters); - case nameof(TemplateInfoFilters): - return typeof(TemplateInfoFilters); - case nameof(TemplateRedisFilters): - return typeof(TemplateRedisFilters); - case nameof(TemplateDbFilters): - return typeof(TemplateDbFilters); - case nameof(TemplateDbFiltersAsync): - return typeof(TemplateDbFiltersAsync); - case nameof(TemplateServiceStackFilters): - return typeof(TemplateServiceStackFilters); - case nameof(TemplateAutoQueryFilters): - return typeof(TemplateAutoQueryFilters); - } - - throw new NotSupportedException("Unknown Filter: " + name); - } - - public IRawString filterLinkToSrc(string name) - { - const string prefix = "https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Templates/Filters/"; - - var type = GetFilterType(name); - var url = type == typeof(TemplateDefaultFilters) - ? prefix - : type == typeof(TemplateHtmlFilters) || type == typeof(TemplateProtectedFilters) - ? $"{prefix}{type.Name}.cs" - : type == typeof(TemplateInfoFilters) - ? "https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/TemplateInfoFilters.cs" - : type == typeof(TemplateRedisFilters) - ? "https://github.com/ServiceStack/ServiceStack.Redis/blob/master/src/ServiceStack.Redis/TemplateRedisFilters.cs" - : type == typeof(TemplateDbFilters) || type == typeof(TemplateDbFiltersAsync) - ? $"https://github.com/ServiceStack/ServiceStack.OrmLite/tree/master/src/ServiceStack.OrmLite/{type.Name}.cs" - : type == typeof(TemplateServiceStackFilters) - ? "https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/TemplateServiceStackFilters.cs" - : prefix; - - return new RawString($"{type.Name}.cs"); - } - - public FilterInfo[] filtersAvailable(string name) - { - var filterType = GetFilterType(name); - var filters = filterType.GetMethods(BindingFlags.Instance | BindingFlags.Public); - var to = filters - .OrderBy(x => x.Name) - .ThenBy(x => x.GetParameters().Count()) - .Where(x => x.DeclaringType != typeof(TemplateFilter) && x.DeclaringType != typeof(object)) - .Where(m => !m.IsSpecialName) - .Select(x => FilterInfo.Create(x)); - - return to.ToArray(); - } - } - - public class FilterInfo - { - public string Name { get; set; } - public string FirstParam { get; set; } - public string ReturnType { get; set; } - public int ParamCount { get; set; } - public string[] RemainingParams { get; set; } - - public static FilterInfo Create(MethodInfo mi) - { - var paramNames = mi.GetParameters() - .Where(x => x.ParameterType != typeof(TemplateScopeContext)) - .Select(x => x.Name) - .ToArray(); - - var to = new FilterInfo { - Name = mi.Name, - FirstParam = paramNames.FirstOrDefault(), - ParamCount = paramNames.Length, - RemainingParams = paramNames.Length > 1 ? paramNames.Skip(1).ToArray() : new string[]{}, - ReturnType = mi.ReturnType?.Name, - }; - - return to; - } - - public string Return => ReturnType != null && ReturnType != nameof(StopExecution) ? " -> " + ReturnType : ""; - - public string Body => ParamCount == 0 - ? $"{Name}" - : ParamCount == 1 - ? $"| {Name}" - : $"| {Name}(" + string.Join(", ", RemainingParams) + $")"; - - public string Display => ParamCount == 0 - ? $"{Name}{Return}" - : ParamCount == 1 - ? $"{FirstParam} | {Name}{Return}" - : $"{FirstParam} | {Name}(" + string.Join(", ", RemainingParams) + $"){Return}"; - } -} diff --git a/src/GitHubMarkdownFilters.cs b/src/GitHubMarkdownFilters.cs deleted file mode 100644 index 5c4e968..0000000 --- a/src/GitHubMarkdownFilters.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Diagnostics; -using System.Threading.Tasks; -using System.Collections.Generic; -using ServiceStack; -using ServiceStack.IO; -using ServiceStack.Text; -using ServiceStack.Templates; - -namespace TemplatePages -{ - public class GitHubMarkdownFilters : TemplateFilter - { - public string ApiBaseUrl { get; set; } = "https://api.github.com"; - - public bool UseMemoryCache { get; set; } = true; - - public string Mode { get; set; } = "gfm"; - - public string RepositoryContext { get; set; } - - public IRawString markdown(TemplateScopeContext scope, string markdown) - { - var html = MarkdownConfig.Transformer.Transform(markdown); - return html.ToRawString(); - } - - public async Task githubMarkdown(TemplateScopeContext scope, string markdownPath) - { - var file = Context.ProtectedFilters.ResolveFile(nameof(githubMarkdown), scope, markdownPath); - var htmlFilePath = file.VirtualPath.LastLeftPart('.') + ".html"; - var cacheKey = nameof(GitHubMarkdownFilters) + ">" + htmlFilePath; - - var htmlFile = Context.VirtualFiles.GetFile(htmlFilePath); - if (htmlFile != null && htmlFile.LastModified >= file.LastModified) - { - if (UseMemoryCache) - { - byte[] bytes; - if (!Context.Cache.TryGetValue(cacheKey, out object oBytes)) - { - using (var stream = htmlFile.OpenRead()) - { - var ms = MemoryStreamFactory.GetStream(); - using (ms) - { - await stream.CopyToAsync(ms); - ms.Position = 0; - bytes = ms.ToArray(); - Context.Cache[cacheKey] = bytes; - } - } - } - else - { - bytes = (byte[])oBytes; - } - scope.OutputStream.Write(bytes, 0, bytes.Length); - } - else - { - using (var htmlReader = htmlFile.OpenRead()) - { - await htmlReader.CopyToAsync(scope.OutputStream); - } - } - } - else - { - var ms = MemoryStreamFactory.GetStream(); - using (ms) - { - using (var stream = file.OpenRead()) - { - await stream.CopyToAsync(ms); - } - - ms.Position = 0; - var bytes = ms.ToArray(); - - var htmlBytes = RepositoryContext == null - ? await ApiBaseUrl.CombineWith("markdown", "raw") - .PostBytesToUrlAsync(bytes, contentType:MimeTypes.PlainText, requestFilter:x => x.UserAgent = "TemplatePages") - : await ApiBaseUrl.CombineWith("markdown") - .PostBytesToUrlAsync(new Dictionary { {"text", bytes.FromUtf8Bytes() }, {"mode", Mode}, {"context", RepositoryContext} }.ToJson().ToUtf8Bytes(), - contentType:MimeTypes.Json, requestFilter:x => x.UserAgent = "TemplatePages"); - - var headerBytes = "
    ".ToUtf8Bytes(); - var footerBytes = "
    ".ToUtf8Bytes(); - - var wrappedBytes = new byte[headerBytes.Length + htmlBytes.Length + footerBytes.Length]; - System.Buffer.BlockCopy(headerBytes, 0, wrappedBytes, 0, headerBytes.Length); - System.Buffer.BlockCopy(htmlBytes, 0, wrappedBytes, headerBytes.Length, htmlBytes.Length); - System.Buffer.BlockCopy(footerBytes, 0, wrappedBytes, headerBytes.Length + htmlBytes.Length, footerBytes.Length); - - if (Context.VirtualFiles is IVirtualFiles vfs) - { - vfs.WriteFile(htmlFilePath, wrappedBytes); - } - - if (UseMemoryCache) - { - Context.Cache[cacheKey] = wrappedBytes; - } - await scope.OutputStream.WriteAsync(wrappedBytes, 0, wrappedBytes.Length); - } - } - } - } -} diff --git a/src/NuGet.Config b/src/NuGet.Config deleted file mode 100644 index c00d271..0000000 --- a/src/NuGet.Config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs deleted file mode 100644 index e611be9..0000000 --- a/src/Program.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; - -namespace TemplatePages -{ - public class Program - { - public static void Main(string[] args) - { - BuildWebHost(args).Run(); - } - - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .UseUrls("http://*:5000/") - .Build(); - } -} diff --git a/src/Startup.cs b/src/Startup.cs deleted file mode 100644 index 8210a07..0000000 --- a/src/Startup.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using ServiceStack; -using System.Net; -using System.Web; - -namespace TemplatePages -{ - public class Startup - { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - var cultureInfo = new System.Globalization.CultureInfo("en-US"); - System.Globalization.CultureInfo.DefaultThreadCurrentCulture = cultureInfo; - System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; - - loggerFactory.AddConsole(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseServiceStack(new AppHost()); - } - } -} diff --git a/src/TemplatePages.csproj b/src/TemplatePages.csproj deleted file mode 100644 index 03e9e05..0000000 --- a/src/TemplatePages.csproj +++ /dev/null @@ -1,38 +0,0 @@ - - - - netcoreapp2.1 - TemplatePages - TemplatePages - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/TemplateServices.cs b/src/TemplateServices.cs deleted file mode 100644 index 4e1b247..0000000 --- a/src/TemplateServices.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Collections.Generic; -using ServiceStack; -using ServiceStack.Templates; -using ServiceStack.IO; -using System.Threading.Tasks; -using System; -using ServiceStack.Web; -using ServiceStack.OrmLite; -using ServiceStack.Data; -using ServiceStack.Text; - -namespace TemplatePages -{ - [Route("/pages/eval")] - public class EvaluateTemplates : IReturn - { - public Dictionary Files { get; set; } - public Dictionary Args { get; set; } - - public string Page { get; set; } - } - - [Route("/template/eval")] - public class EvaluateTemplate - { - public string Template { get; set; } - } - - [Route("/expression/eval")] - public class EvalExpression : IReturn - { - public string Expression { get; set; } - } - - public class EvalExpressionResponse - { - public object Result { get; set; } - public string Tree { get; set; } - public ResponseStatus ResponseStatus { get; set; } - } - - [ReturnExceptionsInJson] - public class TemplateServices : Service - { - public async Task Any(EvaluateTemplates request) - { - var context = new TemplateContext { - TemplateFilters = { - new TemplateProtectedFilters(), - }, - ExcludeFiltersNamed = { "fileWrite","fileAppend","fileDelete","dirDelete" } - }.Init(); - - foreach (var entry in request.Files.Safe()) - { - context.VirtualFiles.WriteFile(entry.Key, entry.Value); - } - - var pageResult = new PageResult(context.GetPage(request.Page ?? "page")); - - foreach (var entry in request.Args.Safe()) - { - pageResult.Args[entry.Key] = entry.Value; - } - - return await pageResult.RenderToStringAsync(); // render to string so [ReturnExceptionsInJson] can detect Exceptions and return JSON - } - - public async Task Any(EvaluateTemplate request) - { - var context = new TemplateContext { - TemplateFilters = { - new TemplateDbFilters(), - new TemplateAutoQueryFilters(), - new TemplateServiceStackFilters(), - new CustomTemplateFilters(), - } - }; - //Register any dependencies filters need: - context.Container.AddSingleton(() => base.GetResolver().TryResolve()); - context.Init(); - var pageResult = new PageResult(context.OneTimePage(request.Template)) - { - Args = base.Request.GetTemplateRequestParams(importRequestParams:true) - }; - return await pageResult.RenderToStringAsync(); // render to string so [ReturnExceptionsInJson] can detect Exceptions and return JSON - } - - public object Any(EvalExpression request) - { - if (string.IsNullOrWhiteSpace(request.Expression)) - return new EvalExpressionResponse(); - - var args = new Dictionary(); - foreach (String name in Request.QueryString.AllKeys) - { - if (name.EqualsIgnoreCase("expression")) - continue; - - var argExpr = Request.QueryString[name]; - var argValue = JS.eval(argExpr); - args[name] = argValue; - } - - var scope = JS.CreateScope(args: args); - var expr = JS.expression(request.Expression.Trim()); - - var response = new EvalExpressionResponse { - Result = expr.Evaluate(scope), - Tree = expr.ToJsAstString(), - }; - return response; - } - } - -} \ No newline at end of file diff --git a/src/web.config b/src/web.config deleted file mode 100644 index dc0514f..0000000 --- a/src/web.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - diff --git a/src/wwwroot/_layout.html b/src/wwwroot/_layout.html deleted file mode 100644 index 478fbbb..0000000 --- a/src/wwwroot/_layout.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - {{ title }} - - - - - - - {{ ifDebug | select: }} - - - - - {{ eq(PathInfo,'/') | ifShow: Fork me on GitHub - | raw }} - - -
    - -
    - - {{ "sidebar" | partial }} - -
    -

    {{ title }}

    - {{ page }} -
    - -
    - -

    made with by ServiceStack

    - - - - - - - - {{ scripts | ifExists }} - - - - - - - \ No newline at end of file diff --git a/src/wwwroot/assets/img/logo-32.png b/src/wwwroot/assets/img/logo-32.png deleted file mode 100644 index 08e81b4..0000000 Binary files a/src/wwwroot/assets/img/logo-32.png and /dev/null differ diff --git a/src/wwwroot/assets/img/screenshot.png b/src/wwwroot/assets/img/screenshot.png deleted file mode 100644 index 42aba26..0000000 Binary files a/src/wwwroot/assets/img/screenshot.png and /dev/null differ diff --git a/src/wwwroot/assets/img/screenshots/ssvs-bootstrap.png b/src/wwwroot/assets/img/screenshots/ssvs-bootstrap.png deleted file mode 100644 index 364fccf..0000000 Binary files a/src/wwwroot/assets/img/screenshots/ssvs-bootstrap.png and /dev/null differ diff --git a/src/wwwroot/assets/img/screenshots/templates-bootstrap.png b/src/wwwroot/assets/img/screenshots/templates-bootstrap.png deleted file mode 100644 index f19985d..0000000 Binary files a/src/wwwroot/assets/img/screenshots/templates-bootstrap.png and /dev/null differ diff --git a/src/wwwroot/assets/js/default.js b/src/wwwroot/assets/js/default.js deleted file mode 100644 index 34b5113..0000000 --- a/src/wwwroot/assets/js/default.js +++ /dev/null @@ -1,142 +0,0 @@ -$(".live-pages").each(function(){ - - var el = $(this) - el.find("textarea").on("input", function(){ - var page = el.data("page") - var files = {} - - el.find(".files section").each(function(){ - var name = $.trim($(this).find("h5").html()) - var contents = $(this).find("textarea").val() - files[name] = contents - }) - - var request = { files: files, page: page } - - $.ajax({ - type: "POST", - url: "/pages/eval", - data: JSON.stringify(request), - contentType: "application/json", - dataType: "html" - }).done(function(data){ - el.removeClass('error').find(".output").html(data) - }).fail(function(jqxhr){ handleError(el, jqxhr) }) - }) - .trigger("input") - -}) - -$(".live-template").each(function(){ - - var el = $(this) - el.find("textarea").on("input", function(){ - - var request = { template: el.find("textarea").val() } - - $.ajax({ - type: "POST", - url: "/template/eval" + location.search, - data: JSON.stringify(request), - contentType: "application/json", - dataType: "html" - }).done(function(data){ - el.removeClass('error').find(".output").html(data) - }).fail(function(jqxhr){ handleError(el, jqxhr) }) - }) - .trigger("input") - -}) - -$(".linq-preview").each(function(){ - var files = {} - - var el = $(this) - el.find("textarea").on("input", function(){ - var files = {} - - el.find(".files section").each(function(){ - var name = $.trim($(this).find("h5").html()) - var contents = $(this).find("textarea").val() - files[name] = contents - }) - - var request = { template: el.find(".template textarea").val(), files: files } - - $.ajax({ - type: "POST", - url: "/linq/eval", - data: JSON.stringify(request), - contentType: "application/json", - dataType: "html" - }).done(function(data){ - el.removeClass('error').find(".output").html(data); - }).fail(function(jqxhr){ handleError(el, jqxhr) }) - }) - .trigger("input") - -}) - -$("#content h2,#content h3,#content h4").each(function(){ - var el = $(this); - - var text = el.html(); - if (text.indexOf("<") >= 0) return; - - if (!el.attr('id')) { - var safeName = text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9_-]+/g,"") - el.attr('id', safeName) - } - - el.on('click', function(){ - var id = el.attr('id') - location.href = "#" + id - }) -}) - -$.fn.ajaxPreview = function(opt) { - var inputs = this.find("input,textarea"); - inputs.on("input", function(){ - var f = $(this).closest("form"); - var data = {}; - inputs.each(function(){ data[this.name] = this.value }) - $.ajax({ - url: f.attr('action'), - method: "POST", - data: JSON.stringify(data), - contentType: 'application/json', - dataType: opt.dataType || 'json', - success: opt.success, - error: opt.error || function(jqxhr,status,errMsg) { handleError(el, jqxhr) } - }) - }) - .first().trigger("input") - - return this.each(function(){ - $(this).submit(function(e){ e.preventDefault() }) - }); -} - -function handleError(el, jqxhr) { - try { - console.log('template error:', jqxhr.status, jqxhr.statusText) - el.addClass('error') - var errorResponse = JSON.parse(jqxhr.responseText); - var status = errorResponse.responseStatus; - if (status) { - el.find('.output').html('
    ' + status.errorCode + ' ' + status.message +
    -             '\n\nStackTrace:\n' + status.stackTrace + '
    ') - } - } catch(e) { - el.find('.output').html('
    ' + jqxhr.status + ' ' + jqxhr.statusText + '
    ') - } -} - -function queryStringParams(qs) { - qs = (qs || document.location.search).split('+').join(' ') - var params = {}, tokens, re = /[?&]?([^=]+)=([^&]*)/g - while (tokens = re.exec(qs)) { - params[tokens[1]] = tokens[2]; - } - return params; -} diff --git a/src/wwwroot/code/linq01.txt b/src/wwwroot/code/linq01.txt deleted file mode 100644 index c42696f..0000000 --- a/src/wwwroot/code/linq01.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -Numbers < 5: -{{#each numbers where it < 5}} - {{it}} -{{/each}} diff --git a/src/wwwroot/code/linq02.txt b/src/wwwroot/code/linq02.txt deleted file mode 100644 index b4515fd..0000000 --- a/src/wwwroot/code/linq02.txt +++ /dev/null @@ -1,4 +0,0 @@ -Sold out products: -{{#each products where UnitsInStock = 0}} - {{ ProductName }} is sold out! -{{/each}} diff --git a/src/wwwroot/code/linq04-customer.txt b/src/wwwroot/code/linq04-customer.txt deleted file mode 100644 index 74f9ca2..0000000 --- a/src/wwwroot/code/linq04-customer.txt +++ /dev/null @@ -1,2 +0,0 @@ -Customer {{ it.CustomerId }} {{ it.CompanyName | raw }} -{{ it.Orders | selectPartial: order }} \ No newline at end of file diff --git a/src/wwwroot/code/linq04-order.txt b/src/wwwroot/code/linq04-order.txt deleted file mode 100644 index 039f91f..0000000 --- a/src/wwwroot/code/linq04-order.txt +++ /dev/null @@ -1 +0,0 @@ - Order {{ it.OrderId }}: {{ it.OrderDate | dateFormat }} diff --git a/src/wwwroot/code/linq04-partials.txt b/src/wwwroot/code/linq04-partials.txt deleted file mode 100644 index c00d2f8..0000000 --- a/src/wwwroot/code/linq04-partials.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ customers - | where => it.Region = 'WA' - | assignTo: waCustomers -}} -Customers from Washington and their orders: -{{ waCustomers | selectPartial: customer }} \ No newline at end of file diff --git a/src/wwwroot/code/linq04.txt b/src/wwwroot/code/linq04.txt deleted file mode 100644 index 7805f73..0000000 --- a/src/wwwroot/code/linq04.txt +++ /dev/null @@ -1,7 +0,0 @@ -Customers from Washington and their orders: -{{#each c in customers where c.Region == 'WA'}} -Customer {{ c.CustomerId }} {{ c.CompanyName | raw }} -{{#each c.Orders}} - Order {{ OrderId }}: {{ OrderDate | dateFormat }} -{{/each}} -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq05.txt b/src/wwwroot/code/linq05.txt deleted file mode 100644 index a1f3ac2..0000000 --- a/src/wwwroot/code/linq05.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] | assignTo:digits }} -Short digits: -{{#each d in digits where d.Length < index}} - The word {{d}} is shorter than its value. -{{/each}} - diff --git a/src/wwwroot/code/linq06.txt b/src/wwwroot/code/linq06.txt deleted file mode 100644 index c6f7dc5..0000000 --- a/src/wwwroot/code/linq06.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -Numbers + 1: -{{#each numbers}} - {{ it + 1 }} -{{/each}} diff --git a/src/wwwroot/code/linq07.txt b/src/wwwroot/code/linq07.txt deleted file mode 100644 index 5d17b14..0000000 --- a/src/wwwroot/code/linq07.txt +++ /dev/null @@ -1,4 +0,0 @@ -Product Names: -{{#each products}} - {{ProductName}} -{{/each}} diff --git a/src/wwwroot/code/linq08.txt b/src/wwwroot/code/linq08.txt deleted file mode 100644 index f3a1771..0000000 --- a/src/wwwroot/code/linq08.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] | assignTo:strings }} -Number strings: -{{#each n in numbers}} -{{strings[n]}} -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq09.txt b/src/wwwroot/code/linq09.txt deleted file mode 100644 index 47e360a..0000000 --- a/src/wwwroot/code/linq09.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ ['aPPLE', 'BlUeBeRrY', 'cHeRry'] | assignTo: words }} -{{ words | map => {Uppercase: upper(it), Lowercase: lower(it)} | assignTo: upperLowerWords }} -{{#each ul in upperLowerWords}} -{{ `Uppercase: ${ul.Uppercase}, Lowercase: ${ul.Lowercase}` }} -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq10.txt b/src/wwwroot/code/linq10.txt deleted file mode 100644 index bca97a1..0000000 --- a/src/wwwroot/code/linq10.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] | assignTo:strings}} -{{ numbers | map => { Digit: strings[it], Even: it % 2 == 0 } | assignTo: digitOddEvens }} -{{#each digitOddEvens}} -The digit {{Digit}} is {{Even ? "even" : "odd"}}. -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq100.txt b/src/wwwroot/code/linq100.txt deleted file mode 100644 index b83f9d7..0000000 --- a/src/wwwroot/code/linq100.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ 0 | assignTo: i }} -{{ numbers | let => { i: i + 1 } | toList | select: v = {index + 1}, i = {i}\n }} \ No newline at end of file diff --git a/src/wwwroot/code/linq101.txt b/src/wwwroot/code/linq101.txt deleted file mode 100644 index 8956379..0000000 --- a/src/wwwroot/code/linq101.txt +++ /dev/null @@ -1,11 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ numbers - | where => it <= 3 - | assignTo: lowNumbers }} -First run numbers <= 3: -{{ lowNumbers | join(`\n`) }} -{{ 10 | times | do => assign('numbers[index]', -numbers[index]) }} -Second run numbers <= 3: -{{ lowNumbers | join(`\n`) }} -Contents of numbers: -{{ numbers | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq11.txt b/src/wwwroot/code/linq11.txt deleted file mode 100644 index 98b54d5..0000000 --- a/src/wwwroot/code/linq11.txt +++ /dev/null @@ -1,6 +0,0 @@ -Product Info: -{{ products | map => { it.ProductName, it.Category, Price: it.UnitPrice } - | assignTo: productInfos }} -{{#each productInfos}} -{{ProductName}} is in the Category {{Category}} and costs {{Price | currency}} per unit. -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq12.txt b/src/wwwroot/code/linq12.txt deleted file mode 100644 index de90074..0000000 --- a/src/wwwroot/code/linq12.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -Number: In-place? -{{#each n in numbers}} - {{n}}: {{ n == index }} -{{/each}} diff --git a/src/wwwroot/code/linq13.txt b/src/wwwroot/code/linq13.txt deleted file mode 100644 index 2f5651b..0000000 --- a/src/wwwroot/code/linq13.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] | assignTo:digits }} -Numbers < 5: -{{#each numbers where it < 5}} - {{ digits[it] }} -{{/each}} diff --git a/src/wwwroot/code/linq14.txt b/src/wwwroot/code/linq14.txt deleted file mode 100644 index b2e8533..0000000 --- a/src/wwwroot/code/linq14.txt +++ /dev/null @@ -1,8 +0,0 @@ -{{ [0, 2, 4, 5, 6, 8, 9] | assignTo: numbersA }} -{{ [1, 3, 5, 7, 8] | assignTo: numbersB }} -Pairs where a < b: -{{ numbersA | zip(numbersB) - | let => { a: it[0], b: it[1] } - | where => a < b - | map => `${a} is less than ${b}` | join(`\n`) -}} \ No newline at end of file diff --git a/src/wwwroot/code/linq15.txt b/src/wwwroot/code/linq15.txt deleted file mode 100644 index 5f77aa3..0000000 --- a/src/wwwroot/code/linq15.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ customers | zip => it.Orders - | let => { c: it[0], o: it[1] } - | where => o.Total < 500 - | map => o - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq16.txt b/src/wwwroot/code/linq16.txt deleted file mode 100644 index 674e807..0000000 --- a/src/wwwroot/code/linq16.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ customers | zip => it.Orders - | let => { c: it[0], o: it[1] } - | where => o.OrderDate >= '1998-01-01' - | map => o - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq17.txt b/src/wwwroot/code/linq17.txt deleted file mode 100644 index 6ee81ae..0000000 --- a/src/wwwroot/code/linq17.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ customers | zip => it.Orders - | let => { c: it[0], o: it[1] } - | where => o.Total >= 2000 - | map => o - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq18.txt b/src/wwwroot/code/linq18.txt deleted file mode 100644 index b0130bd..0000000 --- a/src/wwwroot/code/linq18.txt +++ /dev/null @@ -1,8 +0,0 @@ -{{ '1997-01-01' | assignTo: cutoffDate }} -{{ customers - | where => it.Region = 'WA' - | zip => it.Orders - | let => { c: it[0], o: it[1] } - | where => o.OrderDate >= cutoffDate - | map => o - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq19.txt b/src/wwwroot/code/linq19.txt deleted file mode 100644 index e323fde..0000000 --- a/src/wwwroot/code/linq19.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ customers - | let => { cust: it, custIndex: index } - | zip => cust.Orders - | let => { o: it[1] } - | map => `Customer #${custIndex + 1} has an order with OrderID ${o.OrderId}` | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq20.txt b/src/wwwroot/code/linq20.txt deleted file mode 100644 index 6410b1b..0000000 --- a/src/wwwroot/code/linq20.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -First 3 numbers: -{{#each numbers take 3}} - {{it}} -{{/each}} diff --git a/src/wwwroot/code/linq21.txt b/src/wwwroot/code/linq21.txt deleted file mode 100644 index 67db5c9..0000000 --- a/src/wwwroot/code/linq21.txt +++ /dev/null @@ -1,7 +0,0 @@ - First 3 orders in WA: -{{ customers | zip => it.Orders - | let => { c: it[0], o: it[1] } - | where => c.Region = 'WA' - | take(3) - | map => o - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq22.txt b/src/wwwroot/code/linq22.txt deleted file mode 100644 index 3d6bf0c..0000000 --- a/src/wwwroot/code/linq22.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -All but first 4 numbers: -{{#each numbers skip 4}} -{{it}} -{{/each}} diff --git a/src/wwwroot/code/linq23.txt b/src/wwwroot/code/linq23.txt deleted file mode 100644 index b51fc8f..0000000 --- a/src/wwwroot/code/linq23.txt +++ /dev/null @@ -1,7 +0,0 @@ - All but first 2 orders in WA: -{{ customers | zip => it.Orders - | let => { c: it[0], o: it[1] } - | where => c.Region = 'WA' - | skip(2) - | map => { c.CustomerId, o.OrderId, o.OrderDate } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq24.txt b/src/wwwroot/code/linq24.txt deleted file mode 100644 index a081fb5..0000000 --- a/src/wwwroot/code/linq24.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -First numbers less than 6: -{{ numbers - | takeWhile => it < 6 - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq25.txt b/src/wwwroot/code/linq25.txt deleted file mode 100644 index e75bafe..0000000 --- a/src/wwwroot/code/linq25.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -First numbers not less than their position: -{{ numbers - | takeWhile => it >= index - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq26.txt b/src/wwwroot/code/linq26.txt deleted file mode 100644 index e4c23c9..0000000 --- a/src/wwwroot/code/linq26.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -All elements starting from first element divisible by 3: -{{ numbers - | skipWhile => it % 3 != 0 - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq27.txt b/src/wwwroot/code/linq27.txt deleted file mode 100644 index 365b3e4..0000000 --- a/src/wwwroot/code/linq27.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -All elements starting from first element less than its position: -{{ numbers - | skipWhile => it >= index - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq28.txt b/src/wwwroot/code/linq28.txt deleted file mode 100644 index 004abe4..0000000 --- a/src/wwwroot/code/linq28.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ ['cherry', 'apple', 'blueberry'] | assignTo: words }} -The sorted list of words: -{{#each words orderby it}} -{{it}} -{{/each}} diff --git a/src/wwwroot/code/linq29.txt b/src/wwwroot/code/linq29.txt deleted file mode 100644 index a196c9a..0000000 --- a/src/wwwroot/code/linq29.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ ['cherry', 'apple', 'blueberry'] | assignTo: words }} -The sorted list of words (by length): -{{#each words orderby it.Length}} -{{it}} -{{/each}} diff --git a/src/wwwroot/code/linq30.txt b/src/wwwroot/code/linq30.txt deleted file mode 100644 index 513a4b6..0000000 --- a/src/wwwroot/code/linq30.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ products - | orderBy => it.ProductName - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq31.txt b/src/wwwroot/code/linq31.txt deleted file mode 100644 index f9ed992..0000000 --- a/src/wwwroot/code/linq31.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ ['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] | assignTo: words }} -{{ words - | orderBy(o => o, { comparer }) - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq32.txt b/src/wwwroot/code/linq32.txt deleted file mode 100644 index b2d2a75..0000000 --- a/src/wwwroot/code/linq32.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [1.7, 2.3, 1.9, 4.1, 2.9] | assignTo: doubles }} -The doubles from highest to lowest: -{{#each doubles orderby it descending}} -{{it}} -{{/each}} diff --git a/src/wwwroot/code/linq33.txt b/src/wwwroot/code/linq33.txt deleted file mode 100644 index 0223eb4..0000000 --- a/src/wwwroot/code/linq33.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ products - | orderByDescending => it.UnitsInStock - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq34.txt b/src/wwwroot/code/linq34.txt deleted file mode 100644 index 0d033e5..0000000 --- a/src/wwwroot/code/linq34.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ ['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] | assignTo: words }} -{{ words - | orderByDescending(o => o, { comparer }) - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq35.txt b/src/wwwroot/code/linq35.txt deleted file mode 100644 index ee4a4ab..0000000 --- a/src/wwwroot/code/linq35.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] | assignTo:digits }} -Sorted digits: -{{ digits - | orderBy => it.length - | thenBy => it - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq36.txt b/src/wwwroot/code/linq36.txt deleted file mode 100644 index 96c85de..0000000 --- a/src/wwwroot/code/linq36.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ ['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] | assignTo: words }} -{{ words - | orderBy => it.length - | thenBy(w => w, { comparer }) - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq37.txt b/src/wwwroot/code/linq37.txt deleted file mode 100644 index 58df7b2..0000000 --- a/src/wwwroot/code/linq37.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ products - | orderBy => it.Category - | thenByDescending => it.UnitPrice - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq38.txt b/src/wwwroot/code/linq38.txt deleted file mode 100644 index 28c7fec..0000000 --- a/src/wwwroot/code/linq38.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ ['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] | assignTo: words }} -{{ words - | orderBy => it.length - | thenByDescending(w => w, { comparer }) - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq39.txt b/src/wwwroot/code/linq39.txt deleted file mode 100644 index c0b1039..0000000 --- a/src/wwwroot/code/linq39.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] | assignTo:digits }} -A backwards list of the digits with a second character of 'i': -{{ digits - | where => it[1] = 'i' - | reverse - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq40.txt b/src/wwwroot/code/linq40.txt deleted file mode 100644 index dd9c556..0000000 --- a/src/wwwroot/code/linq40.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: nums }} -{{ nums - | groupBy => it % 5 - | let => { remainder: it.Key, nums: it } - | select: Numbers with a remainder of { remainder } when divided by 5:\n{nums | join('\n')}\n -}} \ No newline at end of file diff --git a/src/wwwroot/code/linq41.txt b/src/wwwroot/code/linq41.txt deleted file mode 100644 index a3e04d4..0000000 --- a/src/wwwroot/code/linq41.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ ['blueberry', 'chimpanzee', 'abacus', 'banana', 'apple', 'cheese'] | assignTo: words }} -{{ words | groupBy => it[0] | map => { firstLetter: it.Key, words: it } | assignTo: groups }} -{{#each groups}} -Words that start with the letter '{{firstLetter}}': -{{ words | join(`\n`) }} -{{/each}} diff --git a/src/wwwroot/code/linq42.txt b/src/wwwroot/code/linq42.txt deleted file mode 100644 index 3755d2a..0000000 --- a/src/wwwroot/code/linq42.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ products - | groupBy => it.Category - | let => { Category: it.Key, Products: it } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq43.txt b/src/wwwroot/code/linq43.txt deleted file mode 100644 index aece830..0000000 --- a/src/wwwroot/code/linq43.txt +++ /dev/null @@ -1,15 +0,0 @@ -{{ customers - | map => { - CompanyName: it.CompanyName, - YearGroups: map ( - groupBy(it.Orders, it => it.OrderDate.Year), - yg => { - Year: yg.Key, - MonthGroups: map ( - groupBy(yg, o => o.OrderDate.Month), - mg => { Month: mg.Key, Orders: mg } - ) - } - ) - } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq44.txt b/src/wwwroot/code/linq44.txt deleted file mode 100644 index d9f374b..0000000 --- a/src/wwwroot/code/linq44.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ ['from ', ' salt', ' earn ', ' last ', ' near ', ' form '] | assignTo: anagrams }} -{{#each groupBy(anagrams, w => trim(w), { comparer: anagramComparer }) }} -{{it | json}} -{{/each}} diff --git a/src/wwwroot/code/linq45.txt b/src/wwwroot/code/linq45.txt deleted file mode 100644 index 3dbb7aa..0000000 --- a/src/wwwroot/code/linq45.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ ['from ', ' salt', ' earn ', ' last ', ' near ', ' form '] | assignTo: anagrams }} -{{#each groupBy(anagrams, w => trim(w), { map: a => upper(a), comparer: anagramComparer }) }} -{{it | json}} -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq46.txt b/src/wwwroot/code/linq46.txt deleted file mode 100644 index ff45655..0000000 --- a/src/wwwroot/code/linq46.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [2, 2, 3, 5, 5] | assignTo: factorsOf300 }} -Prime factors of 300: -{{ factorsOf300 | distinct | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq47.txt b/src/wwwroot/code/linq47.txt deleted file mode 100644 index ca8cb29..0000000 --- a/src/wwwroot/code/linq47.txt +++ /dev/null @@ -1,5 +0,0 @@ -Category names: -{{ products - | map => it.Category - | distinct - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq48.txt b/src/wwwroot/code/linq48.txt deleted file mode 100644 index 859b5e4..0000000 --- a/src/wwwroot/code/linq48.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ [ 0, 2, 4, 5, 6, 8, 9 ] | assignTo: numbersA }} -{{ [ 1, 3, 5, 7, 8 ] | assignTo: numbersB }} -Unique numbers from both arrays: -{{#each union(numbersA,numbersB)}} - {{it}} -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq49.txt b/src/wwwroot/code/linq49.txt deleted file mode 100644 index da8883f..0000000 --- a/src/wwwroot/code/linq49.txt +++ /dev/null @@ -1,10 +0,0 @@ -{{ products - | map => it.ProductName[0] - | assignTo => productFirstChars }} -{{ customers - | map => it.CompanyName[0] - | assignTo => customerFirstChars }} -Unique first letters from Product names and Customer names: -{{#each union(productFirstChars,customerFirstChars) }} - {{it}} -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq50.txt b/src/wwwroot/code/linq50.txt deleted file mode 100644 index fdc4fa0..0000000 --- a/src/wwwroot/code/linq50.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ [ 0, 2, 4, 5, 6, 8, 9 ] | assignTo: numbersA }} -{{ [ 1, 3, 5, 7, 8 ] | assignTo: numbersB }} -Common numbers shared by both arrays: -{{ numbersA | intersect(numbersB) | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq51.txt b/src/wwwroot/code/linq51.txt deleted file mode 100644 index 574894f..0000000 --- a/src/wwwroot/code/linq51.txt +++ /dev/null @@ -1,10 +0,0 @@ -{{ products - | map => it.ProductName[0] - | assignTo => productFirstChars }} -{{ customers - | map => it.CompanyName[0] - | assignTo => customerFirstChars }} -Common first letters from Product names and Customer names: -{{#each intersect(productFirstChars,customerFirstChars) }} - {{it}} -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq52.txt b/src/wwwroot/code/linq52.txt deleted file mode 100644 index 84658a8..0000000 --- a/src/wwwroot/code/linq52.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ [ 0, 2, 4, 5, 6, 8, 9 ] | assignTo: numbersA }} -{{ [ 1, 3, 5, 7, 8 ] | assignTo: numbersB }} -Numbers in first array but not second array: -{{ numbersA | except(numbersB) | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq53.txt b/src/wwwroot/code/linq53.txt deleted file mode 100644 index 5764979..0000000 --- a/src/wwwroot/code/linq53.txt +++ /dev/null @@ -1,10 +0,0 @@ -{{ products - | map => it.ProductName[0] - | assignTo => productFirstChars }} -{{ customers - | map => it.CompanyName[0] - | assignTo => customerFirstChars }} -First letters from Product names, but not from Customer names: -{{#each except(productFirstChars,customerFirstChars) }} - {{it}} -{{/each}} \ No newline at end of file diff --git a/src/wwwroot/code/linq54.txt b/src/wwwroot/code/linq54.txt deleted file mode 100644 index 88f2f50..0000000 --- a/src/wwwroot/code/linq54.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ [ 1.7, 2.3, 1.9, 4.1, 2.9 ] | assignTo: doubles }} -Every other double from highest to lowest: -{{ doubles - | orderByDescending => it - | step({ by: 2 }) - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq55.txt b/src/wwwroot/code/linq55.txt deleted file mode 100644 index 14d0538..0000000 --- a/src/wwwroot/code/linq55.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ [ 'cherry', 'apple', 'blueberry' ] | assignTo: words }} -The sorted word list: -{{ words - | orderBy => it - | toList - | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq56.txt b/src/wwwroot/code/linq56.txt deleted file mode 100644 index f259755..0000000 --- a/src/wwwroot/code/linq56.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [{name:'Alice',score:50},{name:'Bob',score:40},{name:'Cathy',score:45}] | assignTo:records }} -{{ records | toDictionary => it.name | assignTo: scoreRecordsDict }} -Bob's score: {{ scoreRecordsDict.Bob.score }} \ No newline at end of file diff --git a/src/wwwroot/code/linq57.txt b/src/wwwroot/code/linq57.txt deleted file mode 100644 index 779a321..0000000 --- a/src/wwwroot/code/linq57.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ [null, 1.0, 'two', 3, 'four', 5, 'six', 7.0] | assignTo: numbers }} -Numbers stored as doubles: -{{ numbers - | of({ type: 'Double' }) - | select: { it | format('#.0') }\n }} \ No newline at end of file diff --git a/src/wwwroot/code/linq58.txt b/src/wwwroot/code/linq58.txt deleted file mode 100644 index f83f04c..0000000 --- a/src/wwwroot/code/linq58.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ products - | where => it.ProductId = 12 - | first - | dump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq59.txt b/src/wwwroot/code/linq59.txt deleted file mode 100644 index dc565bb..0000000 --- a/src/wwwroot/code/linq59.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] | assignTo:strings }} -{{#each s in strings where s[0] == 'o' take 1 }} -A string starting with 'o': {{s}} -{{/each}} diff --git a/src/wwwroot/code/linq61.txt b/src/wwwroot/code/linq61.txt deleted file mode 100644 index 1651475..0000000 --- a/src/wwwroot/code/linq61.txt +++ /dev/null @@ -1,2 +0,0 @@ -{{ [] | assignTo: numbers }} -{{ numbers | first | otherwise('null') }} \ No newline at end of file diff --git a/src/wwwroot/code/linq62.txt b/src/wwwroot/code/linq62.txt deleted file mode 100644 index 3411401..0000000 --- a/src/wwwroot/code/linq62.txt +++ /dev/null @@ -1,2 +0,0 @@ -{{ products | first => it.ProductId = 789 | assignTo: product789 }} -Product 789 exists: {{ product789 != null }} \ No newline at end of file diff --git a/src/wwwroot/code/linq64.txt b/src/wwwroot/code/linq64.txt deleted file mode 100644 index 37d7adf..0000000 --- a/src/wwwroot/code/linq64.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ] | assignTo: numbers }} -{{ numbers | where => it > 5 | elementAt(1) | assignTo: fourthLowNum }} -Second number > 5: {{ fourthLowNum }} \ No newline at end of file diff --git a/src/wwwroot/code/linq65.txt b/src/wwwroot/code/linq65.txt deleted file mode 100644 index c8f6d3b..0000000 --- a/src/wwwroot/code/linq65.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{#each range(100,50) }} -The number {{it}} is {{ isEven(it) ? 'even' : 'odd' }}. -{{/each}} diff --git a/src/wwwroot/code/linq66.txt b/src/wwwroot/code/linq66.txt deleted file mode 100644 index 337d1db..0000000 --- a/src/wwwroot/code/linq66.txt +++ /dev/null @@ -1 +0,0 @@ -{{ 10 | itemsOf(7) | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq67.txt b/src/wwwroot/code/linq67.txt deleted file mode 100644 index 103f233..0000000 --- a/src/wwwroot/code/linq67.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ ['believe', 'relief', 'receipt', 'field'] | assignTo: words }} -{{ words | any => contains(it, 'ei') | assignTo: iAfterE }} -There is a word that contains in the list that contains 'ei': {{ iAfterE | lower }} \ No newline at end of file diff --git a/src/wwwroot/code/linq69.txt b/src/wwwroot/code/linq69.txt deleted file mode 100644 index 4a1a769..0000000 --- a/src/wwwroot/code/linq69.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ products - | groupBy => it.Category - | where => any(it, it => it.UnitsInStock = 0) - | let => { Category: it.Key, Products: it } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq70.txt b/src/wwwroot/code/linq70.txt deleted file mode 100644 index 9427e74..0000000 --- a/src/wwwroot/code/linq70.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [1, 11, 3, 19, 41, 65, 19] | assignTo: numbers }} -{{ numbers | all => isOdd(it) | assignTo: onlyOdd }} -The list contains only odd numbers: {{ onlyOdd }} \ No newline at end of file diff --git a/src/wwwroot/code/linq72.txt b/src/wwwroot/code/linq72.txt deleted file mode 100644 index 9f163da..0000000 --- a/src/wwwroot/code/linq72.txt +++ /dev/null @@ -1,5 +0,0 @@ -{{ products - | groupBy => it.Category - | where => all(it, it => it.UnitsInStock > 0) - | let => { Category: it.Key, Products: it } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq73.txt b/src/wwwroot/code/linq73.txt deleted file mode 100644 index 0d513ac..0000000 --- a/src/wwwroot/code/linq73.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [2, 2, 3, 5, 5] | assignTo: factorsOf300 }} -{{ factorsOf300 | distinct | count | assignTo: uniqueFactors }} -There are {{uniqueFactors}} unique factors of 300. \ No newline at end of file diff --git a/src/wwwroot/code/linq74.txt b/src/wwwroot/code/linq74.txt deleted file mode 100644 index 9c97c87..0000000 --- a/src/wwwroot/code/linq74.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ numbers | count => isOdd(it) | assignTo: oddNumbers }} -There are {{oddNumbers}} odd numbers in the list. \ No newline at end of file diff --git a/src/wwwroot/code/linq76.txt b/src/wwwroot/code/linq76.txt deleted file mode 100644 index e14c998..0000000 --- a/src/wwwroot/code/linq76.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ customers - | let => { it.CustomerId, OrderCount: count(it.Orders) } - | map => `${CustomerId}, ${OrderCount}` | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq77.txt b/src/wwwroot/code/linq77.txt deleted file mode 100644 index f00315f..0000000 --- a/src/wwwroot/code/linq77.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ products - | groupBy => it.Category - | let => { Category: it.Key, ProductCount: count(it) } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq78.txt b/src/wwwroot/code/linq78.txt deleted file mode 100644 index 728a77d..0000000 --- a/src/wwwroot/code/linq78.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ numbers | sum | assignTo: numSum }} -The sum of the numbers is {{numSum}}. \ No newline at end of file diff --git a/src/wwwroot/code/linq79.txt b/src/wwwroot/code/linq79.txt deleted file mode 100644 index 4d9be01..0000000 --- a/src/wwwroot/code/linq79.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [ 'cherry', 'apple', 'blueberry'] | assignTo: words }} -{{ words | sum => it.Length | assignTo: totalChars }} -There are a total of {{totalChars}} characters in these words. \ No newline at end of file diff --git a/src/wwwroot/code/linq80.txt b/src/wwwroot/code/linq80.txt deleted file mode 100644 index 86c7fae..0000000 --- a/src/wwwroot/code/linq80.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ products - | groupBy => it.Category - | map => { Category: it.Key, TotalUnitsInStock: sum(it, p => p.UnitsInStock) } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq81.txt b/src/wwwroot/code/linq81.txt deleted file mode 100644 index a744e7c..0000000 --- a/src/wwwroot/code/linq81.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ numbers | min | assignTo: minNum }} -The minimum number is {{minNum}}. \ No newline at end of file diff --git a/src/wwwroot/code/linq82.txt b/src/wwwroot/code/linq82.txt deleted file mode 100644 index 5f9c737..0000000 --- a/src/wwwroot/code/linq82.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [ 'cherry', 'apple', 'blueberry' ] | assignTo: words }} -{{ words | min => it.Length | assignTo: shortestWord }} -The shortest word is {{shortestWord}} characters long. \ No newline at end of file diff --git a/src/wwwroot/code/linq83.txt b/src/wwwroot/code/linq83.txt deleted file mode 100644 index 24239b3..0000000 --- a/src/wwwroot/code/linq83.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ products - | groupBy => it.Category - | map => { Category: it.Key, CheapestPrice: min(it, p => p.UnitPrice) } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq84.txt b/src/wwwroot/code/linq84.txt deleted file mode 100644 index 60b27a6..0000000 --- a/src/wwwroot/code/linq84.txt +++ /dev/null @@ -1,8 +0,0 @@ -{{ products - | groupBy => it.Category - | let => { - g: it, - MinPrice: min(it, p => p.UnitPrice), - } - | map => { Category: g.Key, CheapestProducts: where(g, p => p.UnitPrice == MinPrice) } - | htmlDump }} diff --git a/src/wwwroot/code/linq85.txt b/src/wwwroot/code/linq85.txt deleted file mode 100644 index 4f6af55..0000000 --- a/src/wwwroot/code/linq85.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ numbers | max | assignTo: maxNum }} -The maximum number is {{maxNum}}. \ No newline at end of file diff --git a/src/wwwroot/code/linq86.txt b/src/wwwroot/code/linq86.txt deleted file mode 100644 index 3bc089a..0000000 --- a/src/wwwroot/code/linq86.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [ 'cherry', 'apple', 'blueberry' ] | assignTo: words }} -{{ words | max => it.Length | assignTo: longestLength }} -The longest word is {{longestLength}} characters long. \ No newline at end of file diff --git a/src/wwwroot/code/linq87.txt b/src/wwwroot/code/linq87.txt deleted file mode 100644 index 93fb6c6..0000000 --- a/src/wwwroot/code/linq87.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ products - | groupBy => it.Category - | map => { Category: it.Key, MostExpensivePrice: max(it, p => p.UnitPrice) } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq88.txt b/src/wwwroot/code/linq88.txt deleted file mode 100644 index 1143a82..0000000 --- a/src/wwwroot/code/linq88.txt +++ /dev/null @@ -1,8 +0,0 @@ -{{ products - | groupBy => it.Category - | let => { - g: it, - MaxPrice: max(it, p => p.UnitPrice), - } - | map => { Category: g.Key, MostExpensiveProducts: where(g, p => p.UnitPrice = MaxPrice) } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq89.txt b/src/wwwroot/code/linq89.txt deleted file mode 100644 index 4ea00c5..0000000 --- a/src/wwwroot/code/linq89.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ numbers | average | assignTo: averageNum }} -The average number is {{averageNum}}. \ No newline at end of file diff --git a/src/wwwroot/code/linq90.txt b/src/wwwroot/code/linq90.txt deleted file mode 100644 index 241d6c0..0000000 --- a/src/wwwroot/code/linq90.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [ 'cherry', 'apple', 'blueberry' ] | assignTo: words }} -{{ words | average => it.Length | assignTo: averageLength }} -The average word length is {{averageLength}} characters. \ No newline at end of file diff --git a/src/wwwroot/code/linq91.txt b/src/wwwroot/code/linq91.txt deleted file mode 100644 index 5c53d34..0000000 --- a/src/wwwroot/code/linq91.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ products - | groupBy => it.Category - | map => { Category: it.Key, AveragePrice: average(it, p => p.UnitPrice) } - | htmlDump }} \ No newline at end of file diff --git a/src/wwwroot/code/linq92.txt b/src/wwwroot/code/linq92.txt deleted file mode 100644 index 18c283e..0000000 --- a/src/wwwroot/code/linq92.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ [1.7, 2.3, 1.9, 4.1, 2.9] | assignTo: doubles }} -{{ doubles | reduce((runningProduct, nextFactor) => runningProduct * nextFactor) - | assignTo: product }} -Total product of all numbers: {{ product }}. \ No newline at end of file diff --git a/src/wwwroot/code/linq93.txt b/src/wwwroot/code/linq93.txt deleted file mode 100644 index f9d6679..0000000 --- a/src/wwwroot/code/linq93.txt +++ /dev/null @@ -1,6 +0,0 @@ -{{ [20, 10, 40, 50, 10, 70, 30] | assignTo: attemptedWithdrawals }} -{{ attemptedWithdrawals | reduce((balance, nextWithdrawal) => - ((nextWithdrawal <= balance) ? (balance - nextWithdrawal) : balance), - { initialValue: 100.0 }) - | assignTo: endBalance }} -Ending balance: {{endBalance}}. \ No newline at end of file diff --git a/src/wwwroot/code/linq94.txt b/src/wwwroot/code/linq94.txt deleted file mode 100644 index bb05426..0000000 --- a/src/wwwroot/code/linq94.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ [0, 2, 4, 5, 6, 8, 9] | assignTo: numbersA }} -{{ [1, 3, 5, 7, 8] | assignTo: numbersB }} -All numbers from both arrays: -{{ numbersA | concat(numbersB) | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq95.txt b/src/wwwroot/code/linq95.txt deleted file mode 100644 index c9f823a..0000000 --- a/src/wwwroot/code/linq95.txt +++ /dev/null @@ -1,8 +0,0 @@ -{{ customers - | map => it.CompanyName - | assignTo => customerNames }} -{{ products - | map => it.ProductName - | assignTo => productNames }} -Customer and product names: -{{ customerNames | concat(productNames) | join(`\n`) }} \ No newline at end of file diff --git a/src/wwwroot/code/linq96.txt b/src/wwwroot/code/linq96.txt deleted file mode 100644 index 5d91297..0000000 --- a/src/wwwroot/code/linq96.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ [ 'cherry', 'apple', 'blueberry' ] | assignTo: wordsA }} -{{ [ 'cherry', 'apple', 'blueberry' ] | assignTo: wordsB }} -{{ wordsA | equivalentTo(wordsB) | assignTo: match }} -The sequences match: {{ match | lower }} \ No newline at end of file diff --git a/src/wwwroot/code/linq97.txt b/src/wwwroot/code/linq97.txt deleted file mode 100644 index ff62ac5..0000000 --- a/src/wwwroot/code/linq97.txt +++ /dev/null @@ -1,4 +0,0 @@ -{{ [ 'cherry', 'apple', 'blueberry' ] | assignTo: wordsA }} -{{ [ 'apple', 'blueberry', 'cherry' ] | assignTo: wordsB }} -{{ wordsA | equivalentTo(wordsB) | assignTo: match }} -The sequences match: {{ match | lower }} diff --git a/src/wwwroot/code/linq99.txt b/src/wwwroot/code/linq99.txt deleted file mode 100644 index 8eb0e85..0000000 --- a/src/wwwroot/code/linq99.txt +++ /dev/null @@ -1,3 +0,0 @@ -{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] | assignTo: numbers }} -{{ 0 | assignTo: i }} -{{ numbers | let => { i: i + 1 } | select: v = {index + 1}, i = {i}\n }} \ No newline at end of file diff --git a/src/wwwroot/doc-links.html b/src/wwwroot/doc-links.html deleted file mode 100644 index 1e24e08..0000000 --- a/src/wwwroot/doc-links.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/src/wwwroot/docs/api-pages.html b/src/wwwroot/docs/api-pages.html deleted file mode 100644 index c723cc9..0000000 --- a/src/wwwroot/docs/api-pages.html +++ /dev/null @@ -1,213 +0,0 @@ - - -

    - In addition to being productive high-level .NET scripting language for generating dynamic HTML pages, Template Pages can also - be used to rapidly develop Web APIs which can take advantage of the new support for - Dynamic Page Based Routes - to rapidly develop data-driven JSON APIs and make them available under ideal pretty URLs whilst utilizing the same Live Development workflow - that doesn't need to define any C# Types or execute any builds - as all development can happen in real-time whilst the App is running, - enabling the fastest way to develop Web APIs in .NET. -

    - -

    Dynamic API Pages

    - -

    - The only difference between a Template Page that generates HTML or a Template Page that returns an API Response is that API pages - return a value using the return filter. -

    - -

    - For comparison, to create a Hello World C# ServiceStack Service you would typically create a Request DTO, Response DTO and a Service implementation: -

    - -{{ 'gfm/api-pages/04.md' | githubMarkdown }} - -

    Dedicated API Pages

    - -

    - Dedicated API pages lets you specify a path where your "API Pages" are located when registering TemplatePagesFeature: -

    - -{{ 'gfm/api-pages/01.md' | githubMarkdown }} - -

    - All pages within the /api folder are also treated like "API Pages" for creating Web APIs where instead of writing their response to the - Output Stream, their return value is serialized in the requested Content-Type using the return filter: -

    - -
    {{#raw}}{{ response | return }}
    -{{ response | return({ ... }) }}
    -{{ httpResult({ ... }) | return }}
    -{{/raw}}
    - -

    - The route for the dedicated API page starts the same as the filename and one advantage over Dynamic API Pages above is that a single Page - can handle multiple requests with different routes, e.g: -

    - -
    -/api/customers                // PathArgs = []
    -/api/customers/1              // PathArgs = ['1']
    -/api/customers/by-name/Name   // PathArgs = ['by-name','Name']
    -
    - -

    API Page Examples

    - -

    - To demonstrate API Pages in action we've added Web APIs equivalents for Rockwind's - customers and - products HTML pages with the implementation below: -

    - - - -

    /api/customers

    - -

    - The entire implementation of the customers API is below: -

    - -{{ 'gfm/api-pages/02.md' | githubMarkdown }} - -

    - These are some of the API's that are made available with the above implementation: -

    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    /customers API
    All Customers - - - - -
    Accept HTTP Header also supported
    -
    Alfreds Futterkiste Details - - - -
    As List - - - -
    Customers in Germany - - - -
    Customers in London - - - -
    Combination Query - /api/customers?city=London&country=UK&limit=3 -
    - -

    /api/products

    - -

    - The Products API is an example of a more complex API where data is sourced from multiple tables: -

    - -{{ 'gfm/api-pages/03.md' | githubMarkdown }} - -

    - Some API examples using the above implementation: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    /products API
    All Products - - - -
    Chai Product Details - - - -
    As List - - - -
    Beverage Products - - - -
    Bigfoot Breweries Products - - - -
    Products containing Tofu - - - -
    - -

    Untyped APIs

    - -

    - As these APIs don't have a Typed Schema they don't benefit from any of ServiceStack's metadata Services, i.e. - they're not listed in Metadata pages, included in - Open API or have Typed APIs generated using - Add ServiceStack Reference. -

    - - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/api-reference.html b/src/wwwroot/docs/api-reference.html deleted file mode 100644 index 94e596b..0000000 --- a/src/wwwroot/docs/api-reference.html +++ /dev/null @@ -1,252 +0,0 @@ - - -

    Plugins

    - -{{#markdown}} -Plugins are a nice way to extend templates with customized functionality which can encapsulate any number of blocks, filters, -preferred configuration and dependencies by implementing the `ITemplatePlugin` interface. - -The [MarkdownTemplatePlugin](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/MarkdownTemplatePlugin.cs) -is a good example of this which registers a markdown -[Page format](/docs/page-formats), [Filter](/docs/filters), [Filter Transformer](/docs/transformers) and -[markdown Block](/docs/blocks#markdown): -{{/markdown}} - -{{ 'gfm/api-reference/03.md' | githubMarkdown }} - -{{#markdown}} -#### Removing Plugins - -When needed any default plugins can be removed with the `RemovePlugins()` API: -{{/markdown}} - -{{ 'gfm/blocks/20.md' | githubMarkdown }} - -{{#markdown}} -#### Advanced plugin registration - -For greater control over the registration and execution of plugins, they can implement `ITemplatePluginBefore` to have custom logic -executed before plugins are registered or implement `ITemplatePluginAfter` for executing any logic after. -{{/markdown}} - -

    TemplateContext

    - -

    - The TemplateContext is the sandbox where all templates are executed within that can be customized with the - available APIs below: -

    - -

    Preconfigured defaults

    - -

    - Some default filters when called without arguments will use the default configuration - shown below that can be overridden by replacing their default value in the TemplateContext's Args collection: -

    - -{{ 'gfm/api-reference/01.md' | githubMarkdown }} - -

    Args

    - -

    - TemplateContext Arguments can be used to define global variables available to every template, partial, filter, etc: -

    - -

    Virtual Files

    - -

    - Templates only have access to Pages available from its configured VirtualFiles which uses an empty MemoryVirtualFiles. - To make pages available to your TemplateContext instance you can choose to either programatically populate the - VirtualFiles collection from an external source, e.g: -

    - -{{ 'gfm/api-reference/02.md' | githubMarkdown }} - -

    - Alternatively if you want to enable access to an entire sub directory you can replace the Virtual Files with a - FileSystem VFS at the directory you want to make the root directory: -

    - -
    context.VirtualFiles = new FileSystemVirtualFiles("~/template-files".MapProjectPath());
    - -

    DebugMode

    - -

    - DebugMode is used to control whether full Exception details like StackTrace is displayed. In - TemplatePageFeature it defaults to the AppHost DebugMode, otherwise it's true by default. -

    - -

    ScanTypes

    - -

    - Specify a TemplateFilter or TemplateCodePage to auto register. -

    - -

    ScanAssemblies

    - -

    - Specify assemblies that should be scanned to find TemplateFilter's and TemplateCodePage's to auto register. - In TemplatePageFeature the AppHost's Service Assemblies are included by default. -

    - -

    TemplateFilters

    - -

    - Register additional instances of filters you want templates to have access to. -

    - -

    CodePages

    - -

    - Register instances of code pages you want templates to have access to. -

    - -

    Container

    - -

    - The IOC Container used by the TemplateContext to register and resolve dependencies, filters and Code Pages. - Uses SimpleContainer by default. -

    - -

    AppSettings

    - -

    - Specify an optional - App Settings provider that templates can access with the - {{#raw}}{{ key | appSetting }}{{/raw}} - default filter. -

    - -

    CheckForModifiedPages

    - -

    - Whether to check for modified pages by default when not in DebugMode, defaults to true. - Note: if DebugMode is true it will always check for changes. -

    - -

    CheckForModifiedPagesAfter

    - -

    - If provided will specify how long to wait before checking if backing files of pages have changed and to reload them if they have. - Note: if DebugMode is true it will always check for changes. -

    - -

    RenderExpressionExceptions

    - -

    - Whether to Render Expression Exceptions in-line (default = false). -

    - -

    PageResult

    - -

    - The PageResult is the rendering context used to render templates whose output can be customized with the APIs below: -

    - -

    Layout

    - -

    - Override the layout used for the page by specifying a layout name: -

    - -
    new PageResult(page) { Layout = "custom-layout" }
    - -

    LayoutPage

    - -

    - Override the layout used for the page by specifying a Layout page: -

    - -
    new PageResult(page) { LayoutPage = Request.GetPage("custom-layout") }
    - -

    Args

    - -

    - Override existing or specify additional arguments in the Template's scope: -

    - -
    new PageResult(page) { 
    -    Args = { 
    -        ["myArg"] = "argValue",
    -    }
    -}
    - -

    TemplateFilters

    - -

    - Make additional filters available to the Template: -

    - -
    new PageResult(page) { 
    -    TemplateFilters = { new MyFilters() }
    -}
    - -

    OutputTransformers

    - -

    - Transform the entire Template's Output before rendering to the OutputStream: -

    - -
    new PageResult(page) {
    -    ContentType = MimeTypes.Html,
    -    OutputTransformers = { MarkdownPageFormat.TransformToHtml },
    -}
    - -

    PageTransformers

    - -

    - Transform just the Page's Output before rendering to the OutputStream: -

    - -
    new PageResult(page) {
    -    ContentType = MimeTypes.Html,
    -    PageTransformers = { MarkdownPageFormat.TransformToHtml },
    -}
    - -

    FilterTransformers

    - -

    - Specify additional Filter Transformers available to the Template: -

    - -
    new PageResult(page) {
    -    FilterTransformers = {
    -        ["markdown"] = MarkdownPageFormat.TransformToHtml
    -    }
    -}
    - -

    ExcludeFiltersNamed

    - -

    - Disable access to the specified registered filters: -

    - -
    new PageResult(page) {
    -    ExcludeFiltersNamed = { "partial", "selectPartial" }
    -}
    - -

    Options

    - -

    - Return additional HTTP Response Headers when rendering to a HTTP Response: -

    - -
    new PageResult(page) {
    -    Options = { 
    -        ["X-Powered-By"] = "ServiceStack Templates"
    -    }
    -}
    - -

    ContentType

    - -

    - Specify the HTTP Content-Type when rendering to a HTTP Response: -

    - -
    new PageResult(page) {
    -    ContentType = "text/plain"
    -}
    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/blocks.html b/src/wwwroot/docs/blocks.html deleted file mode 100644 index d11a3b0..0000000 --- a/src/wwwroot/docs/blocks.html +++ /dev/null @@ -1,357 +0,0 @@ - - -{{#markdown}} -Blocks lets you define reusable statements that can be invoked with a new context allowing the creation custom iterators and helpers - -making it easy to encapsulate reusable functionality and reduce boilerplate for common functionality. - -The syntax for blocks follows the familiar [handlebars block helpers](https://handlebarsjs.com/block_helpers.html) in both syntax and functionality. -Templates also includes most of handlebars.js block helpers which are useful in a HTML template language whilst minimizing any porting efforts if -needing to reuse existing JavaScript handlebars templates. - -We'll walk through creating a few of the built-in Template blocks to demonstrate how to create them from scratch. - -### noop - -We'll start with creating the `noop` block (short for "no operation") which functions like a block comment by removing its inner contents -from the rendered page: -{{/markdown}} - -{{ 'gfm/blocks/01.md' | githubMarkdown }} - -{{#markdown}} -The `noop` block is also the smallest implementation possible which needs to inherit `TemplateBlock` class, overrides the `Name` getter with -the name of the block and implements the `WriteAsync()` method which for the noop block just returns an empty `Task` there by not writing anything -to the Output Stream, resulting in its inner contents being ignored: -{{/markdown}} - -{{ 'gfm/blocks/02.md' | githubMarkdown }} - -{{#markdown}} -All Block's are executed with 3 parameters: - - - `TemplateScopeContext` - The current Execution and Rendering context - - `PageBlockFragment` - The parsed Block contents - - `CancellationToken` - Allows the async render operation to be cancelled -{{/markdown}} - -

    Registering Blocks

    - -{{#markdown}} -The same flexible registration options for [Registering Filters](/docs/filters#registering-filters) is also available for registering blocks -where if it wasn't already built-in, `TemplateNoopBlock` could be registered by adding it to the `TemplateBlocks` collection: -{{/markdown}} - -{{ 'gfm/blocks/03.md' | githubMarkdown }} - -
    Autowired using TemplateContext IOC
    - -{{#markdown}} -Autowired instances of blocks and filters can also be created using TemplateContext's configured IOC where they're also injected with any -registered IOC dependencies by registering them in the `ScanTypes` collection: -{{/markdown}} - -{{ 'gfm/blocks/04.md' | githubMarkdown }} - -{{#markdown}} -When the `TemplateContext` is initialized it will go through each Type and create an autowired instance of each Type and register them in the -`TemplateBlocks` collection. An alternative to registering individual Types is to register an entire Assembly, e.g: -{{/markdown}} - -{{ 'gfm/blocks/05.md' | githubMarkdown }} - -{{#markdown}} -Where it automatically registers any Blocks or Filters contained in the Assembly where the `MyBlock` Type is defined. -{{/markdown}} - -{{#markdown}} -### bold - -A step up from `noop` is the **bold** Template Block which markup its contents within the `` tag: -{{/markdown}} - -
    -{{#raw}}{{#bold}}This text will be bold{{/bold}}{{/raw}}
    -
    - -{{#markdown}} -Which calls the `base.WriteBodyAsync()` method to evaluate and write the Block's contents to the `OutputStream` using the current -`TemplateScopeContext`: -{{/markdown}} - -{{ 'gfm/blocks/06.md' | githubMarkdown }} - -{{#markdown}} -### with - -The `with` Block shows an example of utilizing arguments. To maximize flexibility arguments passed into your block are captured in a free-form -string (specifically a `ReadOnlyMemory`) which allows creating Blocks varying from simple arguments to complex LINQ-like expressions - a -feature some built-in Blocks take advantage of. - -The `with` block works similarly to [handlebars with helper](https://handlebarsjs.com/block_helpers.html#with-helper) or JavaScript's -[with statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with) where it extracts the properties (or Keys) -of an object and adds them to the current scope which avoids needing a prefix each property reference, -e.g. being able to use `{{Name}}` instead of `{{person.Name}}`: - -
    -{{#with person}}
    -    Hi {{Name}}, your Age is {{Age}}.
    -{{/with}}
    -
    - -Also the `with` Block's contents are only evaluated if the argument expression is `null`. - -The implementation below shows the optimal way to implement `with` by calling `GetJsExpressionAndEvaluate()` to resolve a cached -AST token that's then evaluated to return the result of the Argument expression. - -If the argument evaluates to an object it calls the `ToObjectDictionary()` extension method to convert it into a `Dictionary` -then creates a new scope with each property added as arguments and then evaluates the block's Body contents with the new scope: -{{/markdown}} - -{{ 'gfm/blocks/07.md' | githubMarkdown }} - -{{#markdown}} -To better highlight how this works, a non-cached version of `GetJsExpressionAndEvaluate()` involves parsing the Argument string into -an AST Token then evaluating it with the current scope: -{{/markdown}} - -{{ 'gfm/blocks/08.md' | githubMarkdown }} - -{{#markdown}} -The `ParseJsExpression()` extension method is able to parse virtually any [JavaScript Expression](/docs/expression-viewer) into an AST tree -which can then be evaluated by calling its `token.Evaluate(scope)` method. - -##### Final implementation - -The actual [TemplateWithBlock.cs](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Templates/Blocks/TemplateWithBlock.cs) -used in Templates includes extended functionality which uses `GetJsExpressionAndEvaluateAsync()` to be able to evaluate both **sync** and **async** -results. - -##### else if/else statements - -It also evaluates any `block.ElseBlocks` statements which is **functionality available to all blocks** which are able to evaluate any alternative -**else/else if** statements when the main template isn't rendered, e.g. in this case when the `with` block is called with a `null` argument: -{{/markdown}} - -{{ 'gfm/blocks/09.md' | githubMarkdown }} - -{{#markdown}} -### if - -Since all blocks are able to execute any number of `{{else}}` statements by calling `base.WriteElseAsync()`, the implementation for -the `{{if}}` block ends up being even simpler which just needs to evaluate the argument to `bool`. - -If **true** it writes the body with `WriteBodyAsync()` otherwise it evaluates any `else` statements with `WriteElseAsync()`: -{{/markdown}} - -{{ 'gfm/blocks/10.md' | githubMarkdown }} - -{{#markdown}} -### each - -From what we've seen up till now, the [handlebars.js each block](https://handlebarsjs.com/block_helpers.html#iterators) is also -straightforward to implement which just iterates over a collection argument that evaluates its body with a new scope containing the -elements **properties**, a conventional `it` binding for the element and an `index` argument that can be used to determine the -index of each element: -{{/markdown}} - -{{ 'gfm/blocks/11.md' | githubMarkdown }} - -{{#markdown}} -Despite its terse implementation, the above Template Block can be used to iterate over any expression that evaluates to a collection, -inc. objects, POCOs, strings as well as Value Type collections like ints. - -##### Built-in each - -However the built-in [TemplateEachBlock.cs](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Templates/Blocks/TemplateEachBlock.cs) -has a larger implementation to support its richer feature-set where it also includes support for async results, custom element bindings and LINQ-like syntax for -maximum expressiveness whilst utilizing expression caching to ensure any complex argument expressions are only parsed once. -{{/markdown}} - -{{ 'gfm/blocks/12.md' | githubMarkdown }} - -{{#markdown}} -By using `ParseJsExpression()` to parse expressions after each "LINQ modifier", `each` supports evaluating complex JavaScript expressions in each -of its LINQ querying features, e.g: -{{/markdown}} - -
    -
    - -
    -
    -
    -
    -
    - -{{#markdown}} - -##### Custom bindings - -When using a custom binding like `{{#each c in customers}}` above, the element is only accessible with the custom `c` binding which is more efficient -when only needing to reference a subset of the element's properties as it avoids adding each of the elements properties in the items execution scope. - -Check out [LINQ Examples](/linq/restriction-operators) for more live previews showcasing advanced usages of the `{{#each}}` block. - -### raw - -The `{{#raw}}` block is similar to [handlebars.js's raw-helper](https://handlebarsjs.com/block_helpers.html#raw-blocks) which captures -the template's raw text content instead of having its content evaluated, making it ideal for emitting content that could contain -template expressions like client-side JavaScript or template expressions that shouldn't be evaluated on the server such as -Vue, Angular or Ember templates: - -
    -{{#raw}}
    -<div id="app">
    -    {{ message }}
    -</div>
    -{{/raw}}
    -
    - -When called with no arguments it will render its unprocessed raw text contents. When called with a single argument, e.g. `{{#raw varname}}` it will -instead save the raw text contents to the specified global `PageResult` variable and lastly when called with the `appendTo` modifier it will append -its contents to the existing variable, or initialize it if it doesn't exist. - -This is now the preferred approach used in all [.NET Core and .NET Framework Web Templates](http://docs.servicestack.net/templates-websites) for -pages and partials to append any custom JavaScript script blocks they need on the page, e.g: - -
    -{{#raw appendTo scripts}}
    -<script>
    -    //...
    -</script>
    -{{/raw}}
    -
    - -Where any captured custom scripts are rendered at the -[bottom of _layout.html](https://github.com/NetCoreTemplates/templates/blob/8a082a299d59a0b53b9b6e0a07a6fbabf7bf6e2c/MyApp/wwwroot/_layout.html#L49) with: - -
    -    <script src="/assets/js/default.js"></script>
    -
    -    {{ scripts | raw }}
    -
    -</body>
    -</html>
    -
    - -The implementation to support each of these usages is contained within -[TemplateRawBlock.cs](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Templates/Blocks/TemplateRawBlock.cs) -below which inspects the `block.Argument` to determine whether it should capture the contents into the specified variable or write its raw -string contents directly to the OutputStream: -{{/markdown}} - -{{ 'gfm/blocks/13.md' | githubMarkdown }} - -{{#markdown}} -### capture - -The `{{#capture}}` block is similar to the raw block except instead of using its raw text contents, it instead evaluates its contents and captures -the output. It also supports evaluating the contents with scoped arguments where by each property in the object dictionary is added in the -scoped arguments that the template is executed with: -{{/markdown}} - -{{ 'gfm/blocks/14.md' | githubMarkdown }} - -{{#markdown}} -With this we can dynamically generate some markdown, capture its contents and convert the resulting markdown to html using the `markdown` Filter transformer: -{{/markdown}} - -{{ 'gfm/blocks/15.md' | githubMarkdown }} - -{{#markdown}} -### markdown - -The `{{#markdown}}` block makes it even easier to embed markdown content directly in web pages which works as you'd expect where content in a -`markdown` block is converted into HTML, e.g: -{{/markdown}} - -
    {{#raw}}
    -{{#markdown}}
    -## TODO List
    -  - Item 1
    -  - Item 2
    -  - Item 3
    -{{/markdown}}
    -{{/raw}}
    -
    - -{{#markdown}} -Which is now the easiest and preferred way to embed Markdown content in content-rich hybrid web pages like -[Razor Rockstars content pages](https://github.com/NetCoreTemplates/rockwind-webapp/blob/master/app/rockstars/dead/cobain/index.html), -or even this [blocks.html WebPage itself](https://github.com/NetCoreApps/TemplatePages/blob/master/src/wwwroot/docs/blocks.html) which -makes extensive use of markdown. - -As `markdown` block only supports 2 usages its implementation is much simpler than the `capture` block above: -{{/markdown}} - -{{ 'gfm/blocks/16.md' | githubMarkdown }} - -{{#markdown}} -### partial - -The `{{#partial}}` block lets you create In Memory partials which is useful when working with partial filters like `selectPartial` as -it lets you declare multiple partials within the same page, instead of requiring multiple individual files. See docs on -[Inline partials](/docs/partials#inline-partials) for a Live comparison of using in memory partials. -{{/markdown}} - -{{#markdown}} -### html - -The purpose of the html blocks is to pack a suite of generically useful functionality commonly used when generating html. All html blocks -inherit the same functionality with blocks registered for the most popular HTML elements, currently: - -`ul`, `ol`, `li`, `div`, `p`, `form`, `input`, `select`, `option`, `textarea`, `button`, `table`, `tr`, `td`, `thead`, `tbody`, `tfoot`, -`dl`, `dt`, `dd`, `span`, `a`, `img`, `em`, `b`, `i`, `strong`. - -Ultimately they reduce boilerplate, e.g. you can generate a menu list with a single block: - -
    -{{#ul {each:items, id:'menu', class:'nav'} }} 
    -    <li>{{it}}</li> 
    -{{/ul}}
    -
    - -A more advanced example showcasing many of its different features is contained in the example below: -{{/markdown}} - -{{ 'gfm/blocks/18.md' | githubMarkdown }} - -{{#markdown}} -This example utilizes many of the features in html blocks, namely: - - - `if` - only render the template if truthy - - `each` - render the template for each item in the collection - - `where` - filter the collection - - `it` - change the name of each element `it` binding - - `class` - special property implementing [Vue's special class bindings](https://vuejs.org/v2/guide/class-and-style.html) where an **object literal** - can be used to emit a list of class names for all **truthy** properties, an **array** can be used to display a list of class names or you can instead use - a **string** of class names. - -All other properties like `id` and `selected` are treated like HTML attributes where if the property is a boolean like `selected` it's only displayed -if its true otherwise all other html attribute's names and values are emitted as normal. - -For a better illustration we can implement the same functionality above without using any html blocks: -{{/markdown}} - -{{ 'gfm/blocks/19.md' | githubMarkdown }} - -

    Removing Blocks

    - -{{#markdown}} -Like everything else in Templates, all built-in Blocks can be removed. To make it easy to remove groups of related blocks you can just remove the -plugin that registered them using the `RemovePlugins()` API, e.g: -{{/markdown}} - -{{ 'gfm/blocks/20.md' | githubMarkdown }} - -{{ "doc-links" | partial({ order }) }} - diff --git a/src/wwwroot/docs/db-filters.html b/src/wwwroot/docs/db-filters.html deleted file mode 100644 index 64b6af8..0000000 --- a/src/wwwroot/docs/db-filters.html +++ /dev/null @@ -1,295 +0,0 @@ - - -

    - OrmLite's database filters gives your templates database connectivity to the most popular RDBMS's. - To enable install the - OrmLite NuGet package for your RDBMS - and register it in your IOC, e.g: -

    - -{{ 'gfm/db-filters/01.md' | githubMarkdown }} - -

    - Then to enable register either - TemplateDbFiltersAsync - if you're using either SQL Server, PostgreSQL or MySql, otherwise register the sync - TemplateDbFilters - to avoid the pseudo async overhead of wrapping synchronous results in Task results. -

    - -{{ 'gfm/db-filters/02.md' | githubMarkdown }} - -

    - Now your templates will have access to the available DB Filters - where you can use the db* filters - to execute sql queries: -

    - -
      -
    • dbSelect - returns multiple rows
    • -
    • dbSingle - returns a single row
    • -
    • dbScalar - returns a single field value
    • -
    - -

    - The sql* filters are used to enable - cross-platform RDBMS support where it encapsulates the differences behind each RDBMS and returns the appropriate SQL - for the RDBMS that's registered. -

    - -

    DB Filter Examples

    - -

    - For an interactive example that lets you explore DB Filters checkout the Adhoc Querying use-case. -

    - -

    - To explore a complete data-driven Web App built using Templates and DB Filters, checkout the Rockwind Website below - which uses DB Filters to implement both its Web Pages and Web APIs. - The URL - The Source code link on the right shows the source code used to generate the Web Page or API Response: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Rockwind Website
    DescriptionURLSource Code
    Customers/northwind/customers/northwind/customers.html
    Employees/northwind/employees/northwind/employees.html
    Products/northwind/products/northwind/products.html
    Categories/northwind/categories/northwind/categories.html
    Suppliers/northwind/suppliers/northwind/suppliers.html
    Shipppers/northwind/shippers/northwind/shippers.html
    Page Queries
    Customers in Germany/northwind/customers?country=Germany/northwind/customers.html
    Customers in London/northwind/customers?city=London
    Alfreds Futterkiste Details/northwind/customer?id=ALFKI/northwind/customer.html
    Order #10643/northwind/order?id=10643/northwind/order.html
    Employee Nancy Davolio Details/northwind/employee?id=1/northwind/employee.html
    Chai Product Details/northwind/products?id=1/northwind/products.html
    Beverage Products/northwind/products?category=Beverages
    Products from Bigfoot Breweries/northwind/products?supplier=Bigfoot+Breweries
    Products containing Tofu/northwind/products?nameContains=Tofu
    API Queries
    All Customers - - - - -
    Accept HTTP Header also supported
    -
    /api/customers.html
    Alfreds Futterkiste Details - - - -
    As List - - - -
    Customers in Germany - - - -
    Customers in London - - - -
    All Products - - - - /api/products.html
    Chai Product Details - - - -
    As List - - - -
    Beverage Products - - - -
    Products from Bigfoot Breweries - - - -
    Products containing Tofu - - - -
    - -

    Run Rockwind against your Local RDBMS

    - -

    - You can run the Rockwind Website - against either an SQLite, SQL Server or MySql database by just changing which app.settings the App is run with, e.g: -

    - -
    dotnet web/app.dll ../rockwind/web.sqlserver.settings
    - -
    Populate local RDBMS with Northwind database
    - -

    - If you want to run this Web App against your own Database run the - northwind-data - project against your database, e.g: -

    - -
    dotnet run sqlserver "Server=localhost;Database=northwind;User Id=test;Password=test;"
    - -

    - As Rockwind is a Web Template Web App it doesn't need any compilation so after - running the Rockwind Web App - you can modify the source code and see changes in real-time thanks to its built-in - Hot Reloading support. -

    - -

    PostgreSQL Support

    - -

    - Due to PostgreSQL's automatic conversion of unquoted tables and fields to lower case and MySql not supporting double quotes - for quoting symbols, it's not feasible to develop the same website that runs in both MySql and PostgreSQL unless you - use sqlQuote to quote every column or table or are willing to use lowercase or snake_case for all table and column names. - As a result we've developed an alternate version of Rockwind website called Rockwind VFS - which quotes every Table and Column in double quotes so PostgreSQL preserves casing. The - Rockwind VFS Project - can be run against either PostgreSQL, SQL Server or SQLite by changing the configuration it's run with, e.g: -

    - -
    dotnet web/app.dll ../rockwind-vfs/web.postgres.settings
    - -

    - See the Filters API Reference for the - full list of DB filters available. -

    - - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/default-filters.html b/src/wwwroot/docs/default-filters.html deleted file mode 100644 index 6dec5ae..0000000 --- a/src/wwwroot/docs/default-filters.html +++ /dev/null @@ -1,546 +0,0 @@ - - -

    - The default filters are a comprehensive suite of safe filters useful within a View Engine or Text Template Generation environment. - The source code for all default filters are defined in - TemplateDefaultFilters. -

    - -

    - For examples of querying with filters checkout the Live LINQ Examples, we'll show examples for other useful filters below: -

    - -

    Filters as bindings

    - -

    - Filters with no arguments can be used in-place of an argument binding, now and utcNow are examples of this: -

    - -{{ 'live-template' | partial({ template: "now: {{ now | dateFormat }} -UTC now: {{ utcNow | dateFormat('o') }}" }) }} - -

    - Many filters have an implicit default format which can be overridden in the TemplateContext's Args, e.g - if no Date Format is provided it uses the default format yyyy-MM-dd which can be overridden with: -

    - -{{ 'gfm/default-filters/01.md' | githubMarkdown }} - -

    HTML Encoding

    - -

    - All values of Template Expressions in .html pages are HTML-encoded by default, you can bypass the HTML encoding - to emit raw HTML by adding the raw filter as the last filter in an expression: -

    - -{{ 'live-template' | partial({ template: "With encoding: {{ 'hi' }} -Without: {{ 'hi' | raw }}" }) }} - -

    Arithmetic

    - -

    - Templates supports the same arithmetic expressions as JavaScript: -

    - -{{ 'live-template' | partial({ rows: 7, template: "1 + 1 = {{ 1 + 1 }} -2 x 2 = {{ 2 * 2 }} -3 - 3 = {{ 3 - 3 }} -4 / 4 = {{ 4 / 4 }} -3 % 2 = {{ 3 % 2 }} -1 - 2 + 3 * 4 / 5 = {{ 1 - 2 + 3 * 4 / 5 }} -Bitwise = {{ 3 & 1 }}, {{ (3 | 1) }}, {{ 3 ^ 1 }}, {{ 3 << 1 }}, {{ 3 >> 1 }}, {{ ~1 }}" }) }} - -
    - Behavior is as you would expect in JavaScript except for Bitwise OR | needs to be in (parens) to avoid it being treated as a - filter expression separator and no assignment operations are supported, i.e. =, ++, --, |=, etc. The = is treated - as an equality operator == so it lets you use SQL-like queries if preferred, e.g. a = 1 and b = 2 or c = 3 . -
    - -

    - For those who prefer wordier descriptions, you can use the equivalent built-in filters: -

    - -{{ 'live-template' | partial({ rows: 5, template: "1 + 1 = {{ 1 | add(1) }} or {{ add(1,1) }} or {{ 1 | incr }} or {{ 1 | incrBy(1) }} -2 x 2 = {{ 2 | mul(2) }} or {{ multiply(2,2) }} -3 - 3 = {{ 3 | sub(3) }} or {{ subtract(3,3) }} or {{ 3 | decrBy(3) }} -4 / 4 = {{ 4 | div(4) }} or {{ divide(4,4) }} -3 % 2 = {{ 3 | mod(2) }} or {{ mod(3,2) }}" }) }} - -

    - It should be noted when porting C# code that as filters are normal methods they don't follow the implied - Order of Operations used in math calculations - where an expression like 1 - 2 + 3 * 4 / 5 is executed in the implied order of (1 - 2) + ((3 * 4) / 5). - To achieve the same result you'd need to execute them in their implied grouping explicitly: -

    - -{{ 'live-template' | partial({ template: "1 - 2 + 3 * 4 / 5 = {{ 1 - 2 + 3 * 4 / 5 }} -1 - 2 + 3 * 4 / 5 = {{ add(sub(1,2), div(mul(3,4), 5)) }} -((1 - 2 + 3) * 4) / 5 = {{ 1 | subtract(2) | add(3) | multiply(4) | divide(5) }}" }) }} - -

    Math

    - -

    - Most Math APIs are available including pi and e constants: -

    - -{{ 'live-template' | partial({ template: `Circumference = {{ 2 * pi * 10 | round(5) }} -√ log10 10000 = {{ 10000 | log10 | sqrt }} -Powers of 2 = {{ 10 | times | select: { it + 1 | pow(2) }, }}` }) }} - -

    Date Functions

    - -{{ 'live-template' | partial({ rows: 10, template: "{{ now | addTicks(1) }} -{{ now | addMilliseconds(1) }} -{{ now | addSeconds(1) }} -{{ now | addMinutes(1) }} -{{ now | addHours(1) }} -{{ now | addDays(1) }} -{{ now | addMonths(1) }} -{{ now | addYears(1) }} -{{ '2001-01-01' | toDateTime }}, {{ date(2001,1,1) }}, {{ date(2001,1,1,1,1,1) }} -{{ time(1,2,3,4) }}, {{ '02:03:04' | toTimeSpan }}, {{ time(2,3,4) }}" }) }} - -

    Formatting

    - -

    - Use format filters to customize how values are formatted: -

    - -{{ 'live-template' | partial({ rows: 4, template: ′USD {{ 12.34 | currency }} GBP {{ 12.34 | currency('en-GB') }} EUR {{ 12.34 | currency('FR') }} -Number: {{ 123.456 | format('0.##') }} Currency: {{ 123.456 | format('C') }} -Date: {{ now | dateFormat('dddd, MMMM d, yyyy') }} default: {{ now | dateFormat }} -Time: {{ now.TimeOfDay | timeFormat('h\\:mm') }} default: {{ now.TimeOfDay | timeFormat }}′ }) }} - -

    - When no format provided, the default TemplateConstant's formats are used: -

    - -
      -
    • DefaultDateFormat used by: dateFormat
    • -
    • DefaultDateTimeFormat used by: dateTimeFormat
    • -
    • DefaultTimeFormat used by: timeFormat
    • -
    • DefaultCulture used by: currency
    • -
    - -

    String functions

    - -

    - As can be expected from a templating library there's a comprehensive set of string filters available: -

    - -{{ 'live-template' | partial({ rows: 13, template: "{{ 'upper' | upper }} and {{ 'LOWER' | lower }} -{{ 'SubString'|substring(0,3) }} {{ 'SubString'|substring(3) }} {{ 'IsSafe'|substring(2,100) }} -{{ 'SubStrEllipsis' | substringWithEllipsis(9) }} {{ 'SubStr' | substringWithEllipsis(3,3) }} -{{ 'left:part' | leftPart(':') }} + {{ 'part:right' | rightPart(':') }} -{{ 'last:left:part' | lastLeftPart(':') }} + {{ 'last:right:part' | lastRightPart(':') }} -{{ 'split.on.first' | splitOnFirst('.') |join}} + {{ 'split.on.last' | splitOnLast('.') |join}} -{{ 'split.these.words' | split('.') | get(1) }}, {{ 'replace this' | replace('this', 'that') }} -{{ 'index.of' | indexOf('.') }} + {{ 'last.index.of' | lastIndexOf('.') }} -{{ 'start' | appendLine | append('end') }} -{{ 'in' + ' the ' + 'middle' }} -{{ 'in the {0} of the {1} I go {2}' | fmt('middle','night','walking') }} -{{ 'in the {0} of the {1} I go {2} in my {3}' | fmt(['middle','night','walking','sleep']) }} -{{ 'in the ' | appendFmt('{0} of the {1} I go {2}', 'middle','night','walking') }}" }) }} - -

    Text Style

    - -{{ 'live-template' | partial({ template: `{{ 'aVarName' | humanize }} and {{ 'AVarName' | splitCase }} -{{ 'wAr aNd pEaCe' | titleCase }} -{{ 'pascalCase' | pascalCase }} and {{ 'CamelCase' | camelCase }}` }) }} - -

    Trimming and Padding

    - -{{ 'live-template' | partial({ template: "'{{ ' start ' | trimStart }}', '{{ ' end ' | trimEnd }}', '{{ ' both ' | trim }}' -'{{ 'left' | padLeft(10) }}', '{{ 'right' | padRight(10) }}' -'{{ 'left' | padLeft(10,'_') }}', '{{ 'right' | padRight(10,'_') }}'" }) }} - -

    URL handling

    - -{{ 'live-template' | partial({ rows: 8, template: "{{ 'http://example.org' | assignTo: baseUrl }} -{{ baseUrl | addPath('path') }} -{{ baseUrl | addPaths(['path1', 'path2', 'path3']) }} -{{ baseUrl | addQueryString({ a: 1, b: 2 }) }} -{{ baseUrl | addQueryString({ a: 1, b: 2 }) | setQueryString({ a: 3 }) }} -{{ baseUrl | addHashParams({ c: 3, d: 4 }) }} -{{ baseUrl | addHashParams({ c: 3, d: 4 }) | setHashParams({ c: 5 }) }} -{{ baseUrl | addPath('path') | addQueryString({ a: 1 }) | addHashParams({ b: 2 }) }}" }) }} - -

    Repeating, Ranges and Generation

    - -{{ 'live-template' | partial({ rows: 4, template: "{{ 3 | repeating('ABC ') }} or {{ 'ABC ' | repeat(3) }} -{{ 3 | itemsOf(2) | join }} or {{ 3 | itemsOf(2) | sum }} -{{ 5 | times | select: {it}, }} -{{ range(5) | select: {it}, }} {{ range(5,5) | select: {it}, }}" }) }} - -

    Spacing

    - -

    - The following filters can be used to easily control the precise spacing in templates: -

    - -{{ 'live-template' | partial({ rows: 6, template: "{{ space }}1 space -{{ 3 | spaces }}3 spaces -{{ indent }}1 indent -{{ 3 | indents }}3 indents -{{ newLine }}1 new line above -{{ 3 | newLines }}3 new lines above" }) }} - -
    - The default spacing used for indents \t can be overridden with the Args[DefaultIndent]. -
    - -

    Conditional Tests

    - -

    - These filters allow you to test for different conditions: -

    - -{{ 'live-template' | partial({ rows: 17, template: "ternary: {{ true ? 1 : 0 }}, {{ 1 + 1 > subtract(2, 1) ? 'YES' : 'NO' }} -or: {{ true || false }}, {{ false || false }}, {{ true | OR(true) }} -and: {{ true && false }}, {{ false and false }}, {{ true | AND(true) }} -exists: {{ null | exists }}, {{ 1 | exists }}, {{ unknownArg | exists }} -equals: {{ 1 = 1 }}, {{ 1 == 1 }}, {{ 1 | equals(1) }}, {{ 'A' | equals('A') }}, {{ 1 | eq(1) }} -notEquals: {{ 2 != 1 }}, {{ 2 | notEquals(1) }}, {{ 'A' | notEquals('A') }} or {{ 1 | not(1) }} -greaterThan: {{ 1 > 1 }}, {{ 1 | greaterThan(1) }} or {{ 1 | gt(1) }} -greaterEqual: {{ 1 >= 1 }}, {{ 1 | greaterThanEqual(1) }} or {{ 1 | gte(1) }} -lessThan: {{ 1 < 1}}, {{ 1 | lessThan(1) }} or {{ 1 | lt(1) }} -lessEqual: {{ 1 <= 1}}, {{ 1 | lessThanEqual(1) }} or {{ 2 | lte(1) }} -isNull: {{ null == null }}, {{ null | isNull }}, {{ 1 | isNull }}, {{ 1 == null }} -isNotNull: {{ null != null }} {{ null | isNotNull }}, {{ 1 | isNotNull }}, {{ 1 != null }} -equivalentTo: {{ [1,2] | equivalentTo([1,2]) }}, {{ {a:1, b:2} | equivalentTo({a:1, b:2}) }} -contains: {{ [1,2] | contains(1) }}, {{ [1,2] | contains(3) }}, {{ 'ABC' | contains('A') }} -Even/Odd: {{ 1 | isEven }}, {{ 1 % 2 == 0 }}, {{ 1 | isOdd }}, {{ 1 % 2 == 1 }} -counts: {{ [1,2] | length }}, {{ [1,2] | hasMinCount(1) }}, {{ [1,2] | hasMaxCount(1) }} -starts/ends: {{ 'startsWith' | startsWith('start') }}, {{ 'endsWith' | endsWith('end') }}" }) }} - -

    Object Type Tests

    - -

    - Filters to check the type of objects: -

    - -{{ 'live-template' | partial({ rows: 12, template: "Any Number: {{ 1 | isInteger }}, {{ 1 | toLong | isInteger }}, {{ 1.1 | isNumber }} -Real Numbers: {{ 1.1 | isDouble }}, {{ 1.1 |toFloat| isFloat }}, {{ 1.1 |toDecimal| isDecimal }} -ints: {{ 1 | isInt }}, {{ 1 | isLong }}, {{ 1 | toLong | isLong }} -strings: {{ 'a' | isString }}, {{ 'a' | isChar }}, {{ 'a' | toChar | isChar }} -bool: {{ false | isBool }}, -bytes: {{ 1 | toByte | isByte }}, {{ 'a' | toUtf8Bytes | isBytes }} -chars: {{ 'abc' | toChars | isChars }}, {{ ['a','b','c'] | toChars | isChars }} -enumerable: {{ [1] | isEnumerable }}, {{ {a:1} | isEnumerable }}, {{ 'abc' | isEnumerable }} -list: {{ [1] | isList }}, {{ {a:1} | isList }}, {{ 'abc' | isList }} -dictionary: {{ {a:1} | isDictionary }}, {{ [1] | isDictionary }}, {{ 'abc' | isDictionary }} -object dict: {{ {a:1} | isObjectDictionary }}, {{ {'a':'b'} | isObjectDictionary }} -value/ref type: {{ 1 | isValueType }}, {{ [] | isValueType }}, {{ [] | isClass }}" }) }} - -

    Object Conversions

    - -

    - Conversions and transformations between different types: -

    - -{{ 'live-template' | partial({ rows: 5, template: `toInt: {{ '1' | typeName }}, {{ '1' | toInt | typeName }}, {{ 1 | toDouble | typeName }} -toChar: {{ ',' | typeName }}, {{ ',' | toChar |typeName }}, {{'true' | toBool | typeName }} -{{ {a:1,b:2,c:'',d:null} | assignTo: o }} -toKeys: {{ o | toKeys | join }}, toValues: {{ o | toValues | join }} -without: {{ o | withoutNullValues | toKeys|join }} / {{ o | withoutEmptyValues | toKeys| join }}` }) }} - -

    Conditional Display

    - -

    - These filters can be used in combination with Conditional Test filters above to control what text is displayed: -

    - -{{ 'live-template' | partial({ rows: 11, template: "if: {{ 'Y' | if(true) }}, {{ 'Y' | if(false) | otherwise('N') }}, {{ 'Y' | if(1) }} -when: {{ 'Y' | when(true) }}, {{ 'Y' | when(false) | otherwise('N') }}, {{ 'Y' | when(1) }} -if!: {{ 'Y' | if(!true) | otherwise('N') }}, {{ 'Y' | if(!false) }}, {{ 'Y' | if(!1) }} -ifNot: {{ 'Y' | ifNot(true) }}, {{ 'Y' | ifNot(false) }}, {{ 'Y' | ifNot(1) }} -unless: {{ 'Y' | unless(true) }}, {{ 'Y' | unless(false) }}, {{ 'Y' | unless(1) }} -otherwise: {{ null | otherwise('Y') }} or {{ 'even' | if(isEven(1)) | otherwise('odd') }} -iif: {{ isEven(1) | iif('even', 'odd') }} or {{ iif(isEven(1), 'even', 'odd') }} -ifFalsy: {{ 'F' | ifFalsy(false) }}, {{ 'F' | ifFalsy(0) }}, {{ 'F' | ifFalsy(null) }} -falsy: {{ false | falsy('F') }}, {{ 0 | falsy('F') }}, {{ null | falsy(F) }} -ifTruthy: {{ 'T' | ifTruthy(true) }}, {{ 'T' | ifTruthy(1) }}, {{ 'T' | ifTruthy(null) }} -truthy: {{ true | truthy('T') }}, {{ 1 | truthy('T') }}, {{ null | truthy('T') }}" }) }} - -

    Content Handling

    - -{{ 'live-template' | partial({ rows: 7, template: `default: {{ title | default('A Title') }} -{{ title != null ? title : 'A Title' }} -{{ 'The Title' | assignTo: title }}{{ title | default('A Title') }} : {{ 1 | ifExists(title) }} -{{ noArg }} : {{ noArg | ifExists }} : {{ 1 | ifNotExists(noArg) }} : {{ 1 | ifNo(noArg) }} -{{ 'empty' | ifEmpty('') }} : {{ 'empty' | ifEmpty([]) }} : {{ 'empty' | ifEmpty([1]) }} -{{ [1,2,3] | assignTo: nums }}{{ nums | join | assignTo: list }} -{{ "nums {0}" | fmt(list) | ifNotEmpty(nums) }}{{ "nums {0}" | fmt(list) | ifNotEmpty([]) }}` }) }} - -

    Control Execution

    - -

    - The end* filters short-circuits the execution of a filter and discard any results. - They're useful to use in combination with the - use* filters - which discards the old value and creates a new value - to be used from that point on in the expression. - show is an alias - for use that reads better when used at the end of an expression. -

    - -{{ 'live-template' | partial({ rows: 12, template: `always {{ 1 | end | default('unreachable') }} -null {{ 1 | endIfNull }}/{{ null | endIfNull | default('N/A') }} -empty {{ '' | endIfEmpty | default('N/A') }}/{{ [] | endIfEmpty | default('N/A') }} -if {{ true | ifEnd | use(1) }}/{{ false | ifEnd | use(1) }}:{{ endIf(true) | show: 1 }} -do {{ true | ifDo | select: 1 }}/{{ false | ifDo | select: 1 }}/{{ doIf(true) | show: 1 }} -any {{ 5 | times | endIfAny('it = 4') | join }}/{{ 5 | times | endIfAny('it = 5') | join }} -all {{ 5 | times | endIfAll('lt(it,4)') |join }}/{{ 5 | times | endIfAll('lt(it,5)') |join }} -where {{ 1 | endWhere: isString(it) }}/{{ 'a' | endWhere: isString(it) }} -useFmt {{ arg | endIfExists | useFmt('{0} + {1}', 1, 2) | assignTo: arg }}{{ arg }} -useFmt {{ arg | endIfExists | useFmt('{0} + {1}', 3, 4) | assignTo: arg }}{{ arg }} -useFormat {{ arg2 | endIfExists | useFormat('value', 'key={0}') | assignTo: arg2 }}{{ arg2 }} -{{ noArg | end }} : {{ 1 | end }} : {{ 1 | incr | end }}` }) }} - -
    - You can also use end to discard return values of filters with side-effects, block filters, partials, etc. - It also provides an easy way to comment out any expression by prefixing it at the start, e.g: {{#raw}}{{ end | unreachable }}{{/raw}} -
    - -

    - The ifUse and useIf filters are the inverse of the end* filters where they continue - execution with a new value if the condition is true: -

    - -{{ 'live-template' | partial({ rows: 2, template: `ifUse: {{ true | ifUse(1) }} / {{ false | ifUse(1) | default('unreachable') }} -useIf: {{ 1 | useIf(true) }} / {{ 1 | useIf(false) | default('unreachable') }}` }) }} - -

    - There's also an - only* filter - for each - end* filter - above with the inverse behavior, e.g: -

    - -{{ 'live-template' | partial({ rows: 3, template: `endIfEmpty: {{ a | endIfEmpty | show: a is not empty }} -onlyIfEmpty: {{ a | onlyIfEmpty | show: a is empty }}` }) }} - -

    Assignment

    - -

    - You can create temporary arguments within a templates scope or modify existing arguments with: -

    - -{{ 'live-template' | partial({ rows: 6, template: "{{ [1,2,3,4,5] | assignTo: numbers }} -{{ numbers | join }} -{{ numbers | do => assign('numbers[index]', it * it) }} -{{ numbers | join }} -{{ 5 | times | do => assign(`num${index}`, it) }} -{{ num4 }}" }) }} - -

    Let Bindings and Scope Vars

    - -

    - Let bindings allow you to create scoped argument bindings from individual string expressions: -

    - -{{ 'live-template' | partial({ rows: 4, template: ′{{ [{name:'Alice',score:50},{name:'Bob',score:40}] | assignTo:scoreRecords }} -{{ scoreRecords - | let => { name:it.name, score:it.score, personNum:index + 1 } - | select: {personNum}. {name} = {score}\n }}′ }) }} - -

    - scopeVars lets you create bindings from a Dictionary, List of KeyValuePair's or List of Dictionaries using the key - as the name of the argument binding and the value as its value: -

    - -{{ 'live-template' | partial({ template: "{{ [{name:'Alice',score:50},{name:'Bob',score:40}] | assignTo:scoreRecords }} -{{ scoreRecords | scopeVars | select: {index + 1}. {name} = {score}\n }}" }) }} - -

    Querying Objects

    - -{{ 'live-template' | partial({ rows: 5, template: `{{ [10,20,30,40,50] | assignTo: numbers }} -{{ { a:1, b:2, c:3 } | assignTo: letters }} -Number at [3]: {{ numbers[3] }}, {{ numbers | get(3) }} -Value of 'c': {{ letters['c'] }}, {{ letters.c }}, {{ letters | get('c') }} -Property Value: {{ 'A String'.Length }}, {{ 'A String'['Len' + 'gth'] }}` }) }} - -

    Member Expressions

    - -{{ 'live-template' | partial({ rows: 4, template: `{{ [now] | assignTo: dates }} -{{ round(dates[0].TimeOfDay.TotalHours, 3) }} -{{ dates | get(0) | select: { it.TimeOfDay.TotalHours | round(3) } }} -{{ [now.TimeOfDay][0].TotalHours | round(3) }}` }) }} - -

    - JavaScript member expressions are supported except for calling methods on instances as only registered filters can be invoked. -

    - -

    Mapping and Conversions

    - -

    - Use map when you want to transform each item into a different value: -

    - -{{ 'live-template' | partial({ rows: 4, template: "{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] | assignTo:digits }} -{{ range(3) - | map => it + 5 - | map => digits[it] | join(`\\n`) }}" }) }} - -

    - It's also useful for transforming raw data sources into more manageable ones: -

    - -{{ 'live-template' | partial({ rows: 5, template: "{{ [[1,-2],[3,-4],[5,-6]] | assignTo:coords }} -{{ coords - | map => { x: it[0], y: it[1] } - | scopeVars - | select: {index + 1}. ({x}, {y})\\n }}" }) }} - -

    - Whilst these filters let you perform some other popular conversions: -

    - -{{ 'live-template' | partial({ rows: 8, template: "{{ 100 | toString | select: {it.Length} }} -{{ { x:1, y:2 } | toList | map => `${it.Key} = ${it.Value}` | join(', ') }} -{{ range(5) | toArray | assignTo: numbers }} -{{ numbers.Length | times | do => assign('numbers[index]', -numbers[index]) }} -{{ numbers | join }} -Bob's score: {{ [{name:'Alice',score:50},{name:'Bob',score:40}] - | toDictionary => it.name | map => it.Bob.score }}" }) }} - -

    - Use parseKeyValueText to convert a key/value string into a dictionary, you can then use - values and keys to extract the Dictionaries keys or values collection: -

    - -{{ 'live-template' | partial({ rows: 8, template: `{{' -Rent: 1000 -Internet: 50 -Mobile: 50 -Food: 400 -Misc: 200 -'| trim | parseKeyValueText(':') | assignTo: expenses }} -Expenses: {{ expenses | values | sum | currency }}` }) }} - -

    Serialization

    - -

    - Use the json filter when you want to make your C# objects available to the client JavaScript page: -

    - -{{ 'live-template' | partial({ template: `{{ [{x:1,y:2},{x:3,y:4}] | assignTo:model }} -var coords = {{ model | json }};` }) }} - -

    Embedding in JavaScript

    - -

    - You can use the filters below to embed data on the client in JavaScript. If it's a valid JS Object or JSON it can - be embedded as a native data JS structure without quotes, otherwise use jsString to capture it in a - JavaScript string variable: -

    - -{{ 'live-template' | partial({ rows: 9, template: ′{{ '[ - {"name":"Mc Donald\'s"} -]' | assignTo:json }} -var obj = {{ json }}; -var str = '{{ json | jsString }}'; -var str = {{ json | jsQuotedString }}; -var escapeSingle = '{{ "single' quote's" | escapeSingleQuotes }}'; -var escapeDouble = "{{ 'double" quote"s' | escapeDoubleQuotes }}" -var escapeLines = '{{ "new" | appendLine | append("line") | escapeNewLines }}';′ }) }} - -

    - The JSV Format is great when you want a human-friendly output - of an object graph, you can also use dump if you want the JSV output indented: -

    - -{{ 'live-template' | partial({ template: `{{ [{x:1,y:2},{x:3,y:4}] | assignTo: model }} -{{ model | jsv }} -{{ model | dump }}` }) }} - -

    - If needed, the csv and xml serialization formats are also available: -

    - -{{ 'live-template' | partial({ template: `{{ [{Name:'Alice',Score:50},{Name:'Bob',Score:40}] | assignTo:scoreRecords }} -{{ scoreRecords | csv }}` }) }} - -

    Eval

    - -

    - By default the evalTemplate filter renders Templates in a new TemplateContext which can be customized - to utilize any additional plugins, filters and blocks available in the configured - TemplatePagesFeature, or for full access you can use {use:{context:true}} - to evaluate any Template content under the same context that evalTemplate is run on. -

    - -

    - E.g. you can evaluate dynamic Template Syntax which makes use of the MarkdownTemplatePlugin plugin with: -

    - -
    {{#raw}}{{ content | evalTemplate({use:{plugins:'MarkdownTemplatePlugin'}}) | raw }}{{/raw}}
    - -

    - This filter is used by the /preview.html API Page to - create an API that enables live previews. -

    - -

    Iterating

    - -

    - The select filter and its inverse forEach are the primary way to generate custom output - for each item in a collection: -

    - -{{ 'live-template' | partial({ rows: 5, template: `{{ [{name:'Alice',score:50},{name:'Bob',score:40}] | assignTo:scoreRecords }} -select -
      {{ scoreRecords | select:
    1. {it.name} = {it.score}
    2. }}
    -forEach -
      {{ '
    1. {{ it.name }} = {{ it.score }}
    2. ' | forEach(scoreRecords) }}
    ` }) }} - -

    - Use step if you want to iterate using a custom step function: -

    - -{{ 'live-template' | partial({ rows: 1, template: `{{ range(20) | step({ from: 10, by: 3 }) | join }}` }) }} - -

    Miscellaneous

    - -

    Use #raw block filter or pass to emit a Template Expression without evaluating it: -

    - -{{ 'live-template' | partial({ rows: 4, template: `{{ pass: 'shout' | upper }} -{{#raw}} -{{ 'shout' | upper }} -{{/raw}}` }) }} - -
    - Useful when you want to emit a client-side Vue filter or show examples of Template Expressions. -
    - -

    - of will let you find values of a specified type: -

    - -{{ 'live-template' | partial({ rows: 1, template: `{{ ['A',1.1,2,3.3,true,null] | of({ type: 'Double' }) | join }}` }) }} - -

    - You can use appSetting filter to access the value of the TemplateContext's configured AppSettings Provider: -

    - -
    {{ pass: 'websitePublicUrl' | appSetting }} 
    - -

    Collections and Querying

    - -

    - Checkout the Live LINQ examples to explore the various collection and querying features. -

    - -

    Filter API Reference

    - -

    - See the Filter API Reference for the - full list of default filters available. -

    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/deploying-web-apps.html b/src/wwwroot/docs/deploying-web-apps.html deleted file mode 100644 index fdfb3b2..0000000 --- a/src/wwwroot/docs/deploying-web-apps.html +++ /dev/null @@ -1,202 +0,0 @@ - - -

    - As all Web Apps use the same pre-compiled /web binary, - deployment is greatly simplified as it only needs to be concerned with deploying static files and starting web/app.dll - with the App's app.settings. -

    - -

    Deploying Web Apps to Ubuntu

    - -

    - A common way for reliably hosting .NET Core Apps on Ubuntu is to use supervisor - to monitor the dotnet self-hosting processes behind an nginx reverse proxy which handles external HTTP requests to - your website and proxies them to the dotnet process running your Web App on a local port. You'll need access to a Unix - environment on your client Desktop, either using Linux, OSX or - Installing Windows Subsystem for Linux (WSL). -

    - - -{{#markdown}} -### Setup the deploy User Account - -Using a Unix command-line or [Windows Subsystem for Linux (WSL)](https://github.com/ServiceStack/redis-windows#option-1-install-redis-on-ubuntu-on-windows) -ssh into your remote server: - - $ ssh deploy@web-apps.io - -We'll start by creating a dedicated user account for hosting and running your .NET Core Apps to mitigate potential abuse. -SSH into your Ubuntu server and create the `deploy` user account with a `/home/deploy` home directory and add -them to the `sudo` group: - - sudo useradd -m deploy - sudo usermod -aG sudo deploy - -For seamless deployments use `visudo` to allow `deploy` to run `supervisorctl` without -prompting for a password: - - # Allow members of group sudo to execute any command - %sudo ALL=(ALL:ALL) ALL - %deploy ALL=NOPASSWD: /usr/bin/supervisorctl, /home/deploy/.dotnet/tools/web - -> In vi type `i` to start editing a file and `ESC` to quit edit mode and `:wq` to save your changes before exiting. - -For simplifying the one-time setup, it's easier to sign-in as super user: - - $ sudo su - - -#### Install the dotnet `web` tool: - - $ dotnet tool install -g web - -Change into the directory where want to install the app: - - $ cd /home/deploy/apps - -Install your app from your GitHub project: - - $ web install spirals --source mythz - -Optional, but you can test that your app runs without issues with: - - $ cd spirals && web - -#### Setup nginx - -To setup the nginx reverse proxy for this website change into: - - $ cd /etc/nginx/sites-available/ - -Then create an nginx website configuration template with: - - web init nginx - -Rename it to the domain name you want to host it under: - - $ mv my-app.web-app.io spirals.web-app.io - -Edit the file and replace it to use your App settings: - - $ vi spirals.web-app.io - -Specifically you want to change the **domain name** and **port** you want to host it on which you can do quickly with the search/replace commands: - - :%s/my-app.web-app.io/spirals.web-app.io/g - :%s/5000/5009/g - -Then type `:wq` to quit vi. To enable the site in nginx we need to create a symbolic link in `/sites-enabled`: - - $ ln -s /etc/nginx/sites-available/spirals.web-app.io /etc/nginx/sites-enabled/spirals.web-app.io - -Then reload `nginx` to pick up changes: - - $ /etc/init.d/nginx reload - -#### Setup supervisor - -Now that we have our website configured with nginx we need to setup supervisor to start and monitor the .NET Core process by changing into: - - $ cd /etc/supervisor/conf.d - -Then generate a supervisor configuration template with: - - $ web init supervisor - -Rename it to your Web App's folder name: - - $ mv app.my-app.conf app.spirals.conf - -Then use vi to customize the configuration for your App: - - $ vi app.spirals.conf - -If your apps are located in `/home/deploy/apps/` then you can just run the Search/Replace rules: - - :%s/my-app/spirals/g - :%s/5000/5009/g - -Which will change it to: - - [program:app-spirals] - command=/home/deploy/.dotnet/tools/web --release - directory=/home/deploy/apps/spirals - autostart=true - autorestart=true - stderr_logfile=/var/log/app-spirals.err.log - stdout_logfile=/var/log/app-spirals.out.log - environment=ASPNETCORE_ENVIRONMENT=Production,ASPNETCORE_URLS="http://*:5009/" - user=deploy - stopsignal=INT - -> The `--release` flag overrides **debug** in `app.settings` so it's always run in release mode. - -After reviewing the changes, tell supervisor to register and start the supervisor process with: - - $ supervisorctl update - -Where your website will now be up and running at: [spirals.web-app.io](http://spirals.web-app.io). - -### Deploying Updates - -After the one-time setup above, updating Web Apps are more easily done with: - - $ cd /home/deploy/apps - $ sudo /home/deploy/.dotnet/tools/web install spirals --source mythz - $ sudo supervisorctl restart app-spirals - -Which can also be deployed from the Windows Command Prompt using a remote SSH command by combining the above commands in a `deploy-spirals.sh` text file: - - ssh -t deploy@web-app.io "cd /home/deploy/apps && sudo /home/deploy/.dotnet/tools/web install spirals --source mythz && sudo supervisorctl restart app-spirals" - -Where App updates can then be performed with a single WSL bash command from the Windows Command Prompt: - - $ bash deploy-spirals.sh - -### Using Travis CI to deploy using Docker to AWS ECS - -A popular combination for deploying .NET Core Apps is to use the online [Travis CI](https://travis-ci.org) -Continuous Integration Service to package your App in a Docker Container and deploy it to AWS ECS which takes care of -the management and deployment of Docker instances over a configured cluster of EC2 compute instances. - -The easiest way to set this up is to clone the [rockwind-aws](https://github.com/NetCoreWebApps/rockwind-aws) -Web App which is preconfigured with a working scripts using Travis CI to package the Web App in a Docker container -and deploy it to AWS ECS. In your local copy replace the -[/app](https://github.com/NetCoreWebApps/rockwind-aws/tree/master/app) folder with your App files, e.g: - -#### [Dockerfile](https://github.com/NetCoreWebApps/rockwind-aws/blob/master/Dockerfile) - -{{/markdown}} - -{{ 'gfm/deploying-web-apps/02.md' | githubMarkdown }} - -

    - The only other file that needs to change is deploy-envs.sh to configure it to use your App's deployment settings: -

    - -

    deploy-envs.sh

    - -{{ 'gfm/deploying-web-apps/03.md' | githubMarkdown }} - -

    Setup AWS ECS and Travis CI

    - -

    - After configuring your App deployment scripts you'll then need to - Setup your AWS ECS - with an EC2 instance to deploy to and - Create your project in Travis CI. - You'll then need to add your AWS Account details in the Travis CI project using - Secure Environment Variables - to store your AWS_ACCOUNT_ID, AWS_ACCESS_KEY and AWS_SECRET_KEY as well as any sensitive info and - connection strings your App uses. -

    - -

    Let us know what you create!

    - -We hope you're excited about these new features as we are and can't wait to see what you build with them - please -share them with us -so we can include it in the App Gallery and make it easy for everyone else to discover and use. - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/error-handling.html b/src/wwwroot/docs/error-handling.html deleted file mode 100644 index 5d13270..0000000 --- a/src/wwwroot/docs/error-handling.html +++ /dev/null @@ -1,312 +0,0 @@ - - -

    - Templates uses Exceptions for error flow control flow that by default will fail fast by emitting the Exception details at - the point where the error occured before terminating the Stream and rethrowing the Exception which lets you for - instance catch the Exception in Unit Tests. -

    - -

    Handled Exceptions

    - -

    - You can instead choose to handle exceptions and prevent them from short-circuiting page rendering by assigning them to an - argument using either the assignError filter which will capture any subsequent Exceptions thrown on the page: -

    - -{{ 'live-template' | partial({ rows:3, template: `{{ 'ex' | assignError }} -{{ 'An error!' | throw }} -{{ ex | select: { it.Message } }}` }) }} - -

    - Alternatively it can be specified on each call-site where it will capture the Exception thrown by the filter: -

    - -{{ 'live-template' | partial({ rows:3, template: `{{ 'An error!' | throw({ assignError: 'ex' }) }} -{{ ex | select: { it.Message } }}` }) }} - -
    - An easy way to prettify errors in Web Pages is to use HTML Error Filters. -
    - -

    Unhandled Exceptions Behavior

    - -

    - If Exceptions are unassigned they're considered to be unhandled. The default behavior for TemplateContext is to - rethrow Exceptions, but as ServiceStack's TemplatePagesFeature is executed within the context of a HTTP Server - it's default is configured to: -

    - -{{ 'gfm/error-handling/01.md' | githubMarkdown }} - -

    - Which instead captures any unhandled Exceptions in PageResult.LastFilterError and continues rendering the page except - it skips evaluating any subsequent filters and instead only evaluates the filters which handle errors, - currently: -

    - -{{#raw}} - - - - - - - - - - - - - - - - - - - - - -
    ifError - Only execute the filter if there's an error:
    - {{ ifError | select: FAIL! { it.Message } }} -
    lastError - Returns the last error on the page or null if there was no error, that's passed to the next filter:
    - {{ lastError | ifExists | select: FAIL! { it.Message } }} -
    htmlError - Display detailed htmlErrorDebug when in DebugMode - otherwise display a more appropriate end-user htmlErrorMessage. -
    htmlErrorMessageDisplay the Exception Message
    htmlErrorDebugDisplay a detailed Exception
    -{{/raw}} - -

    - This behavior provides the optimal UX for developers and end-users as HTML Pages with Exceptions are still rendered well-formed - whilst still being easily able to display a curated error messages for end-users without developers needing to guard against - executing filters when Exceptions occur. -

    - -

    Controlling Unhandled Exception Behavior

    - -

    - We can also enable this behavior on a per-page basis using the skipExecutingFiltersOnError filter: -

    - -{{ 'live-template' | partial({ rows:5, template: `{{ skipExecutingFiltersOnError }} -

    {{ 'Before Exception' }}

    -{{ 'An error!' | throw }} -

    {{ 'After Exception' }}

    -{{ htmlErrorMessage }}` }) }} - -

    - Here we can see that any normal filters after exceptions are never evaluated unless they're specifically for handling Errors. -

    - -

    Continue Executing Filters on Unhandled Exceptions

    - -

    - We can also specify that we want to continue executing filters in which case you'll need to manually guard filters you want to - control the execution of using the ifNoError or ifError filters: -

    - -{{ 'live-template' | partial({ rows:8, template: `{{ continueExecutingFiltersOnError }} -{{ ifNoError | select: No Exception thrown yet! }} -

    {{ 'Before Exception' }}

    -{{ 'An error!' | throw }} -{{ ifError | select: There was an error! }} -

    {{ 'After Exception' }}

    -{{ htmlErrorDebug }}` }) }} - -

    Accessing Page Exceptions

    - -

    - The last Exception thrown are accessible using the lastError* filters: -

    - -{{ 'live-template' | partial({ rows:6, template: `{{ continueExecutingFiltersOnError }} -{{ 'arg' | throwArgumentNullExceptionIf(true) }} - - - -
    {{ lastError | typeName }}
    Last Error Message{{ lastErrorMessage }}
    Last Error StackTrace{{ lastErrorStackTrace }}
    ` }) }} - -

    Throwing Exceptions

    - -

    - We've included a few of the popular Exception Types, filters prefixed with throw always throws the Exceptions below: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    FilterException
    throwException(message)
    throwArgumentExceptionArgumentException(message)
    throwArgumentNullExceptionArgumentNullException(paramName)
    throwNotSupportedExceptionNotSupportedException(message)
    throwNotImplementedExceptionNotImplementedException(message)
    throwUnauthorizedAccessExceptionUnauthorizedAccessException(message)
    throwFileNotFoundExceptionFileNotFoundException(message)
    throwOptimisticConcurrencyExceptionOptimisticConcurrencyException(message)
    - -
    - You can extend this list with your own custom filters, see the - Error Handling Filters - for examples. -
    - -

    - These filters will only throw if a condition is met: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    FilterException
    throwIfmessage | Exception | if(test)
    throwArgumentNullExceptionIfparamName | ArgumentNullException | if(test)
    ifthrowif(test) | Exception(message)
    ifThrowArgumentNullExceptionif(test) | ArgumentNullException(paramName)
    ifThrowArgumentException - if(test) | ArgumentException(message)
    - if(test) | ArgumentException(message, paramName)
    -
    - -

    Ensure Argument Helpers

    - -

    - The ensureAll* filters assert the state of multiple arguments where it will either throw an Exception unless - all the arguments meet the condition or return the Object Dictionary if all conditions are met: -

    - -{{ 'live-template' | partial({ rows:5, template: `{{ 1 | assignTo: one }}{{ 'bar' | assignTo: foo }}{{ '' | assignTo: empty }} -{{ { one, foo, empty } | ensureAllArgsNotNull | htmlDump({ caption: 'ensureAllArgsNotNull' }) }} -ensureAllArgsNotEmpty: -{{ { one, foo, empty } | ensureAllArgsNotEmpty({ assignError:'ex' }) | htmlDump }} -{{ ex | htmlErrorMessage }}` }) }} - -

    - The ensureAny* filters only requires one of the arguments meet the condition to return the Object Dictionary: -

    - -{{ 'live-template' | partial({ rows:5, template: `{{ '' | assignTo: empty }} -{{ { foo, empty } | ensureAnyArgsNotNull | htmlDump({ caption: 'ensureAnyArgsNotNull' }) }} -ensureAnyArgsNotEmpty: -{{ { foo, empty } | ensureAnyArgsNotEmpty({ assignError:'ex' }) | htmlDump }} -{{ ex | htmlErrorMessage }}` }) }} - -

    Fatal Exceptions

    - -

    - The Exception Types below are reserved for Exceptions that should never happen, such as incorrect usage of an API where it would've - resulted in a compile error in C#. When these Exceptions are thrown in a filter or a page they'll immediately short-circuit execution of - the page and write the Exception details to the output. - The Fatal Exceptions include: -

    - -
      -
    • NotSupportedException
    • -
    • NotImplementedException
    • -
    • TargetInvocationException
    • -
    - -

    Rendering Exceptions

    - -

    - The OnExpressionException delegate in - Page Formats - are able to control how Exceptions in filter expressions are rendered where if preferred exceptions can be rendered in-line - in the filter expression where they occured with: -

    - -{{ 'gfm/error-handling/02.md' | githubMarkdown }} - -

    - The OnExpressionException can also suppress Exceptions from being displayed by capturing any naked Exception Types registered in - TemplateConfig.CaptureAndEvaluateExceptionsToNull - and evaluate the filter expression to null which by default will suppress the following naked Exceptions thrown in filters: -

    - -
      -
    • NullReferenceException
    • -
    • ArgumentNullException
    • -
    - -

    Implementing Filter Exceptions

    - -

    - In order for your own Filter Exceptions to participate in the above Template Error Handling they'll need to be wrapped in an - StopFilterExecutionException including both the Template's scope and an optional options object - which is used to check if the assignError binding was provided so it can automatically populate it with the Exception. -

    - -

    - The easiest way to Implement Exception handling in filters is to call a managed function which catches all Exceptions and - throws them in a StopFilterExecutionException as seen in OrmLite's - TemplateDbFilters: -

    - -{{ 'gfm/error-handling/03.md' | githubMarkdown }} - -

    - The overloads are so the filters can be called without specifying any filter options. -

    - -

    - For more examples of different error handling features and strategies checkout: - TemplateErrorHandlingTests.cs -

    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/filters-reference.html b/src/wwwroot/docs/filters-reference.html deleted file mode 100644 index 8da2391..0000000 --- a/src/wwwroot/docs/filters-reference.html +++ /dev/null @@ -1,141 +0,0 @@ - - -{{ 'nameContains,tab' | importRequestParams }} - -
    -
    -
    -
    - - - - -
    -
    -
    -
    - - - -{{ `` | raw | assignTo: scripts }} - - - -{{ 6 | assignTo: rows }} -
    -
    -{{ "live-template" | partial({ rows, className, template:`{{ 'TemplateDefaultFilters' | assignTo: filter }} -{{ filter | filtersAvailable | where => contains(lower(it.Name), lower(nameContains ?? '')) - | assignTo: filters }} - -{{#each filters}}{{/each}} -
    {{ filter | filterLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} -
    -
    -{{ "live-template" | partial({ rows, className, template:`{{ 'TemplateHtmlFilters' | assignTo: filter }} -{{ filter | filtersAvailable | where => contains(lower(it.Name), lower(nameContains ?? '')) - | assignTo: filters }} - -{{#each filters}}{{/each}} -
    {{ filter | filterLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} -
    -
    -{{ "live-template" | partial({ rows, className, template:`{{ 'TemplateProtectedFilters' | assignTo: filter }} -{{ filter | filtersAvailable | where => contains(lower(it.Name), lower(nameContains ?? '')) - | assignTo: filters }} - -{{#each filters}}{{/each}} -
    {{ filter | filterLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} -
    -
    -{{ "live-template" | partial({ rows, className, template:`{{ 'TemplateInfoFilters' | assignTo: filter }} -{{ filter | filtersAvailable | where => contains(lower(it.Name), lower(nameContains ?? '')) - | assignTo: filters }} - -{{#each filters}}{{/each}} -
    {{ filter | filterLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} -
    -
    -{{ "live-template" | partial({ rows, className, template:`{{ 'TemplateRedisFilters' | assignTo: filter }} -{{ filter | filtersAvailable | where => contains(lower(it.Name), lower(nameContains ?? '')) - | assignTo: filters }} - -{{#each filters}}{{/each}} -
    {{ filter | filterLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} -
    -
    -{{ "live-template" | partial({ rows, className, template:`{{ 'TemplateDbFiltersAsync' | assignTo: filter }} -{{ filter | filtersAvailable | where => contains(lower(it.Name), lower(nameContains ?? '')) - | assignTo: filters }} - -{{#each filters}}{{/each}} -
    {{ filter | filterLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} -
    -
    -{{ "live-template" | partial({ rows, className, template:`{{ 'TemplateServiceStackFilters' | assignTo: filter }} -{{ filter | filtersAvailable | where => contains(lower(it.Name), lower(nameContains ?? '')) - | assignTo: filters }} - -{{#each filters}}{{/each}} -
    {{ filter | filterLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} -
    -
    - - -{{ "doc-links" | partial({ order }) }} \ No newline at end of file diff --git a/src/wwwroot/docs/filters.html b/src/wwwroot/docs/filters.html deleted file mode 100644 index e662601..0000000 --- a/src/wwwroot/docs/filters.html +++ /dev/null @@ -1,173 +0,0 @@ - - -

    - Templates are sandboxed, they can't call methods on objects nor do they have any access to any static functions built into the .NET Framework, - so just as Arguments define all data and objects available to templates, filters define all functionality available to templates. -

    - -

    - The only filters registered by default are the Default Filters - containing a comprehensive suite of filters useful within View Engine or Template environments and - HTML Filters. - There's nothing special about these filters other than they're pre-registered by default, your filters have access to the same - APIs and functionality and can do anything that built-in filters can do. -

    - -
    Shadowing Filters
    - -

    - You can easily override default filters with the same name and arguments by inserting them at the start of the TemplateFilters list: -

    - -{{ 'gfm/filters/01.md' | githubMarkdown }} - -
    Removing Default Filters
    - -

    - Or if you want to start from a clean slate, the default filters can be removed by clearing the collection: -

    - -{{ 'gfm/filters/02.md' | githubMarkdown }} - -

    What are Filters?

    - -

    - Filters are just C# public instance methods from a class that inherits from TemplateFilter, e.g: -

    - -{{ 'gfm/filters/03.md' | githubMarkdown }} - -

    Registering Filters

    - -

    - The examples below show the number of different ways filters can be registered: -

    - -
    Add them to the TemplateContext.TemplateFilters
    - -

    - Filters can be registered by adding them to the context.TemplateFilters collection directly: -

    - -{{ 'gfm/filters/04.md' | githubMarkdown }} - -

    That can now be called with:

    - -{{ 'gfm/filters/05.md' | githubMarkdown }} - -

    - This also shows that Filters are initialized and have access to the TemplateContext through the Context property. -

    - -
    Add them to PageResult.TemplateFilters
    - -

    - If you only want to use a custom filter in a single Template, it can be registered on the PageResult that renders it instead: -

    - -{{ 'gfm/filters/06.md' | githubMarkdown }} - -
    Autowired using TemplateContext IOC
    - -

    - Autowired instances of filters can also be created using TemplateContext's configured IOC where they're - also injected with any registered IOC dependencies. To utilize this you need to specify the Type of the filter that - should be Autowired by either adding it to the ScanTypes collection: -

    - -{{ 'gfm/filters/07.md' | githubMarkdown }} - -

    - When the TemplateContext is initialized it will go through each Type and create an autowired instance of each Type - and register them in the TemplateFilters collection. An alternative to registering a single Type is to register - an entire Assembly, e.g: -

    - -{{ 'gfm/filters/08.md' | githubMarkdown }} - -

    - Where it will search each Type in the Assembly for Template Filters and automatically register them. -

    - -
    Filter Resolution
    - -

    - Templates will use the first matching filter with the same name and argument count it can find by searching through all - filters registered in the TemplateFilters collection, so you could override default filters with the same name by - inserting your filters as the first item in the collection, e.g: -

    - -{{ 'gfm/filters/09.md' | githubMarkdown }} - -

    Auto coercion into Filter argument Types

    - -

    - A unique feature of Filters is that each of their arguments are automatically coerced into the filter argument Type using the - powerful conversion facilities built into ServiceStack's - Auto Mapping Utils and - Text Serializers which can deserialize most of .NET's primitive Types like - DateTime, TimeSpan, Enums, etc in/out of strings as well being able to convert a Collection into other Collection - Types and any Numeric Type into any other Numeric Type which is how, despite only accepting doubles: -

    - -{{ 'gfm/filters/10.md' | githubMarkdown }} - -

    - squared can also be used with any other .NET Numeric Type, e.g: byte, int, long, decimal, etc. - The consequence to this is that there's no method overloading in filters, filters are matched based on their name and their number of arguments - and each argument is automatically converted into its filter method Param Type before it's called. -

    - -

    Context Filters

    - -

    - Filters can also get access to the current scope by defining a TemplateScopeContext as it's first parameter which - can be used to access arguments in the current scope or add new ones as done by the assignTo filter: -

    - -{{ 'gfm/filters/11.md' | githubMarkdown }} - -

    Block Filters

    - -

    - Filters can also write directly into the OutputStream instead of being forced to return buffered output. A Block Filter - is declared by its Task return Type where instead of returning a value it instead writes directly to the - TemplateScopeContext OutputStream as seem with the implementation of the includeFile protected filter: -

    - -{{ 'gfm/filters/12.md' | githubMarkdown }} - -
    - For maximum performance all default filters which perform any I/O use Block filters to write directly to the OutputStream - and avoid any blocking I/O or buffering. -
    - -

    Block Filters ends the filter chain

    - -

    - Block filters effectively end the Filter chain expression since they don't return any value that can be injected into - a normal filter. The only thing that can come after a Block Filter are other Block Filters or Filter Transformers. - If any are defined, the output of the Block Filter is buffered into a MemoryStream and passed into - the next Block Filter or Filter Transformer in the chain, its output is then passed into the next one in the chain if any, - otherwise the last output is written to the OutputStream. -

    - -

    - An example of using a Block filter with a Filter Transformer is when you want include a markdown document and then - convert it to HTML using the markdown Filter Transformer before writing its HTML output to the OutputStream: -

    - -
    {{ pass: 'doc.md' | includeFile | markdown }}
    - -

    Capture Block Filter Output

    - -

    - You can also capture the output of a Block Filter and assign it to a normal argument by using the assignTo Block Filter: -

    - -
    {{ pass: 'doc.md' | includeFile | assignTo: contents }}
    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/hot-reloading.html b/src/wwwroot/docs/hot-reloading.html deleted file mode 100644 index ba7ec1d..0000000 --- a/src/wwwroot/docs/hot-reloading.html +++ /dev/null @@ -1,58 +0,0 @@ - - -

    - Templates has a simple, but effective configuration-free hot-reloading feature built-in that's enabled - when registering the TemplatePagesFeature plugin: -

    - -{{ 'gfm/hot-reloading/01.md' | githubMarkdown }} - -

    - It can then be enabled in your website by adding the expression below at the top of your _layout.html: -

    - -{{ 'gfm/hot-reloading/02.md' | githubMarkdown }} - -

    - This will embed the dependency-free hot-loader.js - script during development to poll the service endpoint below: -

    - -
    /templates/hotreload/page?path=path/to/page&eTag=lastETagReceived
    - -

    - Which responds with whether or not the client should reload their current page, preserving their current scroll offset. -

    - -
    - An easy way to temporarily disable hot reloading is to surround the expression with the noop block - {{#raw}}{{#noop}}{{#if debug}} ...{{/noop}}{{/raw}} or if preferred you can just use comments - {{#raw}}{{* #if debug ... *}}{{/raw}} -
    - -

    When to reload

    - -

    - Hot Reloading only monitors Template Pages. You'll need to do a hard refresh with Ctrl+Shift+F5 if making changes to - .css or .js to force the browser to not use its cache. For App Code or Configuration changes you'll - need to restart your App to pick up the changes. -

    - -

    Implementation

    - -

    - The Service is just a wrapper around the ITemplatePages.GetLastModified(page) API which returns the - last modified date of the page and any of its dependencies by scanning each expression in the page's AST to - find each referenced partial or included file to find the most recent modified date which it compares against - the eTag provided by the client to determine whether or not any of the pages resources have changed. -

    - -

    - Since it's just scanning the AST instead of evaluating it, it will only be able to find files and partials that were - statically referenced, i.e. the typical case of using a string literal for the file name as opposed to a dynamically creating it. -

    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/html-filters.html b/src/wwwroot/docs/html-filters.html deleted file mode 100644 index b50565c..0000000 --- a/src/wwwroot/docs/html-filters.html +++ /dev/null @@ -1,95 +0,0 @@ - - -

    - HTML-specific filters for use in .html page formats. -

    - -

    htmlDump

    - -

    - Generates a HTML <table/> by recursively traversing the public members of a .NET instance object graph.
    - headerStyle can be any Text Style -

    - -{{ 'live-template' | partial({ template: `{{ [{FirstName: 'Kurt', Age: 27},{FirstName: 'Jimi', Age: 27}] | assignTo: rockstars }} -{{ rockstars | htmlDump }} -Uses defaults: {{ rockstars | htmlDump({ headerTag: 'th', headerStyle: 'splitCase' }) }}` }) }} - -
    htmlDump customizations
    - -{{ 'live-template' | partial({ rows:3, template: `{{ [{FirstName: 'Kurt', Age: 27},{FirstName: 'Jimi', Age: 27}] | assignTo: rockstars }} -{{ rockstars | htmlDump({ className: "table table-striped", caption: "Rockstars" }) }} -{{ [] | htmlDump({ captionIfEmpty: "No Rockstars"}) }}` }) }} - -

    htmlClass

    - -

    - Helper filter to simplify rendering a class="..." list on HTML elements, it accepts Dictionary of boolean flags, List of strings or string, e.g: -

    -{{ 'live-template' | partial({ rows:7, template: `{{ 1 | assignTo: index }} -Dictionary All: {{ {alt:isOdd(index), active:true} | htmlClass }} -Dictionary One: {{ {alt:isEven(index), active:true} | htmlClass }} -Dictionary None: {{ {alt:isEven(index), active:false} | htmlClass }} -List: All {{ ['nav', !disclaimerAccepted? 'blur':'', isEven(index)? 'alt':''] | htmlClass }} -String: {{ 'alt' | if(isOdd(index)) | htmlClass }} -htmlClassList: {{ {alt:isOdd(index), active:true} | htmlClassList }}` }) }} - -

    - Or use htmlClassList if you just want the list of classes. -

    - -

    htmlAttrs

    - -

    - Helper filter to simplify rendering HTML attributes on HTML elements, it accepts a Dictionary of Key/Value Pairs - which will be rendered as HTML attributes. Keys with a boolean value will only render the attribute name if it's true - and htmlAttrs also supports common JS keyword overrides for htmlFor and className, e.g: -

    -{{ 'live-template' | partial({ rows:4, template: `Normal Key/Value Pairs: {{ {'data-index':1,id:'nav',title:'menu'} | htmlAttrs }} -Boolean Values: {{ {selected:true, active:false} | htmlAttrs }} -Keyword Names: {{ {for:'txtName', class:'lnk'} | htmlAttrs }} -Special Cases: {{ {htmlFor:'txtName', className:'lnk'} | htmlAttrs }}` }) }} - -

    htmlError

    - -

    - Renders an Exception into HTML, in DebugMode this renders a - detailed view using the htmlErrorDebug filter otherwise it's rendered using htmlErrorMessage. -

    - -{{ 'live-template' | partial({ rows:3, template: `{{ 'the error' | throw({ assignError: 'ex' }) }} -{{ ex | htmlError }} -{{ ex | htmlError({ className: "alert alert-warning" }) }}` }) }} - -

    htmlErrorMessage

    - -

    - Renders the Exception Message into into HTML using the default class in Args[DefaultErrorClassName], - overridable with className: -

    - -{{ 'live-template' | partial({ rows:3, template: `{{ 'the error' | throw({ assignError: 'ex' }) }} -{{ ex | htmlErrorMessage }} -{{ ex | htmlErrorMessage({ className: "alert alert-warning" }) }}` }) }} - -

    htmlErrorDebug

    - -

    - Renders a debug view of the Exception into into HTML using the default class in Args[DefaultErrorClassName], - overridable with className: -

    - -{{ 'live-template' | partial({ rows:3, template: `{{ 'the error' | throw({ assignError: 'ex' }) }} -{{ ex | htmlErrorDebug }} -{{ ex | htmlErrorDebug({ className: "alert alert-warning" }) }}` }) }} - -

    - See the Filters API Reference for the - full list of HTML filters available. -

    - - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/installation.html b/src/wwwroot/docs/installation.html deleted file mode 100644 index 0b29a00..0000000 --- a/src/wwwroot/docs/installation.html +++ /dev/null @@ -1,137 +0,0 @@ - - -

    - ServiceStack Templates is available in the ServiceStack.Common NuGet Package: -

    - -
    PM> Install-Package ServiceStack.Common
    - -

    - Or if using .NET Core: -

    - -
    PM> Install-Package ServiceStack.Common.Core
    - -

    - You're now all set to play with ServiceStack Templates! Start by creating and rendering a dynamic page: -

    - -{{ 'gfm/installation/01.md' | githubMarkdown }} - -{{ "live-template" | partial({ template: "The time is now: {{ now | dateFormat('HH:mm:ss') }}" }) }} - -

    - Evaluating ad hoc templates can also be condensed down to a single line: -

    - -{{ 'gfm/installation/02.md' | githubMarkdown }} - -

    Configure with ServiceStack

    - -

    - To use Template Pages as a HTML View Engine in ServiceStack, - register the TemplatePagesFeature plugin: -

    - -{{ 'gfm/installation/03.md' | githubMarkdown }} - -

    Starter Project Templates

    - -

    - The Starter Projects below provide a quick way to get started with a pre-configured ServiceStack Template Web App. -

    - -

    .NET Core 2.1 Boostrap Template

    - -

    - Create a new Templates Website .NET Core 2.1 App with - dotnet-new: -

    - -
    -
    -    $ npm install -g @servicestack/cli
    -
    -    $ dotnet-new templates ProjectName
    -
    - - - .NET Core Starter Template - -

    ASP.NET v4.5 Boostrap Starter

    - -

    - For ASP.NET v4.5 projects create a new ServiceStack ASP.NET Templates with Bootstrap from the VS.NET Templates in - ServiceStackVS VS.NET Extension - to create an ASP.NET v4.5 Project using - ServiceStack's recommended project structure: -

    - -

    - - ASP.NET v4.5 Starter Template - -

    - -

    WebApp Project Templates

    - -

    - Web Apps is our revolutionary new approach to dramatically simplify .NET Wep App development by using ServiceStack - Templates to build entire Websites in a live real-time development workflow without any C# and requiring no development environment, - IDE’s or build tools - dramatically reducing the cognitive overhead and conceptual knowledge required for developing .NET Core Websites in - a powerful dynamic templating language that's simple, safe and intuitive enough that Web Designers and Content Authors can use. -

    - -

    Bare WebApp

    - -

    - To start with a simple and mininal website, create a new bare-webapp project template: -

    - - - -
    $ dotnet-new bare-webapp ProjectName
    - -

    - This creates a multi-page Bootstrap Website with Menu navigation that's ideal for content-heavy Websites. -

    - -

    Parcel WebApp

    - -

    - For more sophisticated JavaScript Web Apps we recommended starting with the - parcel-webapp project template: -

    - - - -
    $ dotnet-new parcel-webapp ProjectName
    - -

    - This provides a simple and powerful starting template for developing modern JavaScript .NET Core Web Apps utilizing the - zero-configuration Parcel bundler to enable a rapid development workflow with access to - best-of-class web technologies like TypeScript that's managed by pre-configured - npm scripts to handle its entire dev workflow. -

    - -

    WebApp Examples

    - -

    - View our Example Web Apps to explore different features and see examples of how easy it is to create with Web Apps: -

    - -
      -
    • About Bare Web App - Simple Content Website with Menu navigation
    • -
    • About Parcel WebApp - Simple Integrated Parcel SPA Website utilizing TypeScript
    • -
    • Redis HTML - A Redis Admin Viewer developed as server-generated HTML Website
    • -
    • Redis Vue - A Redis Admin Viewer developed as Vue Client Single Page App
    • -
    • Rockwind - Combination of mult-layout Rockstars website + data-driven Nortwhind Browser
    • -
    • Plugins - Extend WebApps with Plugins, Filters, ServiceStack Services and other C# extensions
    • -
    • Chat - Highly extensible App with custom AppHost that leverages OAuth + Server Events for real-time Chat
    • -
    • Blog - A minimal, multi-user Twitter OAuth blogging platform that can create living, powerful pages
    • -
    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/introduction.html b/src/wwwroot/docs/introduction.html deleted file mode 100644 index 000a43b..0000000 --- a/src/wwwroot/docs/introduction.html +++ /dev/null @@ -1,318 +0,0 @@ - - -

    - ServiceStack Templates is a simple and elegant, highly-extensible, sandboxed, - high-performance general-purpose templating engine for .NET 4.5 and .NET Core. - It's designed from the ground-up to be incrementally adoptable where its basic usage is simple enough - for non-technical users to use whilst it progressively enables access to more power and functionality allowing it to scale up to - support full server-rendering Web Server workloads and beyond. Its high-fidelity with JavaScript syntax allows it to use a common - language for seamlessly integrating with client-side JavaScript Single Page App frameworks where its syntax is designed to be - compatible with Vue filters. -

    - -

    Instant Startup

    - -

    - There's no pre-compilation, pre-loading or Startup penalty, all Pages are lazily loaded on first use and cached for fast subsequent - evaluation. Its instant Startup, fast runtime performance and sandboxed isolation opens it up to a myriad of new use-cases which - can enhance .NET Apps with a rich Live programming experience. -

    - -

    Fast Runtime Performance

    - -

    - ServiceStack Templates is fast, parsing is done using StringSegment for minimal GC pressure, all I/O is non-blocking inc. async - writes to OutputStream's. There's no buffering: Layouts, Pages and Partials are asynchronously written to a forward only stream. - There's no runtime reflection, each filter or binding within template expressions executes compiled and cached C# Expressions. -

    - -

    Pure, Functional and Reactive

    - -

    - Templates are pure at both the library-level where they're a clean library with no external dependencies outside - ServiceStack.Common, no coupling to web frameworks, external configuration files, - designer tooling, build tools, pre-compilation steps or require any special deployment requirements. - It binds to simple, clean, small interfaces for its - Virtual File System, - IOC and - AppSettings providers which are easily overridden to - integrate cleanly into external web frameworks. -

    - -

    - They're also pure at the code-level where it doesn't have any coupling to concrete dependencies, components or static classes. Default filters - don't mutate any external state. There's no imperative statements, everything is an expression forcing a more readable and declarative - programming-style that's easier to quickly determine the subject of expressions and the states that need to be met for filters to be executed. - Conceptually Templates are "evaluated" in that they take in arguments, filters and templates as inputs and evaluates them to an output stream. - They're highly testable by design where the same environment used to create the context can easily be re-created in Unit tests, - including simulating pages in a File System using its In Memory Virtual File System. -

    - -

    Optimal Development Experience

    - -

    - The above attributes enables an instant iterative development workflow with a Live Development experience that supports - configuration-free Hot Reloading out of the box that lets you build entire - Wep Apps and Web APIs without ever having to compile or manually Refresh pages. -

    - -

    Simplicity end-to-end

    - -

    - There are 2 main concepts in Template Expressions: Arguments - variables which can be made available - through a number of cascading sources and Filters - public C# methods that exist in the list of - TemplateFilters registered in the PageResult or TemplateContext that templates are executed within. -

    - -

    - Layouts, Pages and Partials are all just "pages", evaluated in the same way with access to arguments and filters. Parameters passed - to partials are just scoped arguments, accessed like any other arguments. Typically pages are sourced from the configured - File System but when access to more advanced functionality is required they can also be Code Pages implemented - in pure C# that are otherwise interchangeable and can be used anywhere a normal page is requested. -

    - -

    - There's no language constructs or reserved words in Templates itself, all functionality is implemented inside filters. - There's also nothing special about the Default Filters included with Templates - other than they're pre-registered by default. External filters can do anything built-in filters can do which can just - as easily be shadowed or removed giving you a clean slate where you're free to define your own - language and preferred language naming scheme. -

    - -

    - They're non-invasive, by default ServiceStack's View Engine is configured to handle - .html files but can be configured to handle any html extension or text file format. - When a page doesn't contain any Template Expressions it's contents are returned as-is, making it easy to adopt in existing - static websites. -

    - -

    - Templates are sandboxed, they can't call static or instance methods, invoke setters or access anything - outside of the arguments and filters made available to them. Without filters, expressions wouldn't have any methods - they can call, leaving them with the only thing they can do which is access arguments and replace their variable placeholders, - including the {{ pass: page }} placeholder to tell the Layout where to render the page: -

    - -

    Quick Example Walkthrough

    - -{{ "live-pages" | partial( - { - page: 'page', - files: - { - '_layout.html': 'I am the Layout: {{ page }}', - 'page.html' : 'I am the Page' - } - }) -}} - -

    - To render the pages we first create and initialize a TemplateContext -

    - -{{ 'gfm/introduction/01.md' | githubMarkdown }} - -

    - The TemplateContext is the sandbox where all templates are executed within, everything your templates have access to and generates - is maintained within the TemplateContext. Once initialize you can start using it to evaluate templates which you can do with just: -

    - -{{ 'gfm/introduction/02.md' | githubMarkdown }} - -

    - Templates only have access to filters and arguments defined within its Context, which for an empty Context are the comprehensive - suite of safe Default Filters and HTML Filters. -

    - -

    - Typically you'll want to use ServiceStack templates to render entire pages which are sourced from its configured - Virtual File System which uses an In Memory Virtual - File System by default that we can programmatically populate: -

    - -{{ 'gfm/introduction/03.md' | githubMarkdown }} - -

    - Templates are rendered using a PageResult essentially a rendering context that needs to be provided the Page to render: -

    - -{{ 'gfm/introduction/04.md' | githubMarkdown }} - -

    - The template output can then be asynchronously rendered to any Stream: -

    - -{{ 'gfm/introduction/05.md' | githubMarkdown }} - -

    - Or to access the output as a string you can use the convenience extension method: -

    - -{{ 'gfm/introduction/06.md' | githubMarkdown }} - -

    - All I/O within Templates is non-blocking, but if you're evaluating an adhoc template or using the default In Memory Virtual FileSystem - there's no I/O so you can safely block to get the generated output with: -

    - -{{ 'gfm/introduction/07.md' | githubMarkdown }} - -

    - Both APIs returns the result you see in the Live Example above. -

    - -

    Cascading Resolution

    - -

    - There's no forced special centralized folders like /Views or /Views/Shared required to store layouts or share partials or - artificial "Areas" concept to isolate website sections. Different websites or sections are intuitively grouped into different - folders and Templates automatically resolves the closest layout it finds for each page. Cascading resolution also applies to - including files or partial pages where you can use just its name to resolve the closest one, or an absolute path from the - WebRootPath to include a specific partial or file from a different folder. -

    - -

    High-level, Declarative and Intent-based

    - -

    - High-level APIs are usually synonymous with being slow by virtue of paying a penalty for their high-level abstraction, - but in the domain of I/O and Streams such-as rendering text to Streams they make it trivial to compose high-level functionality - that's implemented more efficiently than would be typically written in C# / ASP.NET MVC Apps. -

    - -

    - As an example let's analyze the Template below: -

    - -{{ 'gfm/introduction/08.md' | githubMarkdown }} - -

    - The intent of Template code should be clear even if it's the first time reading it. From left-to-right we can deduce that it - retrieves a url from the quote table, downloads its contents of and converts it to markdown before replacing the text "Razor" and - "2010" and displaying the raw non-HTML encoded html output. -

    - -
    Implementation using ASP.NET MVC
    -

    - In MVC the typical and easiest approach would be to create a an MVC Controller Action, use EF to make a sync call to access the database, - a sync call with a new HTTP Client to download the content which is buffered inside the Controller action before returned inside a View Model - that is handed off to MVC to execute the View inside the default Layout. -

    - -
    Implementation using Templates
    -

    - What does Templates do? lets step through the first filter: -

    - -{{ "live-template" | partial({ output:'no-scroll', rows:1, template: "{{ 'select url from quote where id= @id' | dbScalar({ id:1 }) | htmlLink }}" }) }} - -

    - What filter implementation gets called depends on which DB Filter is registered, if your RDBMS supports ADO.NET's - async API you can register - TemplateDbFiltersAsync - to execute all queries asynchronously, otherwise if using an RDBMS whose ADO.NET Provider doesn't support async you can register the - TemplateDbFilters - to execute each DB request synchronously without paying for any pseudo-async overhead, in each case the exact same code executes - the most optimal ADO.NET APIs. Template also benefits from using the much faster - OrmLite and also saves on the abstraction cost from generating a - parameterized SQL Statement from a Typed Expression-based API. -

    - -
    Arguments chain
    -

    - The next question becomes what is id bound to? Similar to JavaScript's prototype chain it resolves the closest - argument in its Arguments hierarchy, e.g. when evaluated as a view page it could be set - by arguments in [paged based routes](/docs/view-engine#page-based-routing) or when the same page is evaluated as a - partial it could be set from the scoped arguments it was called with. -

    - -
    Async I/O
    -

    - The url returned from the db is then passed to the urlContents filter which if it was the last filter in the expression - avoids any buffering by asynchronously streaming the url content stream directly to the forward-only HTTP Response Stream: -

    - -
    {{ pass: url | urlContents }}
    - -

    - urlContents is a Block Filter which instead of returning a value writes its - response to the OutputStream it's given. But then how could we convert it to Markdown if it's already written to the Response Stream? - Templates analyzes the Expression's AST to determine if there's any filters remaining, if there is it gives the urlContents - Block filter a MemoryStream to write to, then forwards its buffered output to the next filter. Since they don't return values, - the only thing that can come after a Block Filter are other Block filters or Stream Transformers. - markdown is one such Filter Transformer which takes in a stream of Markdown - and returns a Stream of HTML. -

    - -
    {{ pass: url | urlContents | markdown }}
    - -
    Same intent, different implementations
    -

    - The assignTo filter is used to set a value in the current scope. After Block Filters, a different assignTo Block Filter - is used with the same name and purpose but a different implementation that reads all the contents of the stream into a UTF-8 string - and sets a value in the current scope before returning an empty Stream so nothing is written to the Response. -

    - -
    {{ pass: url | urlContents | markdown | assignTo: quote }}
    - -

    - Once the streamed output is captured and assigned it goes back into becoming a normal argument that opens it up to be - able to use all filters again, which is how we're able to use the string replace filters before rendering the final result :) -

    - -

    /examples/qotd?id=1

    - -
    Using the most efficient implementation allowable
    -

    - So whilst it conceptually looks like each filter is transforming large buffered strings inside every filter, the expression is - inspected to utilize the most efficient implementation allowable. At the same time filters are not statically bound to - any implementation so you could for instance insert a Custom Filter before the Default - Filters containing the same name and arguments count to have templates execute your custom filters instead, all whilst the template's - source code and intent remains untouched. -

    - -
    Intent-based code is easier to augment
    -

    - If it was later discovered that some URLs were slow or rate-limited and you needed to introduce caching, your original C# code - would require a fair amount of rework, in templates you can simply add WithCache to call the - urlContentsWithCache filter to return locally cached contents - on subsequent requests. -

    - -
    {{ pass: url | urlContentsWithCache | markdown }}
    - -

    Simplified Language

    - -

    - As there's very little ceremony in Templates, a chain of filters looks like it's using its own DSL to accomplish each task and - given implementing and registering custom filters is trivial you're - encouraged to write the intent of your code first then implement any filters that are missing to realize its intent. Once - you've captured the intent of what you want to do, it's less likely to ever need to change, focus is instead on - fixing any bugs and making the filter implementations as efficient as possible, which benefits all existing code using the same filter. -

    - -

    - To improve readability and make it more approachable, templates aims to normalize the mechanics of the underlying implementation from - the code's intent so you can use the same syntax to access an argument, e.g. {{ pass: arg }} as you would a filter without - arguments {{ pass: now }} and just like JavaScript you can use obj.Property syntax to access both a public property - on a Type or an entry in a Dictionary. -

    - -

    Late-bound flexibility

    - -

    - There's no static coupling to concrete classes, static methods or other filters, ambiguous method exceptions or namespace collisions. - Each filter is self-contained and can easily be shared and dropped into any Web App by - registering them in a list. Inspired by the power and - flexibility in Smalltalk and LISP, filters are late-bound at run-time to the first matching filter - in the user-defined list of TemplateFilters. This ability to shadow filters enables high-level intent-based APIs decoupled from implementations which - sendToAutoQuery leverages to automatically route filter invocations to - the appropriate implementation depending of if it's an AutoQuery RDBMS or an - AutoQuery Data request, masking their implementations as a transparent detail. - This flexibility also makes it easy create proxies, intercept and inject behavior like logging or profiling without modifying existing - filter implementations or template source code. -

    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/model-view-controller.html b/src/wwwroot/docs/model-view-controller.html deleted file mode 100644 index 6f0b87e..0000000 --- a/src/wwwroot/docs/model-view-controller.html +++ /dev/null @@ -1,62 +0,0 @@ - - -

    - Simplicity is a driving goal behind the design of Templates where in its simplest form it's usable by non-programmers who just - know HTML as they're able to embed dynamic content in their HTML pages using intuitive Mustache syntax and with the - intuitive way in how View Engine works they're able to develop entire content-heavy websites - without needing to write any code. -

    - -

    - As requirements become more demanding you can use progressively advanced features to enable greater flexibility and - functionality whilst still retaining using Templates to generate the HTML view with access to existing layouts and partials. -

    - -

    - The first option to enable functionality is to reuse the rich functionality available in Services to populate the data - required by your view. To do this in ServiceStack add a reference to the ITemplatePages dependency which was registered - by TemplateContext, then return a PageResult containing the .html page you want to - render as well as any additional arguments you want available in the page: -

    - -

    CustomerServices.cs

    - -{{ 'gfm/model-view-controller/01.md' | githubMarkdown }} - -

    - The above can Service can also be made slightly shorter by using the Request.GetPage() extension method, e.g: -

    - -{{ 'gfm/model-view-controller/02.md' | githubMarkdown }} - -

    Model PageResult Property

    - -

    - The Model property is a special argument that automatically registers all its public properties as arguments - as well as registering itself in the model argument, this lets views reference model properties directly like - {{ pass: CustomerId }} instead of the more verbose {{ pass: model.CustomerId }} as used in: -

    - -

    examples/customer.html

    - -{{ 'gfm/model-view-controller/03.md' | githubMarkdown }} - -

    - Now that we have the Service handling the Request and populating the Model for our page we can use it to view each Customer - in a nice detail page: -

    - -
    -
    - -
    -
    -
    -
    -
    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/mvc-netcore.html b/src/wwwroot/docs/mvc-netcore.html deleted file mode 100644 index c720f04..0000000 --- a/src/wwwroot/docs/mvc-netcore.html +++ /dev/null @@ -1,105 +0,0 @@ - - -

    - The easiest way to enable Templates support in a .NET Core App is to register an empty ServiceStack AppHost - with the TemplatePagesFeature plugin enabled: -

    - -{{ 'gfm/mvc-netcore/01.md' | githubMarkdown }} - -

    - This let you use the View Engine and Code Pages features - to build your entire .NET Core Web App with Templates without needing to use anything else in ServiceStack. -

    - -

    MVC PageResult

    - -

    - Just as you can use a ServiceStack Service as the Controller for a Template Page View, you can also use an MVC Controller - which works the same way as a Service, but instead of returning the PageResult directly, you need to call - ToMvcResult() extension method to wrap it in an MVC ActionResult, e.g: -

    - -{{ 'gfm/mvc-netcore/02.md' | githubMarkdown }} - -

    - This has the same effect as it has in a ServiceStack Service where the PageResult sets the Content-Type - HTTP Response Header and asynchronously writes the template to the Response OutputStream for maximum performance. -

    - -

    Sharing Dependencies

    - -

    - Unfortunately you'd need to resolve ITemplatePages directly from ServiceStack's IOC as dependencies registered in - ServiceStack's IOC aren't visible in .NET Core MVC but dependencies registered in .NET Core MVC are visible in ServiceStack - so to have the same dependencies available to both .NET Core MVC and ServiceStack you would instead register it in .NET Core's - IOC then resolve the same instance in your ServiceStack AppHost, e.g: -

    - -{{ 'gfm/mvc-netcore/03.md' | githubMarkdown }} - -

    - This will then let you access ITemplatePages as a normal dependency in your MVC Controller: -

    - -{{ 'gfm/mvc-netcore/04.md' | githubMarkdown }} - -

    Using Templates without a ServiceStack AppHost

    - -

    - If you don't need the View Engine support and would like to use Templates without a - ServiceStack AppHost you can, just register a TemplateContext instance in .NET Core's IOC, replace - its Virtual File System to point to the WebRootPath and you can start returning PageResult's as above: -

    - -{{ 'gfm/mvc-netcore/05.md' | githubMarkdown }} - -

    - Checkout the NetCoreApps/MvcTemplates repository for a - stand-alone example of this, complete with a - sidebar.html partial - and a CustomTemplateFilters.cs. -

    - - - - - -

     

    - -

    ASP.NET v4.5 Framework MVC

    - -

    - You can return a Template Page in a classic ASP.NET MVC similar to ASP.NET Core MVC Controller except that in order to - work around the lack of being able to async in classic ASP.NET MVC Action Results you need to return a task, but because we can - replace the IOC Controller Factory in ASP.NET MVC - you can use Constructor Injection to access the ITemplatePages dependency: -

    - -{{ 'gfm/mvc-aspnet/01.md' | githubMarkdown }} - -

    ASP.NET MVC + Templates Demo

    - -

    - Checkout the ServiceStackApps/MvcTemplates repository for a - stand-alone example, complete with a - sidebar.html partial - and a CustomTemplateFilters.cs. -

    - - - - - -
    - This demo was created from the - ServiceStack ASP.NET MVC5 Empty VS.NET Template. -
    - - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/page-formats.html b/src/wwwroot/docs/page-formats.html deleted file mode 100644 index 7da9bf7..0000000 --- a/src/wwwroot/docs/page-formats.html +++ /dev/null @@ -1,74 +0,0 @@ - - -

    - Templates is a general purpose text templating language which doesn't have any notion of HTML or any other format embedded in the language itself. - It simply emits text outside of mustaches verbatim and inside mustaches evaluates the expression and emits the result. - All custom behavior pertinent to specific text formats are instead extrapolated in its Page Format. -

    - -

    - Templates supports rendering multiple different text formats simultaneously within the same TemplateContext, - but the only Page Format pre-registered by default is the HtmlPageFormat which is defined below: -

    - -

    HTML Page Format

    - -{{ 'gfm/page-formats/01.md' | githubMarkdown }} - -

    - The HtmlPageFormat is used to specify: -

    - -
      -
    • That the Page Arguments should be defined within HTML comments at the top of the page
    • -
    • That files with the .html extension should use this format
    • -
    • That it should return the text/html Content-Type in HTTP Responses
    • -
    • That the result of all Template Expressions should be HTML-encoded by default
    • -
    • That it should not use a default layout for complete HTML files that start with a HTML or HTML5 tag
    • -
    • How Expression Exceptions should be rendered in HTML if RenderExpressionExceptions is enabled
    • -
    - -

    Results of all Template Expressions are HTML-encoded by default

    - -

    - The EncodeValue in the HtmlPageFormat automatically encodes the results of all template expressions so they're - safe to embed in HTML, e.g: -

    - -{{ 'live-template' | partial({ rows: 1, template: "{{ 'hi' }}" }) }} - -

    - You can use the raw default filter to skip HTML-encoding which will let you emit raw HTML as-is: -

    - -{{ 'live-template' | partial({ rows: 1, template: "{{ 'hi' | raw }}" }) }} - -
    - Inside filters you can return strings wrapped in a new RawString(string) or use the ToRawString() extension - method to skip HTML-encoding. -
    - -

    Markdown Page Format

    - -

    - By contrast here is what the MarkdownPageFormat looks like which is able to use most of the default implementations: -

    - -{{ 'gfm/page-formats/02.md' | githubMarkdown }} - -

    Registering a new PageFormat

    - -

    - To register a new Page Format you just need to add it to the TemplateContext's PageFormat collection: -

    - -{{ 'gfm/page-formats/03.md' | githubMarkdown }} - -

    - Which now lets you resolve pages with a .md file extension who will use the behavior defined in MarkdownPageFormat. -

    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/partials.html b/src/wwwroot/docs/partials.html deleted file mode 100644 index 062a4b0..0000000 --- a/src/wwwroot/docs/partials.html +++ /dev/null @@ -1,113 +0,0 @@ - - -

    - Partials are just normal pages which contain reusable templates you'd like to embed in different pages. - There is no difference between pages and partials other than how they're embedded where pages are embedded - in a _layout using the {{ pass: page }} expression and partials are embedded using the - partial block filter which can also define scoped arguments on the call-site using an Object - literal: -

    - -{{ "live-pages" | partial( - { - page: 'page', - files: - { - '_layout.html': `{{ 'from layout' | assignTo: layoutArg }} -I am a Layout with page -{{ page }}`, - 'page.html' : `I am a Page with a partial -{{ 'my-partial' | partial({ arg: "from page" }) }}`, - 'my-partial.html' : `I am a partial called with the scoped argument {{ arg }} -Who can also access other arguments in scope {{ layoutArg }}` - } - }) -}} - -

    Select Partial

    - -

    - Another way that partials can be embedded is using the selectPartial block filter which will re-evaluate - the same partial for each item in the collection which is made available in the it binding, e.g: -

    - -
    -
    -
    -
    customer.html
    - -
    -
    -
    order.html
    - -
    -
    -
    - -
    -
    -
    -
    -
    - -

    Inline Partials

    - -

    - Partials can also be defined in-line with the {{pass: #partial}} block statement: -

    - -{{ 'live-template' | partial({ rows:13, template: ` -{{#partial order}} - Order {{ it.OrderId }}: {{ it.OrderDate | dateFormat }} -{{/partial}} -{{#partial customer}} -Customer {{ it.CustomerId }} {{ it.CompanyName | raw }} -{{ it.Orders | selectPartial: order }}{{/partial}} - -{{ customers - | where => it.Region = 'WA' - | assignTo: waCustomers - }} - Customers from Washington and their orders: - {{ waCustomers | selectPartial: customer }}` }) }} - -
    - The linq 04 - example below shows how to implement this without partials using {{pass: #each}} statement blocks: -
    - -{{ 'live-template' | partial({ rows:7, template: `Customers from Washington and their orders: -{{#each c in customers where c.Region == 'WA'}} -Customer {{ c.CustomerId }} {{ c.CompanyName | raw }} -{{#each c.Orders}} - Order {{ OrderId }}: {{ OrderDate | dateFormat }} -{{/each}} -{{/each}}` }) }} - - -

    Resolving Partials and Pages

    - -

    - Just like pages, partials are resolved from the TemplateContext's configured VirtualFiles sources - where partials in the root directory can be resolved without any specifying any folders: -

    - -
    {{ pass: 'my-partial' | partial }}
    - -
    Cascading resolution
    - -

    - If an exact match isn't found it will look for the closest page with that name it can find, starting from the directory - where the page that contains the partial is located and traversing up until it reaches the root folder. -

    - -

    - Otherwise you can specify the virtual path to the partial from the Virtual Files root, e.g: -

    - -
    {{ pass: 'dir/subdir/my-partial' | partial }}
    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/protected-filters.html b/src/wwwroot/docs/protected-filters.html deleted file mode 100644 index 9d219e0..0000000 --- a/src/wwwroot/docs/protected-filters.html +++ /dev/null @@ -1,261 +0,0 @@ - - -

    - One of the goals of Templates is that its defaults should be safe enough to be able to execute arbitrary templates by untrusted 3rd parties. - Given this constraint, only default filters are pre-registered which contain a comprehensive set of filters we deem safe for use by anyone. - Other filters available in Templates which are useful to have in a server-generated website environment but we don't want 3rd Parties to - access are filters in - TemplateProtectedFilters.cs. -

    - -

    - TemplateProtectedFilters are not pre-registered when creating a new TemplateContext but they are pre-registered when - registering the TemplatePagesFeature ServiceStack Plugin as that's designed to use Templates as a View Engine where access - to its context is limited to the server's Web Application. -

    - -

    - To access TemplateProtectedFilters features within your own TemplateContext it can be - registered like any other filter: -

    - -{{ 'gfm/protected-filters/01.md' | githubMarkdown }} - -

    includeFile

    -
    fileContents
    - -

    - Use includeFile to embed content directly within templates: -

    - -{{ "live-pages" | partial( - { - page: 'page', - files: - { - 'page.html' : `I am a Page with a file -{{ 'my-file.txt' | includeFile }}`, - 'my-file.txt' : `I am a text file` - } - }) -}} - -
    Cascading resolution
    - -

    - If an exact match isn't found it will look for the closest file with that name it can find, starting from the directory - where the containing page that uses the filter is located and traversing up until it reaches the root folder. -

    - -

    includeFileWithCache

    -
    fileContentsWithCache
    - -

    - If your VirtualFiles is configured to use a combination - of various sources that includes a remote file service like - S3VirtualFiles, you'll - likely want to cache the contents in memory to ensure fast subsequent access the next time the file is requested, - which you can cache without expiration using includeFileWithCache without arguments: -

    - -
    {{ pass: 'my-file.txt' | includeFileWithCache }}
    - -

    - In which case it will use the 1 minute default overridable in Args[DefaultFileCacheExpiry] - or if you want the content to be refreshed after 1hr you can use: -

    - -
    {{ pass(`'my-file.txt' | includeFileWithCache({ expiresInSecs: 3600 })`) }}
    - -

    includeUrl

    -
    urlContents
    - -

    - You can also embed the contents of remote URLs in your page using includeUrl: -

    - -{{ "live-pages" | partial( - { - page: 'page', - files: - { - 'page.html' : '{{ "https://raw.githubusercontent.com/NetCoreApps/TemplatePages/master/src" | assignTo: src }} -{{ `${src}/wwwroot/code/linq01.txt` | includeUrl }}' - } - }) -}} - -

    - includeUrl is actually a very flexible HTTP Client which can leverage the - URL Handling filters to easily construct urls and the additional - filter arguments to customize the HTTP Request that's sent, here are some examples: -

    - -

    - Accept JSON responses: -

    - -{{#raw}} -
    {{ url | includeUrl({ accept: 'application/json' }) }}
    -
    {{ url | includeUrl({ dataType: 'json' }) }}
    - -

    - Send data as form-urlencoded in a HTTP PUT Request with a Templates User-Agent: -

    - -
    {{ url | includeUrl({ method:'PUT', data: { id: 1, name: 'foo' }, userAgent:"Templates" }) }}
    - -

    - Send data as JSON in a HTTP POST request and Accept JSON response: -

    - -
    {{ url | includeUrl({ method: 'POST', data: { id: 1, name: 'foo' }, 
    -                      accept: 'application/json', contentType: 'application/json' }) }}
    - -

    - Shorter version of above request: -

    - -
    {{ url | includeUrl({ method:'POST', data: { id: 1, name: 'foo' }, dataType: 'json' }) }}
    - -

    - Send data as CSV in a HTTP POST Request and Accept a CSV response: -

    - -
    includeUrl({ method:'POST', data: [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }], dataType:'csv' })
    - - -

    includeUrlWithCache

    -
    urlContentsWithCache
    - -

    - In the same way includeFileWithCache can cache file contents, includeUrlWithCache can cache URL content, - either without an expiration: -

    - - -
    {{ url | includeUrlWithCache }}
    - -

    - In which case it will use the 1 minute default overridable in Args[DefaultUrlCacheExpiry] - or if you want to ensure that no more than 1 url request is made per hour for this url you can specify a custom expiry with: -

    - -
    {{ url | includeUrlWithCache({ expiresInSecs: 3600 }) }}
    - -

    Virtual File System APIs

    - -

    - The Virtual File System APIs are mapped to the following filters: -

    -{{/raw}} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Filter Name - Virtual File System API -
    File APIs
    filesFindGetAllMatchingFiles(globPatern)
    fileExistsFileExists(virtualPath)
    fileGetFile(virtualPath)
    fileWriteWriteFile(virtualPath, contents)
    fileAppendAppendFile(virtualPath, contents)
    fileDeleteDeleteFile(virtualPath)
    fileReadAllGetFile(virtualPath).ReadAllText()
    fileReadAllBytesGetFile(virtualPath).ReadAllBytes()
    fileHashGetFileHash(virtualPath)
    Directory APIs
    dirGetDirectory(virtualPath)
    dirExistsDirectoryExists(virtualPath)
    dirFileGetDirectory(dirPath).GetFile(fileName)
    dirFiles.GetDirectory(dirPath).GetFiles()
    dirDirectoryGetDirectory(dirPath).GetDirectory(dirName)
    dirDirectoriesGetDirectory(dirPath).GetDirectories()
    dirFilesFindGetDirectory(dirPath).GetAllMatchingFiles(globPatern)
    Virtual File System APIs
    vfsAllFilesGetAllFiles()
    vfsAllRootFilesGetRootFiles()
    vfsAllRootDirectoriesGetRootDirectories()
    vfsCombinePathCombineVirtualPath(basePath, relativePath)
    - -

    - See the Filters API Reference for the - full list of Protected filters available. -

    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/redis-filters.html b/src/wwwroot/docs/redis-filters.html deleted file mode 100644 index fbd61be..0000000 --- a/src/wwwroot/docs/redis-filters.html +++ /dev/null @@ -1,154 +0,0 @@ - - -

    - The Redis Filters provide connectivity with a Redis Server instance using the - ServiceStack.Redis library. To enable, install - the ServiceStack.Redis NuGet Package, then - register a Redis Client Manager - in the IOC: -

    - -{{ 'gfm/redis-filters/01.md' | githubMarkdown }} - -

    - Then to enable in templates register - TemplateRedisFilters: -

    - -{{ 'gfm/redis-filters/02.md' | githubMarkdown }} - -

    - Your templates are now able to use the available Redis Filters. -

    - -

    redisCall

    - -

    - This is your main interface into Redis which utilizes the - Custom Commands API - to be able to execute any arbitrary Redis command. It can either be called with a string similar to commands sent - with redis-cli, e.g: -

    - -
    {{ pass: 'GET foo' | redisCall }}
    - -

    - This works for most Redis commands but uses internal heuristics to determine where to split the command into the multiple arguments that - redis-server natively understands. A more deterministic usage which doesn't rely on any heuristics is to pass an array of arguments: -

    - -
    {{ pass: ['GET', 'foo'] | redisCall }}
    - -

    - redisCall returns either a single object for redis commands returning single values or a List or nested List of - objects for Redis commands returning complex Responses. -

    - -

    redisInfo

    - -

    - Returns the Redis Server Info from Redis Info command in a String Dictionary. -

    - -

    redisSearchKeys

    - -

    - Provides an efficient API for searching Redis Keys by utilizing - Redis's non-blocking SCAN command, to fetch results and a server-side LUA script - to populate the results with metadata. Results are returned in a typed List<RedisSearchResult>: -

    - -{{ 'gfm/redis-filters/03.md' | githubMarkdown }} - -

    redisSearchKeysAsJson

    - -

    - If preferred, you can access the search results as JSON and parse it on the client instead. -

    - -

    redisConnection

    - -

    - Returns the current Connection Info in an Object Dictionary containing: {{ pass: host, port, db }} -

    - -

    redisToConnectionString

    - -

    - Converts an redisConnection Object Dictionary into a - Redis Connection String. -

    - -

    Redis App Examples

    - -

    - The Web Apps below utilize Redis Filters for all their Redis functionality: -

    - -

    Redis Vue

    -

    - redis.web-app.io is a generic Redis Database Viewer that provides a human-friendly view - of any Redis data Type, including a dedicated UI to create and populate any Redis data type which just utilizes the above Redis Filters - and a single Vue index.html App to power the Redis UI: -

    - -

    - .NET Core Redis Viewer -

    - -

    Redis HTML

    - -

    - redis-html.web-app.io is a version of Redis UI built using just Templates and Redis - filters where all functionality is maintained in a single index.html - weighing under <200 LOC including HTML and JavaScript. It's a good example of how the declarative style of programming - that ServiceStack Templates encourages a highly-readable code-base that packs a lot of functionality in a tiny foot print. -

    - -
    Server Generated HTML
    - -

    - It's not immediatly obvious when running this locally since both Templates and Redis are super fast, but Redis HTML was developed as - a traditional Website where all HTML is server-generated and every search box key stroke and click on search results performs - a full-page reload. There's a slight sub-second delay that causes a noticable flicker when hosted on the Internet due to network lag, but - otherwise server-generated Template Websites can enable a highly responsive UI (especially in Intranets) with great SEO and deep-linking - and back-button support working as expected without the complexity of adopting a client-side JavaScript SPA framework and build-system. -

    - -

    Run against your local Redis instance

    - -

    - You can run the above Redis WebApp's against your local redis instance by cloning the - NetCoreApps/WebApp GitHub project and in the - /src/apps folder run: -

    - -
    dotnet web/app.dll ../redis/app.settings
    - -

    - As this is a Web Template Web App it doesn't need any compilation so after - running the Redis Web App - you can modify the source code and see changes in real-time thanks to its built-in Hot Reloading support. -

    - -
    Populate redis with the Northwind database
    - -

    - You can use the - northwind-data - project to populate a Redis instance with Northwind test data by running: -

    - -
    dotnet run redis localhost:6379
    - -

    - See the Filters API Reference for the - full list of Redis filters available. -

    - - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/sandbox.html b/src/wwwroot/docs/sandbox.html deleted file mode 100644 index b443074..0000000 --- a/src/wwwroot/docs/sandbox.html +++ /dev/null @@ -1,60 +0,0 @@ - - -

    - Another useful feature of Templates is that it operates within a controlled sandbox where each TemplateContext instance is - isolated and defines the entire execution environment on which Templates are executed within as such it should be safe to run - Templates from untrusted 3rd Party sources as they're confined to what's available within their allowed TemplateContext instance. -

    - -

    TemplateContext

    - -

    - The only functionality a new TemplateContext instance has access to are the - safe set of default filters and the htmlencode Filter Transformer. - Templates can't call methods on instances or have any other way to invoke a method unless it's explicitly registered. -

    - -

    - If running a template from an untrusted source we recommend running them within a new TemplateContext instance so they're - kept isolated from any other TemplateContext instance. Context's are cheap to create, so there won't be a noticeable delay when - executing in a new instance but they're used to cache compiled lambda expressions which will need to be recreated if executing Templates - in new TemplateContext instances. For improved performance you can instead have all untrusted templates use the same - TemplateContext instance that way they're able to reuse compiled expressions. -

    - -

    Remove default filters

    - -

    - If you want to start from a clean slate, the default filters can be removed by clearing the TemplateFilters collection: -

    - -{{ 'gfm/sandbox/01.md' | githubMarkdown }} - -

    Disabling adhoc Filters

    - -

    - Or if you only want to disable access to some filters without removing them all, you can disable access to adhoc filters - by adding to the ExcludeFiltersNamed collection: -

    - -{{ 'gfm/sandbox/02.md' | githubMarkdown }} - -
    - Filters can also be disabled on an individual PageResult by populating its ExcludeFiltersNamed collection. -
    - -

    Instance creation and MaxQuota

    - -

    - The only instances that can be created within templates are what's allowed in - JavaScript Literals and the - Generation and Repeating Filters. To limit any potential CPU and GC abuse any default filters - that can generate instances are limited to a MaxQuota of 10000 iterations. This quota can be modified with: -

    - -{{ 'gfm/sandbox/03.md' | githubMarkdown }} - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/servicestack-filters.html b/src/wwwroot/docs/servicestack-filters.html deleted file mode 100644 index d28c419..0000000 --- a/src/wwwroot/docs/servicestack-filters.html +++ /dev/null @@ -1,365 +0,0 @@ - - -

    - The ServiceStack Filters provide integration with ServiceStack features that are already pre-registered in ServiceStack's - TemplatePagesFeature which are implemented in - TemplateServiceStackFilters. -

    - -
    - See Info Filters for accessing the currently authenticated user. -
    - - - -

    sendToGateway

    - -

    - sendToGateway lets you easily call any ServiceStack Service available through its - Service Gateway which allows the same API to transparently - call an In-Process or Remote Service. -

    - -

    - The example below calls this QueryCustomers - AutoQuery RDBMS Service, its entire implementation is below: -

    - -{{ 'gfm/servicestack-filters/01.md' | githubMarkdown }} - -{{ 'examples/sendtogateway-customers.html' | includeFile | assignTo: template }} -{{ "live-template" | partial({ rows:11, template }) }} - -

    publishToGateway

    - -

    - publishToGateway is for sending OneWay requests with side-effects to IReturnVoid Services, e.g: -

    - -{{ 'gfm/servicestack-filters/02.md' | githubMarkdown }} - -

    sendToAutoQuery

    - -

    - The sendToAutoQuery filter makes requests directly against the AutoQuery API. - The TemplateServiceStackFilters only supports calling - AutoQuery Data Services as it's implementation is contained - within the ServiceStack NuGet package. -

    - -

    - AutoQuery Data - is an open querying abstraction that supports multiple pluggable back-ends that enables rich querying of - In Memory collections, - results from executed ServiceStack Services as well as - AWS Dynamo DB data stores. It also maintains the equivalent - external API and wire-format as AutoQuery RDBMS Services - which is how AutoQuery Viewer is able to transparently support - building custom queries for any AutoQuery Service. -

    - -

    GitHub AutoQuery Data Example

    - -

    - For this example we'll register a ServiceSource which will call the - GetGithubRepos Service implementation - for any AutoQuery Data DTOs that query GithubRepo data sources: -

    - -{{ 'gfm/servicestack-filters/03.md' | githubMarkdown }} - -

    - This registration also specifies to cache the response of the GetGithubRepos Service in the registered - Caching Provider and operate on the cached data set for up to - 10 minutes to mitigate GitHub API's rate-limiting. All that's remaining is to create the QueryGitHubRepos - Service by defining the Request DTO below and implement the backing - GetGithubRepos Service - it calls which combines a number of GitHub API calls to fetch all Repo's for a GitHub User or Organization: -

    - -{{ 'gfm/servicestack-filters/04.md' | githubMarkdown }} - -

    - That's all that's required to be able to query GitHub's User and Organization APIs, since they're just normal ServiceStack Services - we could've used sendToAutoQuery to call QueryGitHubRepos but it would be limited to only being able to call properties - explicitly defined on the Request DTO, whereas sendToAutoQuery executes against the IAutoQueryData API which - enables access to all Implicit Conventions - and other Querying related functionality: -

    - -{{ 'examples/sendToAutoQuery-data.html' | includeFile | assignTo: template }} -{{ "live-template" | partial({ rows:6, template }) }} - -

    AutoQuery RDBMS

    - -

    - AutoQuery RDBMS is implemented in the - ServiceStack.Server NuGet package - which you'll need to install along with the - OrmLite NuGet package for your RDBMS - which can then be registered in the IOC with: -

    - -{{ 'gfm/servicestack-filters/05.md' | githubMarkdown }} - -

    - AutoQuery can then be enabled by registering the AutoQueryFeature plugin: -

    - -{{ 'gfm/servicestack-filters/06.md' | githubMarkdown }} - -

    - Which will let you start developing AutoQuery Services. - To then let your Templates to call AutoQuery Services directly, register the TemplateAutoQueryFilters: -

    - -{{ 'gfm/servicestack-filters/07.md' | githubMarkdown }} - -

    sendToAutoQuery

    - -

    - As they share the same semantics and wire-format, you can use the same sendToAutoQuery filter name to call - either AutoQuery Data or AutoQuery RDBMS Services which automatically gets routed to the appropriate implementation. - This also means that you can replace your implementation to from AutoQuery Data to RDBMS and vice-versa behind the - scenes and your templates will continue to work untouched. -

    - -

    - For this example we'll re-use the same QueryCustomers AutoQuery Implementation that the - sendToGateway uses: -

    - -{{ 'gfm/servicestack-filters/08.md' | githubMarkdown }} - -

    - But instead of being limited by explicit properties on the Request DTO - sendToAutoQuery extends the queryability of AutoQuery Services to enable querying all - implicit conventions as well: -

    - -{{ 'examples/sendToAutoQuery-rdms.html' | includeFile | assignTo: template }} -{{ "live-template" | partial({ rows:6, template }) }} - -

    - See the Filters API Reference for the - full list of ServiceStack filters available. -

    - -

    Info Filters

    - - -

    - The debug info filters provide an easy to inspect the state of a remote ServiceStack Instance by making a number of metadata - objects and APIs available to query. The - TemplateInfoFilters - are pre-registered in ServiceStack's TemplatePagesFeature. You can make them available in a TemplateContext with: -

    - -{{ 'gfm/info-filters/01.md' | githubMarkdown }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Filter NameAPI Mapping
    Environment APIs
    envVariableEnvironment.GetEnvironmentVariable(variable)
    envExpandVariablesEnvironment.ExpandEnvironmentVariables(name)
    envProcessorCountEnvironment.ProcessorCount
    envTickCountEnvironment.TickCount
    envServerUserAgentEnv.ServerUserAgent
    envServiceStackVersionEnv.ServiceStackVersion
    envIsWindowsEnv.IsWindows (net45) / IsOSPlatform(Windows) (netcore)
    envIsLinuxEnv.IsLinux (net45) / IsOSPlatform(Linux) (netcore)
    envIsOSXEnv.IsOSX (net45) / IsOSPlatform(OSX) (netcore)
    networkIpv4AddressesGetAllNetworkInterfaceIpv4Addresses()
    networkIpv6AddressesGetAllNetworkInterfaceIpv6Addresses()
    ServiceStack User APIs
    userSessionIRequest.GetSession()
    userSessionIdIRequest.GetSessionId()
    userSessionIRequest.GetSession()
    userPermanentSessionIdIRequest.GetPermanentSessionId()
    userSessionOptionsIRequest.GetSessionOptions()
    userHasRoleIAuthSession.HasRole(role)
    userHasPermissionIAuthSession.HasPermission(permission)
    ServiceStack Metadata APIs
    metaAllDtosMetadata.GetAllDtos()
    metaAllDtoNamesMetadata.GetOperationDtos(x => x.Name)
    metaAllOperationsMetadata.Operations
    metaAllOperationNamesMetadata.GetAllOperationNames()
    metaAllOperationTypesMetadata.GetAllOperationTypes()
    metaOperationMetadata.GetOperation(name)
    - -
    - -

    /metadata/debug

    -

    Debug Template

    - - - Metadata Debug Templates Screenshot - - -

    - The Debug Template is a Service in TemplatePagesFeature that's pre-registered in DebugMode. - The Service can also be available when not in DebugMode by enabling it with: -

    - -{{ 'gfm/info-filters/02.md' | githubMarkdown }} - -

    - This registers the Service but limits it to Users with the Admin role, alternatively you configure an - Admin Secret Key: -

    - -{{ 'gfm/info-filters/03.md' | githubMarkdown }} - -

    - Which will let you access it by appending the authsecret to the querystring: - /metadata/debug?authsecret=secret -

    - -

    - Alternatively if preferred you can make the Debug Template Service available to all users with: -

    - -{{ 'gfm/info-filters/04.md' | githubMarkdown }} - -

    - Which is the configuration used in this .NET Core App which makes the Debug Template Service accessible to everyone: -

    - -

    /metadata/debug

    - -

    - Debug Templates are executed in a new TemplateContext instance pre-configured with the above TemplateInfoFilters - and a copy of any context arguments in TemplatePagesFeature which is the only thing it can access from the plugin. -

    - -

    - In addition to TemplatePagesFeature arguments, they're also populated with the following additional arguments: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    requestThe current IHttpRequest
    appHostThe ServiceStack AppHost Instance
    appConfigServiceStack's AppHost Configuration
    appVirtualFilesPathThe Read/Write IVirtualFiles (aka ContentRootPath)
    appVirtualFileSourcesPathMultiple read-only Virtual File Sources (aka WebRootPath)
    metaServiceStack Metadata Services Object
    - -

    - See the Filters API Reference for the - full list of Info filters available. -

    - - - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/syntax.html b/src/wwwroot/docs/syntax.html deleted file mode 100644 index c3f015d..0000000 --- a/src/wwwroot/docs/syntax.html +++ /dev/null @@ -1,189 +0,0 @@ - - -

    - The syntax is inspired by and it most cases compatible with Vue.js filters, - with the goal being you can use the same common language to implement server rendering with ServiceStack Templates as you would do - in client-side rendering of Single Page Apps using Vue filters. With this in mind, the syntax within mustaches is compatible with JavaScript - where you can use native JS data structures despite it creating C# objects and calling C# methods behind-the-scenes. -

    - -

    Mustache expressions

    - -

    - Just like Vue filters, only expressions inside mustaches are evaluated, everything outside mustaches are emitted as-is: -

    - -{{ 'live-template' | partial({ template: "outside {{ 'shout' | upper }} text" }) }} - -

    - Which calls the upper default filter function where the argument on the left-side of the "pipe" symbol is - passed as the first argument to the filter which is implemented as: -

    - -{{ 'gfm/syntax/01.md' | githubMarkdown }} - -

    - This can also be rewritten without the "pipe" symbol by calling the filter with an argument instead: -

    - -{{ 'live-template' | partial({ template: "outside {{ upper('shout') }} text" }) }} - -

    Filters can be chained

    - -

    - Filters are chained from left-to-right where the value on the left side of the "pipe" symbol is passed as the first - argument in the filter on the right and the output of that is passed as the input of the next filter in the chain and so on: -

    - -{{ 'live-template' | partial({ template: "{{ 'shout' | upper | substring(2) | padRight(6, '_') | repeat(3) }}" }) }} - -

    - Filters can also accept additional arguments which are passed starting from the 2nd argument since the first - argument is the value the filter is called with. E.g. here are the implementations for the substring and - padRight default filters: -

    - -{{ 'gfm/syntax/02.md' | githubMarkdown }} - -

    JavaScript literal notation

    - -

    - You can use the same literal syntax used to define numbers, strings, booleans, null, Objects and Arrays in JavaScript - within templates and it will get converted into the most appropriate .NET Type, e.g: -

    - -{{ 'live-template' | partial({ rows: 8, template: "{{ null | typeName }} -{{ true | typeName }} -{{ 1 | typeName }} -{{ 1.1 | typeName }} -{{ 'string' | typeName }} -{{ ['array', 'items'] | typeName }} -{{ { key: 'value' } | typeName }} -" }) }} - -

    - ES6 Shorthand notation is also supported where you can use the argument name as its property name in a Dictionary: -

    - -{{ 'live-template' | partial({ template: "{{ 'foo' | assignTo: bar }} -{{ { bar } | assignTo: obj }} -{{ obj['bar'] }}" }) }} - -

    Quotes

    - -

    - As strings are prevalent in Templates, you can define them using single quotes, double quotes, prime quotes or backticks: -

    - -{{ 'live-template' | partial({ rows:4, template: "{{ \"double quotes\" }} -{{ 'single quotes' }} -{{ ′prime quotes′ }} -{{ `backticks` }}" }) }} - -
    - Strings can also span multiple lines. -
    - -

    Template Literals

    - -Backticks strings implement JavaScript's Template literals -which can be be used to embed expressions: - -{{ 'live-template' | partial({ rows:4, template: "{{ `Time is ${now}, expr= ${true ? pow(1+2,3) : ''}` }} -Prime Quotes {{ now.TimeOfDay | timeFormat(′h\\:mm\\:ss′) }} -Template Literal {{ now.TimeOfDay | timeFormat(`h\\\\:mm\\\\:ss`) }}" }) }} - -The example above also shows the difference in escaping where Template literals evaluate escaped characters whilst normal strings -leave \ backspaces unescaped. - -

    Shorthand arrow expression syntax

    - -

    - Filters have full support for JavaScript expressions but doesn't support statements or function declarations although it - does support JavaScript's arrow function expressions which can be used in functional filters to enable LINQ-like queries. - You can use fat arrows => immediately after filters to define lambda's with an implicit (it => ...) binding, e.g: -

    - -{{ 'live-template' | partial({ template: ′{{ [0,1,2,3,4,5] - | where => it >= 3 - | map => it + 10 | join(`\n`) }}′ }) }} - -

    - This is a shorthand for declaring lambda expressions with normal arrow expression syntax: -

    - -{{ 'live-template' | partial({ template: ′{{ [0,1,2,3,4,5] - | where(it => it >= 3) - | map(x => x + 10) | join(`\n`) }}′ }) }} - -

    - Using normal lambda expression syntax lets you rename lambda parameters as seen in the map(x => ...) example. -

    - -

    Special string argument syntax

    - -

    - As string expressions are a prevalent in Templates, we've also given them special wrist-friendly syntax where you - can add a colon at the end of the filter name which says to treat the following characters up until the end of the - line or mustache expression as a string, trim it and convert '{' and '}' chars into mustaches. With this syntax - you can write: -

    - -{{ 'live-template' | partial({ template: "{{ [3,4,5] | select: { it | incrBy(10) }\\n }}" }) }} - -

    - and it will be rewritten into its equivalent and more verbose form of: -

    - -{{ 'live-template' | partial({ template: "{{ [3,4,5] | select(′{{ it | incrBy(10) }}\\n′) }}" -}) }} - -

    SQL-like Boolean Expressions

    - -

    - To maximize readability and intuitiveness for non-programmers, boolean expressions can also adopt an SQL-like syntax where - instead of using &&, || or == operator syntax to define boolean expressions you can also - use the more human-friendly and, or and = alternatives: -

    - -{{ 'live-template' | partial({ template: "{{ [0,1,2,3,4,5] - | where => (it = 2 or it = 3) and isOdd(it) - | join }}" }) }} - -

    Include Raw Content Verbatim

    - -Use #raw blocks to ignore evaluating expressions and emit content verbatim. This is useful when using a -client Handlebars-like templating solution like Vue or Angular templates where expressions need to be evaluated -with JavaScript in the browser instead of on the Server with Templates: - -{{ 'live-template' | partial({ rows: 4, template: "{{#raw}}Hi {{ person.name }}, Welcome to {{ site.name }}!{{/raw}} - -{{#raw template}}Assign contents with {{ expressions }} into 'template' argument{{/raw}} -Captured Argument: {{template}}" - }) }} - -

    Multi-line Comments

    - -

    - Any text within {{#raw}}{{#noop}} ... {{/noop}}{{/raw}} block statements are ignored and can be used for temporarily removing - sections from pages without needing to delete it. -

    - -

    - Everything within multi-line comments {{#raw}}{{‌* and *‌}}{{/raw}} is ignored and removed from the page. -

    - -

    - An alternative way to temporarily disable an expression is to prefix the expression with the end filter to - immediately short-circuit evaluation, e.g: {{#raw}}{{ end | now | dateFormat }}{{/raw}} -

    - -

    - See Ignoring Pages for different options for ignoring entire pages and - templates. -

    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/transformers.html b/src/wwwroot/docs/transformers.html deleted file mode 100644 index 3e86740..0000000 --- a/src/wwwroot/docs/transformers.html +++ /dev/null @@ -1,70 +0,0 @@ - - -

    - You can apply a chain of multiple Stream transformations to Template output using Transformers which are just functions that - accept an Input Stream and return a modified Output Stream. The MarkdownPageFormat's TransformToHtml shows an example - of a Transformer which converts Markdown Input and returns a Stream of HTML output: -

    - -{{ 'gfm/transformers/01.md' | githubMarkdown }} - -

    Output Transformers

    - -

    - You can transform the entire output of a Template with Output Transformers which you would do if both your _layout - and page both contain markdown, e.g: -

    - -{{ 'gfm/transformers/02.md' | githubMarkdown }} - -
    PageResult with Output Transformer
    - -{{ 'gfm/transformers/03.md' | githubMarkdown }} - -

    - After the Template is evaluated it's entire output gets passed into the chain of OutputTransformers defined, which in this case will - send a MemoryStream of the generated Markdown Output into the MarkdownPageFormat.TransformToHtml transformer which returns - a Stream of converted HTML which is what's written to the OutputStream. -

    - -

    Page Transformers

    - -

    - You can also apply Transformations to only the Page's output using Page Transformers which you would do if only the page was in - Markdown and the _layout was already in HTML, e.g: -

    - -{{ 'gfm/transformers/04.md' | githubMarkdown }} - -
    PageResult with Page Transformer
    - -{{ 'gfm/transformers/05.md' | githubMarkdown }} - -

    Filter Transformers

    - -

    - Filter Transformers are used to apply Stream Transformations to Block Filters which you - could use if you only wanted to convert an embedded Markdown file inside a Page to HTML. You can register Filter Transformers - in either the TemplateContext's or PageResult's FilterTransformers Dictionary by assigning it the name you want it available in - your Templates under, e.g: -

    - -{{ 'gfm/transformers/06.md' | githubMarkdown }} - -
    PageResult with Filter Transformer
    - -{{ 'gfm/transformers/07.md' | githubMarkdown }} - -

    htmlencode

    - -

    - The htmlencode Filter Transformer is pre-registered in TemplateContext which lets you encode Block Filter outputs - which is useful when you want to HTML Encode a text file before embedding it in the page, e.g: -

    - -
    {{ pass: "page.txt" | includeFile | htmlencode }}
    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/view-engine.html b/src/wwwroot/docs/view-engine.html deleted file mode 100644 index 6e320f5..0000000 --- a/src/wwwroot/docs/view-engine.html +++ /dev/null @@ -1,414 +0,0 @@ - - -

    - One of the most popular use-cases for a high-performance Templating engine like ServiceStack Templates is as a server-side - HTML View Engine for .NET Web Applications where it can provide a simpler, cleaner and portable alternative than Razor and - Razor Pages in ASP.NET and ASP.NET Core Web Apps. -

    - -

    View Engine in ServiceStack

    - -

    - The TemplatePagesFeature plugin provides a first-class experience for generating dynamic websites where it's able to - generate complete server-generated websites (like this one) without requiring any additional Controllers or Services. -

    - -

    - To enable Template Pages as a View Engine in ServiceStack you just need to register the TemplatePagesFeature plugin: -

    - -{{ 'gfm/view-engine/01.md' | githubMarkdown }} - -

    - TemplatePagesFeature is a subclass of TemplateContext which defines the context on which all ServiceStack - Template Pages are executed within. It provides deep integration within ServiceStack by replacing the TemplateContext's stand-alone - dependencies with ServiceStack AppHost providers, where it: -

    - -
      -
    • Configures it to use ServiceStack's Virtual File Sources - allowing Pages to be loaded from any configured VFS Source
    • -
    • Configures it to use ServiceStack's Funq IOC Container - so all ServiceStack dependencies are available to Code Pages
    • -
    • Configures it to use ServiceStack's AppSettings - so all AppHost AppSettings are available to Template Pages as well
    • -
    • Configures ScanAssemblies to use AppHost Service Assemblies so it auto-registers all Filters in Service .dlls
    • -
    • Registers the TemplateProtectedFilters allowing Templates to access richer server-side functionality
    • -
    • Registers the markdown Filter Transformer using ServiceStack's built-in MarkdownDeep implementation
    • -
    • Makes the ServiceStackCodePage subclass available so Code Pages has access to same functionality as Services
    • -
    • Registers a Request Handler which enables all requests .html pages to be handled by Template Pages
    • -
    - -

    If preferred, you can change which .html extension gets handled by Template Pages with:

    - -{{ 'gfm/view-engine/02.md' | githubMarkdown }} - -

    Runs Everywhere

    - -

    - The beauty of ServiceStack Templates working natively with ServiceStack is that it runs everywhere ServiceStack does - which is in all major .NET Server Platforms. That is, your same Templates-based Web Application is able to use - the same Templates implementation, "flavour" and feature-set and is portable across whichever platform you choose to host it on: -

    - - - -

    - Once registered, TemplatePagesFeature gives all your .html pages Template super powers where sections can be - compartmentalized and any duplicated content can now be extracted into reusable partials, metadata can be added to the top of - each page and its page navigation dynamically generated, contents of files and urls can be embedded directly and otherwise - static pages can come alive with access to Default Filters. -

    - -

    Page Based Routing

    - -

    - Any .html page available from your AppHost's configured Virtual File Sources - can be called directly, typically this would mean the File System which in a .NET Core Web App starts from the WebRootPath - (usually /wwwroot) so a request to /docs/view-engine goes through all configured VirtualFileSources to find the first - match, which for this website is the file - /src/wwwroot/docs/view-engine.html. -

    - -

    Pretty URLs by default

    - -

    - Essentially Template Pages embraces conventional page-based routing which enables pretty urls inferred from the pages file and directory names - where each page can be requested with or without its .html extension: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    pathpage
    /db 
    /db.html/db.html
    /posts/new 
    /posts/new.html/posts/new.html
    - -

    - The default route / maps to the index.html in the directory if it exists, e.g: -

    - - - - - - - - - - - - - - - - - - -
    pathpage
    //index.html
    /index.html/index.html
    - -

    Dynamic Page Routes

    - -

    - In addition to these static conventions, Template Pages now supports Nuxt-like - Dynamic Routes - where any file or directory names prefixed with an _underscore enables a wildcard path which assigns the matching path component - to the arguments name: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    pathpagearguments
    /ServiceStack/_user/index.htmluser=ServiceStack
    /posts/markdown-example/posts/_slug/index.htmlslug=markdown-example
    /posts/markdown-example/edit/posts/_slug/edit.htmlslug=markdown-example
    - -

    Layout and partial recommended naming conventions

    - -

    - The _underscore prefix for declaring wildcard pages is also what is used to declare "hidden" pages, to distinguish them from hidden - partials and layouts, the recommendation is for them to include layout and partial their name, e,g: -

    - -
      -
    • _layout.html
    • -
    • _alt-layout.html
    • -
    • _menu-partial.html
    • -
    - -
    - Pages with layout or partial in their name remain hidden and are ignored in wildcard path resolution. -
    - -

    - If following the recommended _{name}-partial.html naming convention, you'll be able to reference them using just their name: -

    - -{{#raw}} -
    -{{ 'menu' | partial }}          // Equivalent to:
    -{{ '_menu-partial' | partial }}
    -
    -{{/raw}} - -

    View Pages

    - -

    - View Pages lets you use .html Template Pages to render the HTML for Services Responses. - It works similarly to Razor ViewPages - where it uses first matching View Page with the Response DTO is injected as the `Model` property. - The View Pages can be in any folder within the /Views folder using the format {PageName}.html where PageName - can be either the Request DTO or Response DTO Name, but all page names within the /Views folder need to be unique. -

    - -

    - Just like ServiceStack.Razor, you can specify to use different Views or Layouts by returning a custom HttpResult, e.g: -

    - -{{ 'gfm/view-engine/09.md' | githubMarkdown }} - -

    - Or add the [ClientCanSwapTemplates] Request Filter attribute to allow clients to specify which View and Template to use via the query - string, e.g: ?View=CustomPage&Template=_custom-layout -

    -

    - Additional examples of dynamically specifying the View and Template are available in - TemplateViewPagesTests. -

    - -

    Cascading Layouts

    - -

    - One difference from Razor is that it uses a cascading _layout.html instead of /Views/Shared/_Layout.cshtml. -

    - -

    So if your view page was in:

    - -
    -
    -/Views/dir/MyRequest.html
    -
    -
    - -

    It will use the closest `_layout.html` it can find starting from:

    - -
    -
    -/Views/dir/_layout.html
    -/Views/_layout.html
    -/_layout.html
    -
    -
    - -

    Layout Selection

    - -

    - Unless it's a complete HTML Page (e.g. starts with html or HTML5 tag) the page gets rendered using the closest _layout.html - page it can find starting from the directory where the page is located, traversing all the way up until it reaches the root directory. - Which for this page uses the - /src/wwwroot/_layout.html template - in the WebRoot directory, which as it's in the root directory, is the fallback Layout for all .html pages. -

    - -

    - Pages can change the layout they use by either adding their own _layout.html page in their sub directory or specifying - a different layout in their pages metadata header, e.g: -

    - -{{ 'gfm/view-engine/03.md' | githubMarkdown }} - -

    - Where it instead embed the page using the closest mobile-layout.html it can find, starting from the Page's directory. - If your templates are instead embedded in the different folder you can request it directly from the root dir: -

    - -{{ 'gfm/view-engine/04.md' | githubMarkdown }} - -

    Request Variables

    - -

    - The QueryString and FORM variables sent to the page are available as arguments within the page using the - form and query (or its shorter qs alias) filter collections, so a request like - /docs/view-engine?id=1 - can access the id param with {{ pass: qs.id }}. The combined {{pass: 'id' | formQuery }} - filter enables the popular use-case of checking for the param in POST FormData before falling back to - the QueryString. Use {{pass: 'ids' | formQueryValues }} for accessing multiple values sent by multiple checkboxes - or multiple select inputs. The {{pass: 'id' | httpParam }} filter searches all Request params including HTTP Headers, QueryString, - FormData, Cookies and Request.Items. -

    - -

    - To help with generating navigation, the following Request Variables are also available: -

    - -
      -
    • {{ pass: Verb }} evaluates to {{ Verb }}
    • -
    • {{ pass: AbsoluteUri }} evaluates to {{ AbsoluteUri }}
    • -
    • {{ pass: RawUrl }} evaluates to {{ RawUrl }}
    • -
    • {{ pass: PathInfo }} evaluates to {{ PathInfo }}
    • -
    - -

    - You can use {{ pass: PathInfo }} to easily highlight the active link in a links menu as done in - sidebar.html: -

    - -{{ 'gfm/view-engine/05.md' | githubMarkdown }} - -

    Init Pages

    - -

    - Just as how Global.asax.cs can be used to run Startup initialization logic in ASP.NET Web Applications and - Startup.cs in .NET Core Apps, you can now add a /_init.html page for Templates logic that's only executed once on Startup. -

    - -

    - This is used in the Blog Web App's _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: -

    - -{{ 'gfm/view-engine/10.md' | githubMarkdown }} - -

    Ignoring Pages

    - -

    - You can ignore ServiceStack Templates from evaluating static .html files with the following page arguments: -

    - -{{ 'gfm/view-engine/06.md' | githubMarkdown }} - -{{ 'gfm/view-engine/07.md' | githubMarkdown }} - -{{ 'gfm/view-engine/08.md' | githubMarkdown }} - -
    - Complete .html pages starting with <!DOCTYPE HTML> or <html have their layouts ignored by default. -
    - -

    Templates Admin Service

    - -

    - The new Templates Admin Service lets you run admin actions against a running instance which by default is only accessible to Admin - users and can be called with: -

    - -
    /templates/admin
    - -

    - Which will display the available actions which are currently only: -

    - -
      -
    • invalidateAllCaches - Invalidate all caches and force pages to check if they've been modified on next request
    • -
    • RunInitPage - Runs the Init page again
    • -
    - -

    Zero downtime deployments

    - -

    - These actions are useful after an xcopy/rsync deployment to enable zero downtime deployments by getting a running instance to invalidate all - internal caches and force existing pages to check if it has been modified, the next time their called. -

    - -

    - Actions can be invoked in the format with: -

    - -
    /templates/admin/{Actions}
    - -

    - Which can be used to call 1 or more actions: -

    - -
    /templates/admin/invalidateAllCaches
    -/templates/admin/invalidateAllCaches,RunInitPage
    - -

    - By default it's only available to be called by **Admin** Users (or AuthSecret) - but can be changed with: -

    - -{{ 'gfm/view-engine/11.md' | githubMarkdown }} - -

    ServiceStack Filters

    - -

    - Filters for integrating with ServiceStack are available in - ServiceStack Filters and Info Filters both - of which are pre-registered when registering the TemplatePagesFeature Plugin. -

    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/docs/web-apps.html b/src/wwwroot/docs/web-apps.html deleted file mode 100644 index b0fe3a5..0000000 --- a/src/wwwroot/docs/web-apps.html +++ /dev/null @@ -1,1106 +0,0 @@ - - -

    - Web Apps are a new approach to dramatically simplify .NET Wep App development - and provide the most productive development experience possible whilst maximizing reuse and component sharing. - They also open up a number of new use-cases for maintaining clean isolation between front-end and back-end development with - front-end developers not needing any knowledge of C#/.NET to be able to develop UIs for high-performance .NET Web Apps. - Web Apps also make it easy to establish and share an approved suite of functionality amongst multiple websites all - consuming the same back-end systems and data stores. -

    - -

    - Web Apps leverages Templates to develop entire content-rich, data-driven websites without needing to write any C#, - compile projects or manually refresh pages - resulting in the easiest and fastest way to develop Web Apps in .NET! -

    - -

    Ultimate Simplicity

    - -

    - Not having to write any C# code or perform any app builds dramatically reduces the cognitive overhead and conceptual knowledge - required for development where the only thing front-end Web developers need to know is Template's syntax - and what filters are available to call. - Because of Template's high-fidelity with JavaScript, developing a Website with Templates will be instantly familiar to JavaScript - devs despite calling and binding directly to .NET APIs behind the scenes. -

    - -

    - All complexity with C#, .NET, namespaces, references, .dlls, strong naming, packages, MVC, Razor, build tools, IDE environments, etc - has been eliminated leaving all Web Developers needing to do is run the cross-platform - web dotnet tool - and configure a simple - app.settings - text file to specify which website folder to use, which ServiceStack features to enable, which db or redis providers to connect to, etc. - Not needing to build also greatly simplifies deployments where multiple websites can be deployed with a - single rsync or xcopy command or - if deploying your App in a Docker Container, you just need to - copy your website files, or just the app.settings - if you're using an S3 or Azure Virtual File System. -

    - -

    Rapid Development Workflow

    - -

    - The iterative development experience is also unparalleled for a .NET App, no compilation is required so you can just leave - the web dotnet tool running whilst you add the template .html files needed to build your App and thanks - to the built-in Hot Reloading support, pages will refresh automatically as you save. - You'll just need to do a full page refresh when modifying external .css/.js files to bypass the browser cache and - you'll need to restart web to pick up any changes to your app.settings or added any - .dlls to your /plugins folder. -

    - -

    Getting Started

    - -

    - All Web Apps can be run either as .NET Core Web Apps by installing the web dotnet tool: -

    - -
    $ dotnet tool install -g web
    - -Then you can run web list to see which apps are available to install: - -
    $ web list
    - -

    - By default this will list all NetCoreWebApps available, ordered by popularity: -

    - -
    1. redis           Redis Admin Viewer developed as Vue Client Single Page App
    -2. bare            Bootstrap + jQuery multi-page Website with dynamic Menu Navigation + API pages
    -3. chat            Highly extensible App with custom AppHost leveraging OAuth + SSE for real-time Chat
    -4. plugins         Extend WebApps with Plugins, Filters, ServiceStack Services and other C# extensions
    -5. blog            Minimal multi-user Twitter OAuth blogging platform that creates living powerful pages
    -6. rockwind-aws    Rockwind Cloud Web App on AWS
    -7. rockwind-azure  Rockwind Cloud Web App on Azure
    -8. redis-html      Redis Admin Viewer developed as server-generated HTML Website
    -9. spirals         Explore and generate different Spirals with SVG
    -10. rockwind        Web App combining multi-layout Rockstars website + data-driven Northwind Browser
    -
    -Usage: web install <name>
    - -

    - Where any of the apps can be installed by specifying the name, e.g. - spirals can be installed with: -

    - -
    $ web install spirals
    - -

    - Then to run any app, change into its installed directory and run web without any arguments: -

    - -
    $ cd spirals && web
    - -

    - Each Web App can also be run as a - .NET Core Windows Desktop App - by installing the app dotnet tool: -

    - -
    $ dotnet tool install -g app
    - -

    - Then running the Web App with app instead of web. -

    - -
    $ app
    - -

    Spirals

    -
    - web install spirals - - spirals.web-app.io - - mythz/spirals -
    - -

    - Spirals is a good example showing how easy it is to create .NET Core Desktop Web Apps utilizing HTML5's familiar and simple development - model to leverage advanced Web Technologies like SVG in a fun, interactive and live development experience. -

    - - -

    - -

    - - -
    - -

    - -

    - See the Making of Spirals for a walk through on how to create - the Spirals Web App from scratch. -

    - -

    WebApp Project Templates

    - -

    - A more complete starting option is to start from one of the project templates below. All project templates can be installed using the - dotnet-new tool, which if not already can be installed with: -

    - -
    $ npm install -g @servicestack/cli
    - -

    Bare WebApp

    - -

    - To start with a simple and mininal website, create a new bare-webapp project template: -

    - - - -
    $ dotnet-new bare-webapp ProjectName
    - -

    - This creates a multi-page Bootstrap Website with Menu navigation that's ideal for content-heavy Websites. -

    - -

    Parcel WebApp

    - -

    - For more sophisticated JavaScript Web Apps we recommended starting with the - parcel-webapp project template: -

    - - - -
    $ dotnet-new parcel-webapp ProjectName
    - -

    - This provides a simple and powerful starting template for developing modern JavaScript .NET Core Web Apps utilizing the - zero-configuration Parcel bundler to enable a rapid development workflow with access to - best-of-class web technologies like TypeScript that's managed by pre-configured - npm scripts to handle its entire dev workflow. -

    - -

    - If needed, it includes an optional server - component containing any C# extensions needed that's automatically built and deployed to the app's /plugins folder when started. - This enables an extensibile .NET Core Web App with an integrated Parcel bundler to enable building sophisticated JavaScript - Apps out-of-the-box within a live development environment. -

    - -

    Cloud Apps Starter Projects

    - -

    - If you intend to deploy your Web App on AWS or Azure you may prefer to start with one of the example - Cloud Apps below which come pre-configured with deployment scripts for deploying with Travis CI and Docker: -

    - - - -

    About Bare WebApp

    - - -

    - The Getting Started project contains a copy of the bare-webapp.web-templates.io project below - which is representative of a typical Company splash Website: -

    - -Bare WebApp screenshot - -

    - The benefits over using a static website is improved maintenance - as you can extract and use its common _layout.html - instead of having it duplicated in each page. - The menu.html partial also makes menu items - easier to maintain by just adding an entry in the JavaScript object literal. The dynamic menu also takes care of highlighting the active menu item. -

    - -
    {{ 'examples/webapps-menu.html' | includeFile }}
    - -

    Ideal for Web Designers and Content Authors

    - -

    - The other primary benefit is that this is an example of a website that can be maintained by employees who don't have any - programming experience as Templates in their basic form are intuitive and approachable to non-developers, e.g: - The title of each page is maintained as metadata HTML comments: -

    - -
    <!--
    -title: About Us
    --->
    -
    - -

    - Template's syntax is also the ideal way to convey variable substitution, e.g: <title>{{ pass: title }}</title> - and even embedding a partial reads like english {{ pass: 'menu' | partial }} which is both intuitive and works well - with GUI HTML designers. -

    - -
    app.settings
    - -

    - Below is the app.settings for a Basic App, with contentRoot being the only setting required as the - rest can be inferred but including the other relevant settings is both more descriptive to other developers as well - making it easier to - use tools like sed or - powershell to replace them - during deployment. -

    - -
    debug true
    -name Bare Web App
    -contentRoot ~/../bare
    -webRoot ~/../bare
    - -
    - debug true controls the level of internal diagnostics available and whether or not Hot Reloading is enabled. -
    - -

    About Parcel WebApp

    - - -{{ 'gfm/web-apps/13.md' | githubMarkdown }} - -

    Example Web Apps

    - -

    - In addition to the templates there's a number of Web Apps to illustrate the various features available and to showcase the different - kind of Web Apps that can easily be developed. The source code for each app is available either individually from - github.com/NetCoreWebApps. -

    - -

    Redis HTML

    -
    - web install redis-html - - redis-html.web-app.io - - NetCoreWebApps/redis-html -
    - -

    - For the Redis Browser Web App, we wanted to implement an App that was an ideal candidate for a Single Page App but constrain ourselves - to do all HTML rendering on the server and have each interaction request a full-page reload to see how a traditional server-generated - Web App feels like with the performance of .NET Core 2.1 and Templates. We're pleasantly surprised with the result as when - the App is run locally the responsiveness is effectively indistinguishable from an Ajax App. When hosted on the Internet - there is a sub-second delay which causes a noticeable flicker but it still retains a pleasant UX that's faster than most websites. -

    - -

    - The benefits of a traditional website is that it doesn't break the web where the back button and deep linking work without effort - and you get to avoid the complexity train of adopting a premier JavaScript SPA Framework's configuration, dependencies, workflow - and build system which has become overkill for small projects. -

    - -Redis HTML WebApp Screenshot - -

    - We've had a sordid history developing Redis UI's which we're built using the popular JavaScript frameworks that appeared - dominant at the time but have since seen their ecosystem decline, starting with the - Redis Admin UI (src) - built using - Google's Closure Library that as it works different to everything else - needed a complete rewrite when creating redisreact.servicestack.net - (src) using the hot new React framework, unfortunately it uses React's old - deprecated ES5 syntax and Reflux which is sufficiently different from our current recommended - TypeScript + React + Redux + WebPack JavaScript SPA Stack, - that is going to require a significant refactor to adopt our preferred SPA tech stack. -

    - -
    Beautiful, succinct, declarative code
    - -

    - The nice thing about generating HTML is that it's the one true constant in Web development that will always be there. - The entire functionality for the Redis Web App is contained in a single - /redis-html/app/index.html which includes - all Template and JavaScript Source Code in < 200 lines which also includes all as server logic as it doesn't rely on any - back-end Services and just uses the Redis Filters to interface with Redis directly. - The source code also serves as a good - demonstration of the declarative coding style that Templates encourages that in addition to being highly-readable requires orders - of magnitude less code than our previous Redis JavaScript SPA's with a comparable feature-set. -

    - -

    - Having a much smaller code-base makes it much easier to maintain and enhance whilst being less susceptible to becoming obsolete - by the next new JavaScript framework as it would only require rewriting 75 lines of JavaScript instead of the complete rewrite - that would be required to convert the existing JavaScript Apps to a use different JavaScript fx. -

    - -
    app.settings
    - -

    - The app.settings for Redis is similar to Web App Starter above except it adds a redis.connection - to configure a RedisManagerPool at the - connection string provided - as well as Redis Filters to give Templates access to the Redis instance. -

    - -
    debug true
    -name Redis Web App
    -contentRoot ~/../redis-html
    -webRoot ~/../redis-html
    -redis.connection localhost:6379
    - -

    Redis Vue

    -
    - web install redis - - redis.web-app.io - - NetCoreWebApps/Redis -
    - -

    - Whilst the above server-generated HTML Redis UI shows how you can easily develop traditional Web Apps using Templates, we've also - rewritten the Redis UI as a Single Page App which is the more suitable choice for an App like this as it provides a more - optimal and responsive UX by only loading the HTML page once on Startup then utilizes Ajax to only download and update - the incremental parts of the App's UI that needs changing. -

    - -

    - Instead of using jQuery and server-side HTML this version has been rewritten to use Vue - where the UI has been extracted into isolated Vue components utilizing - Vue X-Templates to render the App on the client where - all Redis Vue's functionality is contained within the - Redis/app/index.html page. -

    - -Redis Vue WebApp Screenshot - -

    Simple Vue App

    - -

    - Templates also provides a great development experience for Single Page Apps which for the most part gets out of your way letting you - develop the Single Page App as if it were a static .html file, but also benefits from the flexibility of a - dynamic web page when needed. -

    -

    - The containing _layout.html - page can be separated from the index.html page - that contains the App's functionality, where it's able to extract the title of the page and embed it in the - HTML <head/> as well as embed the page's <script /> in its optimal location at the bottom of the - HTML <body/>, after the page's blocking script dependencies: -

    - -{{ 'gfm/web-apps/07.md' | githubMarkdown }} - -

    - Redis Vue avoids the complexity of adopting a npm build system by referencing Vue libraries as a simple script include: -

    - -
    <script src="../assets/js/vue{{ pass: '.min' | if(!debug) }}.js">
    - -

    - Where it uses the more verbose and developer-friendly - vue.js - during development whilst using the production optimized - vue.min.js - for deployments. So despite avoiding the complexity tax of an npm-based build system it still gets some of its benefits - like conditional deployments and effortless hot reloading. -

    - -

    Server Templates

    - -

    - Whilst most of index.html is a static Vue - app, templates is leveraged to generate the body of the <redis-info/> Component on the initial home page render: -

    - -{{ 'gfm/web-apps/08.md' | githubMarkdown }} - -

    - This technique increases the time to first paint by being able to render the initial Vue page without waiting for an Ajax call response - whilst benefiting from improved SEO from server-generated HTML. -

    - -

    Server Handling

    - -

    - Another area Templates is used is to handle the HTTP POST where it calls the redisChangeConnection filter to change - the current Redis connection before rendering the - connection-info.html partial with the - current connection info: -

    - -{{ 'gfm/web-apps/09.md' | githubMarkdown }} - -

    Vue Ajax Server APIs

    - -

    - All other Server functionality is invoked by Vue using Ajax to call one of the Ajax APIs below implemented as - API Pages: -

    - -
    search.html
    - -

    - Called when searching for Redis keys where the query is forwarded to the redisSearchKeys filter: -

    - -{{ 'gfm/web-apps/10.md' | githubMarkdown }} - -
    call.html
    - -

    - Called to execute an arbitrary Redis command on the connected instance, with the response from Redis is returned as a - plain-text HTTP Response: -

    - -{{ 'gfm/web-apps/11.md' | githubMarkdown }} - -

    - The benefits of using API Pages instead of a normal C# Service is being able to retain Web App's productive development workflow - where the entire Redis Vue App is built without requiring any compilation. -

    - -

    Deep linking and full page reloads

    - -

    - The Redis Vue Single Page App also takes advantage of HTML5's history.pushState API to enable deep-linking and back-button - support where most UI state changes is captured on the query string and used to initialize the Vue state on page navigation or - full-page reloads where it provides transparent navigation and back-button support that functions like a traditional Web App but with - the instant performance of a Single Page App. -

    - -

    Rockwind

    -
    - web install rockwind - - rockwind-sqlite.web-app.io - - NetCoreWebApps/Rockwind -
    - -

    - The Rockwind website shows an example of combining multiple websites in a single Web App - a - Rockstars Content Website and a dynamic data-driven UI for the Northwind database which can - run against either SQL Server, MySql or SQLite database using just configuration. It also includes - API Pages examples for rapidly developing Web APIs. -

    - -

    Rockstars

    - -

    - /rockstars is an - example of a Content Website that itself maintains multiple sub sections with their own layouts - - /rockstars/alive - for living Rockstars and - /rockstars/dead - for the ones that have died. Each Rockstar maintains their own encapsulated - mix of HTML, markdown content and splash image - that intuitively uses the closest _layout.html, content.md and splash.jpg from the page they're - referenced from. This approach makes it easy to move entire sub sections over by just moving a folder and it will automatically - use the relevant layout and partials of its parent. -

    - -Rockwind WebApp screenshot - -

    Northwind

    - -

    - /northwind is an example of - a dynamic UI for a database containing a - form to filter results, multi-nested - detail pages and - deep-linking for quickly navigating between - referenced data. Templates is also a great solution for rapidly developing Web APIs where the - /api/customers.html - API Page below: -

    - -{{ 'gfm/api-pages/02.md' | githubMarkdown }} - -

    - Is all the code needed to generate the following API endpoints: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    /customers API
    All Customers - - - - -
    Accept HTTP Header also supported
    -
    Alfreds Futterkiste Details - - - -
    As List - - - -
    Customers in Germany - - - -
    Customers in London - - - -
    Combination Query - /api/customers?city=London&country=UK&limit=3 -
    - -

    Multi platform configurations

    - -

    - In addition to being a .NET Core 2.1 App that runs flawlessly cross-platform on Windows, Linux and OSX, Web Apps can also support - multiple RDBMS's and Virtual File Systems using just configuration. -

    - -

    app.sqlite.settings

    - -

    - SQLite uses a file system database letting you bundle your database with your App. So we can share the - northwind.sqlite database across multiple Apps, - the contentRoot is set to the /apps directory which can only be accessed by your App, whilst - the webRoot is configured to use the Web Apps folder that hosts all the publicly accessible files of your App. -

    - -
    debug true
    -name Rockwind SQLite Web App
    -contentRoot ..
    -webRoot .
    -db sqlite
    -db.connection ~/northwind.sqlite
    - -

    - To run the Rockwind app using the northwind.sqlite database, run the command below on Windows, Linux or OSX: -

    - -
    dotnet web/app.dll ../rockwind/app.sqlite.settings
    - -
    app.sqlserver.settings
    - -

    - To switch to use the Northwind database in SQL Server we just need to update the configuration to point to a SQL Server database - instance. Since the App no longer need access to the northwind.sqlite database, the contentRoot can be reverted - back to the Web Apps folder: -

    - -
    debug true
    -name Rockwind SQL Server Web App
    -port 5000
    -db sqlserver
    -db.connection Server=localhost;Database=northwind;User Id=test;Password=test;
    - -

    - The /support/northwind-data - project lets you quickly try out Rockwind against your local RDBMS by populating it with a copy of the Northwind database - using the same sqlserver identifier and connection string from the App, e.g: -

    - -
    dotnet run sqlserver "Server=localhost;Database=northwind;User Id=test;Password=test;"
    - -

    app.mysql.settings

    - -

    - You can run against a MySql database in the same way as SQL Server above but using a MySql db connection string: -

    - -
    debug true
    -name Rockwind MySql Web App
    -port 5000
    -db mysql
    -db.connection Server=localhost;Database=northwind;UID=root;Password=test;SslMode=none
    - -

    app.azure.settings

    - -

    - The example app.azure.settings - Azure configuration is also configured to use a different Virtual File System where instead of sourcing - Web App files from the filesystem they're sourced from an - Azure Blob Container. - In this case we're not using any files from the App so we don't need to set a contentRoot or webRoot path. - This also means that for deployment we're just deploying the WebApp binaries with just this app.settings since both the - Web App files and database are sourced remotely. -

    - -
    # Note: values prefixed with '$' are resolved from Environment Variables
    -debug false
    -name Azure Blob SQL Server Web App
    -bind *
    -port 5000
    -db sqlserver
    -db.connection $AZURE_SQL_CONNECTION_STRING
    -files azure
    -files.config {ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind-fs}
    -
    -# Reduces a Blob Storage API call, but takes longer for modified pages to appear
    -checkForModifiedPagesAfterSecs 60
    -defaultFileCacheExpirySecs     60
    - -

    - The /support/copy-files - project lets you run Rockwind against your own Azure Blob Container by populating it with a copy of the - /rockwind App's files using - the same configuration above: -

    - -
    dotnet run azure "{ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind}"
    - -

    Multi-RDBMS SQL

    - -

    - As Templates is unable to use a Typed ORM like OrmLite - to hide the nuances of each database, we need to be a bit more diligent in Templates to use parameterized SQL that works across - multiple databases by using the - sql* DB Filters to avoid using RDBMS-specific - SQL syntax. The - /northwind/customer.html - contains a good example containing a number of things to watch out for: -

    - -{{ 'gfm/web-apps/01.md' | githubMarkdown }} - -

    - Use sqlConcat to concatenate strings using the RDBMS-specific SQL for the configured database. Likewise - sqlCurrency utilizes RDBMS-specific SQL functions to return monetary values in a currency format, whilst - sqlQuote is used for quoting tables named after a reserved word. -

    - -
    - Of course if you don't intend on supporting multiple RDBMS's, you can ignore this and use RDBMS-specific syntax. -
    - -

    Rockwind VFS

    -
    - web install rockwind-aws - - rockwind-aws.web-app.io - - NetCoreWebApps/rockwind-aws -
    - -

    - /rockwind-vfs is a clone of - the Rockwind Web App with 3 differences: It uses the resolveAsset filter for each .js, .css and - image web asset so that it's able to generate external URLs directly to the S3 Bucket, Azure Blob Container or CDN - hosting a copy of your files to both reduce the load on your Web App and maximize the responsiveness to the end user. -

    - -

    - To maximize responsiveness when using remote storage, all - embedded files utilize caching: -

    - -
    {{ pass: "content.md" | includeFileWithCache | markdown }}
    - -

    - The other difference is that each table and column has been quoted in "double-quotes" so that it works in PostgreSQL which - otherwise treats unquoted symbols as lowercase. This version of Rockwind also works with SQL Server and SQLite as they also - support "Table" quotes but not MySql which uses `BackTicks` or [SquareBrackets]. It's therefore - infeasible to develop Apps that support both PostgreSQL and MySql unless you're willing to use all lowercase, - snake_case or the sqlQuote filter for every table and column. -

    - -Rockwind VFS WebApp screenshot - -

    resolveAsset

    - -

    - If using a remote file storage like AWS S3 or Azure Blob Storage it's a good idea to use the resolveAsset filter - for each external file reference. By default it returns the same path it was called with so it will continue to work locally - but then ServiceStack effectively becomes a proxy where it has to call the remote Storage Service for each requested download. -

    - -{{ 'gfm/web-apps/02.md' | githubMarkdown }} - -

    - ServiceStack asynchronously writes each file to the Response Stream with the last Last-Modified HTTP Header to - enable browser caching so it's still a workable solution but for optimal performance you can specify an args.assetsBase - in your app.settings to populate the assetsBase TemplateContext Argument the resolveAsset filter uses to generate - an external URL reference to the file on the remote storage service, reducing the load and improving the performance of your App, - especially if it's configured to use a CDN. -

    - -

    Pure Cloud Apps

    - -
    rockwind-aws/app.settings
    - -

    - The AWS settings shows an example of this where every external resource - rockwind-aws.web-app.io has been replaced with a direct reference to the - asset on the S3 bucket: -

    - -
    # Note: values prefixed with '$' are resolved from Environment Variables
    -debug false
    -name AWS S3 PostgreSQL Web App
    -db postgres
    -db.connection $AWS_RDS_POSTGRES
    -files s3
    -files.config {AccessKey:$AWS_S3_ACCESS_KEY,SecretKey:$AWS_S3_SECRET_KEY,Region:us-east-1,Bucket:rockwind}
    -args.assetsBase http://s3-postgresql.s3-website-us-east-1.amazonaws.com/
    -
    -# Reduces an S3 API call, but takes longer for modified pages to appear
    -checkForModifiedPagesAfterSecs 60
    -defaultFileCacheExpirySecs     60
    - -

    - With all files being sourced from S3 and the App configured to use AWS RDS PostgreSQL, the AWS settings is an example of - a Pure Cloud App where the entire App is hosted on managed cloud services that's decoupled from the .NET Core 2.1 binary - that runs it that for the most part won't require redeploying the Web App binary unless making configuration changes or - upgrading the web/app.dll as any App changes can just be uploaded straight to S3 which changes reflected within the - checkForModifiedPagesAfterSecs setting, which tells the Web App how long to wait before checking for file changes - whilst defaultFileCacheExpirySecs specifies how long to cache files like content.md for. -

    - -
    DockerFile
    - -

    - Deployments are also greatly simplified as all that's needed is to deploy the WebApp binary and app.settings of your Cloud App, - e.g. here's the DockerFile for rockwind-aws.web-app.io - deployed to AWS ECS - using the deployment scripts in rockwind-aws and following our - .NET Core Docker Deployment Guideline: -

    - -{{ 'gfm/web-apps/03.md' | githubMarkdown }} - -
    rockwind-azure/app.settings
    - -

    - We can also create Azure Cloud Apps in the same we've done for AWS above, which runs the same - /rockwind-vfs - Web App but using an Azure hosted SQL Server database and its files hosted on Azure Blob Storage: -

    - -
    # Note: values prefixed with '$' are resolved from Environment Variables
    -debug false
    -name Azure Blob SQL Server Web App
    -bind *
    -db sqlserver
    -db.connection $AZURE_SQL_CONNECTION_STRING
    -files azure
    -files.config {ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind}
    -args.assetsBase https://servicestack.blob.core.windows.net/rockwind/
    -
    -# Reduces an S3 API call, but takes longer for modified pages to appear
    -checkForModifiedPagesAfterSecs 60
    -defaultFileCacheExpirySecs     60
    - -

    Plugins

    -
    - web install plugins - - plugins.web-app.io - - NetCoreWebApps/Plugins -
    - -

    - Up till now the Apps above only have only used functionality built into ServiceStack, to enable even greater functionality - but still retain all the benefits of developing Web Apps you can drop .dll with custom functionality into your - Web App's /plugins folder. The plugins support in Web Apps is as friction-less as we could make it, there's no - configuration to maintain or special interfaces to implement, you're able to drop your existing implementation .dll's - as-is into the App's `/plugins` folder. -

    - -

    - Plugins allow "no touch" sharing of - ServiceStack Plugins, - Services, - Template Filters - Template Code Pages, - Validators, etc. - contained within .dll's or .exe's dropped in a Web App's - /plugins folder which are auto-registered - on startup. The source code for all plugins used in this App were built from the .NET Core 2.1 projects in the - /example-plugins folder. The - plugins.web-app.io Web App below walks through examples of using Custom Filters, - Services and Validators: -

    - -Plugins WebApp screenshot - -

    Registering ServiceStack Plugins

    - -

    - ServiceStack Plugins can be added to your App by - listing it's Type Name in the features config entry in - app.settings: -

    - -
    debug true
    -name Web App Plugins
    -contentRoot ~/../plugins
    -webRoot ~/../plugins
    -features CustomPlugin, OpenApiFeature, PostmanFeature, CorsFeature, ValidationFeature
    -CustomPlugin { ShowProcessLinks: true }
    -ValidationFeature { ScanAppHostAssemblies: true }
    -
    - -

    - All plugins listed in features will be added to your Web App's AppHost in the order they're specified. - They can further customized by adding a separate config entry with the Plugin Name and a JavaScript Object literal to - populate the Plugin at registration, e.g the config above is equivalent to: -

    - -{{ 'gfm/web-apps/04.md' | githubMarkdown }} - -

    Custom Plugin

    - -

    - In this case it tells our CustomPlugin - from /plugins/ServerInfo.dll to also show Process Links in its - /metadata Page: -

    - -{{ 'gfm/web-apps/05.md' | githubMarkdown }} - -

    - Where as it was first registered in the list will appear before any links registered by other plugins: -

    - -Metadata screenshot - -

    Built-in Plugins

    - -

    - It also tells the ValidationFeature to scan all Service Assemblies for Validators and to automatically register them - which is how ServiceStack was able to find the - ContactValidator - used to validate the StoreContact request. -

    - -

    - Other optional plugins registered in this Web App is the metadata Services required for - Open API, - Postman as well as - support for CORS. - You can check the /metadata/debug Template for all Plugins loaded in your AppHost. -

    - -

    .NET Extensibility

    - -

    - Plugins can also implement .NET Core's IStartup to be able to register any required dependencies without any coupling to any Custom AppHost. -

    - -

    - To simplify configuration you can use the `plugins/*` wildcard in - app.settings - at the end of an ordered plugin list to register all remaining Plugins it finds in the apps `/plugins` folder: -

    - - -
    features OpenApiFeature, PostmanFeature, CorsFeature, ValidationFeature, plugins/*
    -CustomPlugin { ShowProcessLinks: true }
    -
    - -

    - Each plugin registered can continue to be furthered configured by specifying its name and a JavaScript object literal as seen above. -

    - -

    - The /plugins2 App shows an example of this with the - StartupPlugin - registering a StartupDep dependency which is used by its StartupServices at runtime: -

    - -{{ 'gfm/web-apps/12.md' | githubMarkdown }} - -

    ServiceStack Ecosystem

    - -

    - All Services loaded by plugins continue to benefit from ServiceStack's rich metadata services, including being listed - in the /metadata page, being able to explore and interact with Services using - /swagger-ui/ as well as being able to generate Typed APIs for the most popular - Mobile, Web and Desktop platforms. -

    - -

    Chat

    -
    - web install chat - - chat.web-app.io - - NetCoreWebApps/Chat -
    - -

    - /chat is an example of the ultimate form - of extensibility where instead of just being able to add Services, Filters and Plugins, etc. You can add your entire - AppHost which Web Apps will use instead of its own. This vastly expands the use-cases that can be built with - Web Apps as it gives you complete fine-grained control over how your App is configured. -

    - -Chat WebApp screenshot - -

    Develop back-end using .NET IDE's

    - -

    - For chat.web-app.io we've taken a copy of the existing .NET Core 2.1 - Chat App and moved its C# code to - /example-plugins/Chat - and its files to /apps/chat - where it can be developed like any other Web App except it utilizes the Chat AppHost and implementation in the - SelfHost Chat App. -

    - -

    - Customizations from the original - .NET Core Chat implementation - includes removing MVC and Razor dependencies and configuration, extracting its - _layout.html and - converting index.html - to use Templates from its original - default.cshtml. - It's also been enhanced with the ability to evaluate Templates from the Chat window, as seen in the screenshot above. -

    - -

    Chat AppHost

    - -{{ 'gfm/web-apps/06.md' | githubMarkdown }} - -
    Reusing Web App's app.setting and files
    - -

    - One nice thing from being able to reuse existing AppHost's is being able to develop all back-end C# Services and Custom Filters - as a stand-alone .NET Core Project where it's more productive with access to .NET IDE tooling and debugging. -

    - -

    - To account for these 2 modes we use AddIfNotExists to only register the TemplatePagesFeature plugin - when running as a stand-alone App and add an additional constructor so it reuses the existing app.settings as its - IAppSettings provider for is custom App configuration like OAuth App keys - required for enabling Sign-In's via with Twitter, Facebook and GitHub when running on http://localhost:5000: -

    - -
    debug true
    -name Chat Web App
    -contentRoot ~/../chat
    -webRoot ~/../chat
    -
    -oauth.RedirectUrl http://localhost:5000/
    -oauth.CallbackUrl http://localhost:5000/auth/{0}
    -oauth.twitter.ConsumerKey JvWZokH73rdghDdCFCFkJtCEU
    -oauth.twitter.ConsumerSecret WNeOT6YalxXDR4iWZjc4jVjFaydoDcY8jgRrGc5FVLjsVlY2Y8
    -oauth.facebook.Permissions email
    -oauth.facebook.AppId 447523305684110
    -oauth.facebook.AppSecret 7d8a16d5c7cbbfab4b49fd51183c93a0
    -oauth.github.Scopes user
    -oauth.github.ClientId dbe8c242e3d1099f4558
    -oauth.github.ClientSecret 42c8db8d0ca72a0ef202e0c197d1377670c990f4
    -
    - -

    - After the back-end has been implemented we can build and copy the compiled Chat.dll into the Chat's - /plugins folder where - we can take advantage of the improved development experience for rapidly developing its UI. -

    - -

    Blog

    -
    - web install blog - - blog.web-app.io - - NetCoreWebApps/Blog -
    - -{{ 'gfm/web-apps/14.md' | githubMarkdown }} - -

    Customizable Auth Providers

    - -

    - Authentication can now be configured using plain text config in your app.settings where initially you need register the AuthFeature - plugin as normal by specifying it in the - features list: -

    - -
    features AuthFeature
    - -

    - Then using AuthFeature.AuthProviders you can specify which Auth Providers you want to have registered, e.g: -

    - -
    AuthFeature.AuthProviders TwitterAuthProvider, GithubAuthProvider
    - -

    - Each Auth Provider checks the Web Apps app.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 now utilize Markdig implementation to render its Markdown. - You can also switch it back to the built-in Markdown provider that ServiceStack uses with: -

    - -
    markdownProvider MarkdownDeep
    - -

    Rich Template Config Arguments

    - -

    - Any app.settings configs that are prefixed with args. are made available to Template Pages and any arguments starting with - a { 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: -

    - -{{#raw appendTo configExample}}{{blog.name}}{{/raw}} - -{{#raw}}{{#each tags}} {{it}} {{/each}}{{/raw}} - -
    {{configExample}}
    - -

    - The alternative approach is to give each argument value a different name: -

    - -
    args.blogName blog.web-app.io
    -args.blogHref /
    -
    - -{{ "doc-links" | partial({ order }) }} diff --git a/src/wwwroot/examples/adhoc-query-db.html b/src/wwwroot/examples/adhoc-query-db.html deleted file mode 100644 index 452f669..0000000 --- a/src/wwwroot/examples/adhoc-query-db.html +++ /dev/null @@ -1,14 +0,0 @@ -

    HTML Results (select):

    -{{ "select customerId, companyName, city, country from customer where country = @country" - | assignTo: selectSql }} -{{ selectSql | dbSelect({ country: 'UK' }) | htmlDump({ caption: selectSql }) }} - -

    Text Results (single):

    -{{ "select * from customer c join [order] o on c.customerId = o.customerId - order by total desc limit 1" | assignTo: singleSql }} -
    {{ singleSql }}:
    -{{ singleSql | dbSingle | dump }}
    - -

    Object Value (scalar):

    -{{ "select min(orderDate) from [order]" | assignTo: scalarSql }} -
    {{ scalarSql }}: {{ scalarSql | dbScalar | dateFormat }}
    diff --git a/src/wwwroot/examples/customer-card.txt b/src/wwwroot/examples/customer-card.txt deleted file mode 100644 index 70cfeca..0000000 --- a/src/wwwroot/examples/customer-card.txt +++ /dev/null @@ -1 +0,0 @@ -{{ 'customerCard' | partial({ customerId: "ALFKI" }) }} \ No newline at end of file diff --git a/src/wwwroot/examples/email-template.txt b/src/wwwroot/examples/email-template.txt deleted file mode 100644 index 7ad5b98..0000000 --- a/src/wwwroot/examples/email-template.txt +++ /dev/null @@ -1,18 +0,0 @@ - -Hello {{ customer.CompanyName }}, - -Thank you for shopping with us. We'll send a confirmation when your item ships. - -# Ship to: -{{ customer.Address }} -{{ customer.City }}, {{ customer.PostalCode }}, {{ customer.Country }} - -# Details: -Order #{{ order.OrderId }} on {{ order.OrderDate | dateFormat('dd/MM/yyyy') }} -Total: {{ order.Total | currency }} - -We hope to see you again soon. - -[acme.org][1] - -[1]: http://acme.org \ No newline at end of file diff --git a/src/wwwroot/examples/introspect-drive.html b/src/wwwroot/examples/introspect-drive.html deleted file mode 100644 index f625d2b..0000000 --- a/src/wwwroot/examples/introspect-drive.html +++ /dev/null @@ -1,7 +0,0 @@ - - {{ it.Name }} - {{ it.DriveType }} #{{ it.VolumeLabel }} ({{ it.DriveFormat }}) - {{ it.AvailableFreeSpace | divide(1024) | format('n0') }} KB - {{ it.TotalFreeSpace | divide(1024) | format('n0') }} KB - {{ it.TotalSize | divide(1024) | format('n0') }} KB - \ No newline at end of file diff --git a/src/wwwroot/examples/introspect-process.html b/src/wwwroot/examples/introspect-process.html deleted file mode 100644 index 9f7b40b..0000000 --- a/src/wwwroot/examples/introspect-process.html +++ /dev/null @@ -1,10 +0,0 @@ -{{ "h':'mm':'ss'.'FFF" | assignTo: fmtTime }} - - {{ it.Id }} - {{ it.ProcessName | substringWithElipsis(15) }} - {{ it.TotalProcessorTime | timeFormat(fmtTime) }} - {{ it.UserProcessorTime | timeFormat(fmtTime) }} - {{ it.WorkingSet64 | divide(1024) | format('n0') }} KB - {{ it.PeakWorkingSet64 | divide(1024) | format('n0') }} KB - {{ it.Threads.Count }} - \ No newline at end of file diff --git a/src/wwwroot/examples/introspect.html b/src/wwwroot/examples/introspect.html deleted file mode 100644 index 04041be..0000000 --- a/src/wwwroot/examples/introspect.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - {{#each drives orderby it.TotalSize descending take 5}} - - - - - - - - {{/each}} -
    Top 5 Local Disks
    NameTypeAvailable SpaceTotal Free SpaceTotal Size
    {{ Name | substringWithEllipsis(50) }}{{ DriveType }} #{{ VolumeLabel }} ({{ DriveFormat }}){{ AvailableFreeSpace / 1024 | format('n0') }} KB{{ TotalFreeSpace / 1024 | format('n0') }} KB{{ TotalSize / 1024 | format('n0') }} KB
    - - - - - - {{#if currentProcess}} - {{ currentProcess | assignTo: p }} - {{ "h':'mm':'ss'.'FFF" | assignTo: fmtTime }} - - - - - - - - - - {{/if}} -
    Current Process
    IdNameCPU TimeUser TimeMemory (current)Memory (peak)Active Threads
    {{ Id }}{{ p.ProcessName | substringWithEllipsis(15) }}{{ p.TotalProcessorTime | timeFormat(fmtTime) }}{{ p.UserProcessorTime | timeFormat(fmtTime) }}{{ p.WorkingSet64 / 1024 | format('n0') }} KB{{ p.PeakWorkingSet64 / 1024 | format('n0') }} KB{{ p.Threads.Count }}
    \ No newline at end of file diff --git a/src/wwwroot/examples/monthly-budget.txt b/src/wwwroot/examples/monthly-budget.txt deleted file mode 100644 index 7229ed8..0000000 --- a/src/wwwroot/examples/monthly-budget.txt +++ /dev/null @@ -1,32 +0,0 @@ -{{ 11200 | assignTo: balance }} -{{ 3 | assignTo: projectedMonths }} -{{' -Salary: 4000 -App Royalties: 200 -'| trim | parseKeyValueText(':') | assignTo: monthlyRevenues }} -{{' -Rent 1000 -Internet 50 -Mobile 50 -Food 400 -Misc 200 -'| trim | parseKeyValueText | assignTo: monthlyExpenses }} -{{ monthlyRevenues | values | sum | assignTo: totalRevenues }} -{{ monthlyExpenses | values | sum | assignTo: totalExpenses }} -{{ subtract(totalRevenues, totalExpenses) | assignTo: totalSavings }} - -Current Balance: {{ balance | currency }} - -Monthly Revenues: -{{ monthlyRevenues | toList | select: { it.Key | padRight(17) }{ it.Value | currency }\n }} -Total {{ totalRevenues | currency }} - -Monthly Expenses: -{{ monthlyExpenses | toList | select: { it.Key | padRight(17) }{ it.Value | currency }\n }} -Total {{ totalExpenses | currency }} - -Monthly Savings: {{ totalSavings | currency }} - -Projected Cash Position: -{{ projectedMonths | times | map => index + 1 - | select: { now | addMonths(it) | dateFormat } { totalSavings | multiply(it) | add(balance) | currency }\n }} \ No newline at end of file diff --git a/src/wwwroot/examples/nav-links.txt b/src/wwwroot/examples/nav-links.txt deleted file mode 100644 index ce61a81..0000000 --- a/src/wwwroot/examples/nav-links.txt +++ /dev/null @@ -1,9 +0,0 @@ -{{ 'navLinks' | partial( - { - links: { - '/docs/model-view-controller': 'MVC', - '/docs/view-engine': 'View Engine', - '/docs/code-pages': 'Code Pages' - } - }) -}} diff --git a/src/wwwroot/examples/qotd.html b/src/wwwroot/examples/qotd.html deleted file mode 100644 index f99ec84..0000000 --- a/src/wwwroot/examples/qotd.html +++ /dev/null @@ -1,15 +0,0 @@ - - -

    Before

    - -

    {{ 'select url from quote where id= @id' | dbScalar({ qs.id }) | urlContents | htmlencode }}

    - -

    Code

    - -
    {{ 'examples/quote.html' | includeFile }}
    - -

    After

    - -{{ 'examples/quote' | partial({ qs.id }) }} diff --git a/src/wwwroot/examples/quote.html b/src/wwwroot/examples/quote.html deleted file mode 100644 index 8bf5103..0000000 --- a/src/wwwroot/examples/quote.html +++ /dev/null @@ -1,3 +0,0 @@ -{{ 'select url from quote where id= @id' | dbScalar({ qs.id }) | urlContents | markdown | assignTo:quote}} - -{{ quote | replace('Razor', 'Templates') | replace('2010', now.Year) | raw }} \ No newline at end of file diff --git a/src/wwwroot/examples/sendToAutoQuery-data.html b/src/wwwroot/examples/sendToAutoQuery-data.html deleted file mode 100644 index a754a2c..0000000 --- a/src/wwwroot/examples/sendToAutoQuery-data.html +++ /dev/null @@ -1,5 +0,0 @@ -{{ { UserName:'ServiceStack', NameStartsWith:'ServiceStack', OrderBy:'-Watchers_Count',take:10 } - | sendToAutoQuery: QueryGitHubRepos - | toResults - | selectFields: Name, Homepage, Language, Watchers_Count - | htmlDump({ caption: "Most popular ServiceStack repos starting with 'ServiceStack'" }) }} \ No newline at end of file diff --git a/src/wwwroot/examples/sendToAutoQuery-rdms.html b/src/wwwroot/examples/sendToAutoQuery-rdms.html deleted file mode 100644 index e9d42a6..0000000 --- a/src/wwwroot/examples/sendToAutoQuery-rdms.html +++ /dev/null @@ -1,5 +0,0 @@ -{{ { cityIn:['London','Madrid','Rio de Janeiro'], faxContains:'555' } - | sendToAutoQuery: QueryCustomers - | toResults - | selectFields: CustomerId, CompanyName, City, Fax - | htmlDump({ caption: 'Implicit AutoQuery Conventions' }) }} diff --git a/src/wwwroot/examples/sendtogateway-customers.html b/src/wwwroot/examples/sendtogateway-customers.html deleted file mode 100644 index 7b51dff..0000000 --- a/src/wwwroot/examples/sendtogateway-customers.html +++ /dev/null @@ -1,10 +0,0 @@ -{{ { customerId } | sendToGateway('QueryCustomers') | toResults | get(0) - | htmlDump({ caption: 'ALFKI' }) }} - -{{ { countryIn:['UK','Germany'], orderBy:'customerId',take:5 } | sendToGateway: QueryCustomers - | toResults | selectFields(['CustomerId', 'CompanyName', 'City']) - | htmlDump({ caption: 'Implicit AutoQuery Conventions' }) }} - -{{ { companyNameContains:'the',orderBy:'-Country,CustomerId' } | sendToGateway: QueryCustomers - | toResults | selectFields: CustomerId, CompanyName, Country - | htmlDump }} diff --git a/src/wwwroot/gfm/adhoc-querying/01.html b/src/wwwroot/gfm/adhoc-querying/01.html deleted file mode 100644 index aa9e755..0000000 --- a/src/wwwroot/gfm/adhoc-querying/01.html +++ /dev/null @@ -1,9 +0,0 @@ -
    var context = new TemplateContext {
    -    TemplateFilters = {
    -        new TemplateDbFiltersAsync(),
    -    }
    -};
    -context.Container.AddSingleton<IDbConnectionFactory>(() => new OrmLiteConnectionFactory(
    -    connectionString, SqlServer2012Dialect.Provider));
    -context.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/adhoc-querying/01.md b/src/wwwroot/gfm/adhoc-querying/01.md deleted file mode 100644 index 70124b1..0000000 --- a/src/wwwroot/gfm/adhoc-querying/01.md +++ /dev/null @@ -1,10 +0,0 @@ -```csharp -var context = new TemplateContext { - TemplateFilters = { - new TemplateDbFiltersAsync(), - } -}; -context.Container.AddSingleton(() => new OrmLiteConnectionFactory( - connectionString, SqlServer2012Dialect.Provider)); -context.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/adhoc-querying/02.html b/src/wwwroot/gfm/adhoc-querying/02.html deleted file mode 100644 index c3cc122..0000000 --- a/src/wwwroot/gfm/adhoc-querying/02.html +++ /dev/null @@ -1,3 +0,0 @@ -
    container.Register<IDbConnectionFactory>(c => new OrmLiteConnectionFactory(
    -    $"Data Source={filePath};Read Only=true", SqliteDialect.Provider));
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/adhoc-querying/02.md b/src/wwwroot/gfm/adhoc-querying/02.md deleted file mode 100644 index 7715215..0000000 --- a/src/wwwroot/gfm/adhoc-querying/02.md +++ /dev/null @@ -1,4 +0,0 @@ -```csharp -container.Register(c => new OrmLiteConnectionFactory( - $"Data Source={filePath};Read Only=true", SqliteDialect.Provider)); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/adhoc-querying/03.html b/src/wwwroot/gfm/adhoc-querying/03.html deleted file mode 100644 index aee76ce..0000000 --- a/src/wwwroot/gfm/adhoc-querying/03.html +++ /dev/null @@ -1,12 +0,0 @@ -
    container.Register<IDbConnectionFactory>(c => 
    -    new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider));
    -
    -using (var db = container.Resolve<IDbConnectionFactory>().Open())
    -{
    -    db.CreateTable<Order>();
    -    db.CreateTable<Customer>();
    -    db.CreateTable<Product>();
    -    TemplateQueryData.Customers.Each(x => db.Save(x, references:true));
    -    db.InsertAll(TemplateQueryData.Products);
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/adhoc-querying/03.md b/src/wwwroot/gfm/adhoc-querying/03.md deleted file mode 100644 index 6ea62e7..0000000 --- a/src/wwwroot/gfm/adhoc-querying/03.md +++ /dev/null @@ -1,13 +0,0 @@ -```csharp -container.Register(c => - new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider)); - -using (var db = container.Resolve().Open()) -{ - db.CreateTable(); - db.CreateTable(); - db.CreateTable(); - TemplateQueryData.Customers.Each(x => db.Save(x, references:true)); - db.InsertAll(TemplateQueryData.Products); -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/adhoc-querying/04.html b/src/wwwroot/gfm/adhoc-querying/04.html deleted file mode 100644 index ad41e94..0000000 --- a/src/wwwroot/gfm/adhoc-querying/04.html +++ /dev/null @@ -1,121 +0,0 @@ -

    The combination of features in the new Templates makes easy work of typically tedious tasks, e.g. if you were tasked to create a report -that contained all information about a Northwind Order displayed on a -single page, you can create a new page at:

    - -

    packed with all Queries you need to run and execute them with a DB Filter and display them -with a HTML Filter:

    -
    <!--
    -title: Order Report
    --->
    -
    -{{ `SELECT o.Id, OrderDate, CustomerId, Freight, e.Id as EmployeeId, s.CompanyName as ShipVia, 
    -           ShipAddress, ShipCity, ShipPostalCode, ShipCountry
    -      FROM ${sqlQuote("Order")} o
    -           INNER JOIN
    -           Employee e ON o.EmployeeId = e.Id
    -           INNER JOIN
    -           Shipper s ON o.ShipVia = s.Id
    -     WHERE o.Id = @id` 
    -  | dbSingle({ id }) | assignTo: order }}
    -
    -{{#with order}}
    -  {{ "table table-striped" | assignTo: className }}
    -  <style>table {border: 5px solid transparent} th {white-space: nowrap}</style>
    -  
    -  <div style="display:flex">
    -      {{ order | htmlDump({ caption: 'Order Details', className }) }}
    -      {{ `SELECT * FROM Customer WHERE Id = @CustomerId` 
    -         | dbSingle({ CustomerId }) | htmlDump({ caption: `Customer Details`, className }) }}
    -      {{ `SELECT Id,LastName,FirstName,Title,City,Country,Extension FROM Employee WHERE Id = @EmployeeId` 
    -         | dbSingle({ EmployeeId }) | htmlDump({ caption: `Employee Details`, className }) }}
    -  </div>
    -
    -  {{ `SELECT p.ProductName, ${sqlCurrency("od.UnitPrice")} UnitPrice, Quantity, Discount
    -        FROM OrderDetail od
    -             INNER JOIN
    -             Product p ON od.ProductId = p.Id
    -       WHERE OrderId = @id`
    -      | dbSelect({ id }) 
    -      | htmlDump({ caption: "Line Items", className }) }}
    -{{else}}
    -  {{ `There is no Order with id: ${id}` }}
    -{{/with}}
    -

    This will let you view the complete details of any order at the following URL:

    - -

    -

    -SQL Studio Example

    -

    To take the ad hoc SQL Query example even further, it also becomes trivial to implement a SQL Viewer to run ad hoc queries on your App's configured database.

    -

    -

    The Northwind SQL Viewer above was developed using the 2 Template Pages below:

    -

    -/northwind/sql/index.html -

    -

    A Template Page to render the UI, shortcut links to quickly see the last 10 rows of each table, a <textarea/> to capture the SQL Query which -is sent to an API on every keystroke where the results are displayed instantly:

    -
    <h2>Northwind SQL Viewer</h2>
    -
    -<textarea name="sql">select * from "Customer" order by Id desc limit 10</textarea>
    -<ul class="tables">
    -  <li>Category</li>
    -  <li>Customer</li>
    -  <li>Employee</li>
    -  <li>Order</li>
    -  <li>Product</li>
    -  <li>Supplier</li>
    -</ul>
    -
    -<div class="preview"></div>
    -<style>/*...*/</style>
    -
    -<script>
    -let textarea = document.querySelector("textarea");
    -let listItems = document.querySelectorAll('.tables li');
    -for (let i=0; i<listItems.length; i++) {
    -  listItems[i].addEventListener('click', function(e){
    -    var table = e.target.innerHTML;
    -    textarea.value = 'select * from "' + table + '" order by Id desc limit 10';
    -    textarea.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
    -  });
    -}
    -// Enable Live Preview of SQL
    -textarea.addEventListener("input", livepreview, false);
    -livepreview({ target: textarea });
    -function livepreview(e) {
    -  let el = e.target;
    -  let sel = '.preview';
    -  if (el.value.trim() == "") {
    -    document.querySelector(sel).innerHTML = "";
    -    return;
    -  }
    -  let formData = new FormData();
    -  formData.append("sql", el.value);
    -  fetch("api", {
    -    method: "post",
    -    body: formData
    -  }).then(function(r) { return r.text(); })
    -    .then(function(r) { document.querySelector(sel).innerHTML = r; });
    -}
    -</script>
    -

    -/northwind/sql/api.html -

    -

    All that's left is to implement the API which just needs to check to ensure the SQL does not contain any destructive operations using the -isUnsafeSql DB filter, if it doesn't execute the SQL with the dbSelect DB Filter, generate a HTML Table with htmlDump and return -the partial HTML fragment with return:

    -
    {{#if isUnsafeSql(sql) }} 
    -    {{ `<div class="alert alert-danger">Potentially unsafe SQL detected</div>` | return }}
    -{{/if}}
    -
    -{{ sql | dbSelect | htmlDump | return }}
    -

    -Live Development Workflow

    -

    Thanks to the live development workflow of Template Pages, this is the quickest way we've previously been able to implement any of this functionality. -Where all development can happen at runtime with no compilation or builds, yielding a highly productive iterative workflow to implement common functionality -like viewing ad hoc SQL Queries in Excel or even just to rapidly prototype APIs so they can be consumed immediately by Client Applications before -formalizing them into Typed ServiceStack Services where they can take advantage of its rich typed metadata and ecosystem.

    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/adhoc-querying/04.md b/src/wwwroot/gfm/adhoc-querying/04.md deleted file mode 100644 index 6d9bcf3..0000000 --- a/src/wwwroot/gfm/adhoc-querying/04.md +++ /dev/null @@ -1,134 +0,0 @@ -The combination of features in the new Templates makes easy work of typically tedious tasks, e.g. if you were tasked to create a report -that contained all information about a [Northwind Order](http://rockwind-sqlite.web-app.io/northwind/order?id=10643) displayed on a -single page, you can create a new page at: - - - [/northwind/order-report/_id.html](https://github.com/ServiceStack/dotnet-app/blob/master/src/apps/rockwind/northwind/order-report/_id.html) - -packed with all Queries you need to run and execute them with a [DB Filter](http://templates.servicestack.net/docs/db-filters) and display them -with a [HTML Filter](http://templates.servicestack.net/docs/html-filters): - -```hbs - - -{{ `SELECT o.Id, OrderDate, CustomerId, Freight, e.Id as EmployeeId, s.CompanyName as ShipVia, - ShipAddress, ShipCity, ShipPostalCode, ShipCountry - FROM ${sqlQuote("Order")} o - INNER JOIN - Employee e ON o.EmployeeId = e.Id - INNER JOIN - Shipper s ON o.ShipVia = s.Id - WHERE o.Id = @id` - | dbSingle({ id }) | assignTo: order }} - -{{#with order}} - {{ "table table-striped" | assignTo: className }} - - -
    - {{ order | htmlDump({ caption: 'Order Details', className }) }} - {{ `SELECT * FROM Customer WHERE Id = @CustomerId` - | dbSingle({ CustomerId }) | htmlDump({ caption: `Customer Details`, className }) }} - {{ `SELECT Id,LastName,FirstName,Title,City,Country,Extension FROM Employee WHERE Id = @EmployeeId` - | dbSingle({ EmployeeId }) | htmlDump({ caption: `Employee Details`, className }) }} -
    - - {{ `SELECT p.ProductName, ${sqlCurrency("od.UnitPrice")} UnitPrice, Quantity, Discount - FROM OrderDetail od - INNER JOIN - Product p ON od.ProductId = p.Id - WHERE OrderId = @id` - | dbSelect({ id }) - | htmlDump({ caption: "Line Items", className }) }} -{{else}} - {{ `There is no Order with id: ${id}` }} -{{/with}} -``` - -This will let you view the complete details of any order at the following URL: - - - [/northwind/order-report/10643](http://rockwind-sqlite.web-app.io/northwind/order-report/10643) - -[![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/rockwind/order-report.png)](http://rockwind-sqlite.web-app.io/northwind/order-report/10643) - -### SQL Studio Example - -To take the ad hoc SQL Query example even further, it also becomes trivial to implement a SQL Viewer to run ad hoc queries on your App's configured database. - -[![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/rockwind/sql-viewer.png)](http://rockwind-sqlite.web-app.io/northwind/sql/) - -The [Northwind SQL Viewer](http://rockwind-sqlite.web-app.io/northwind/sql/) above was developed using the 2 Template Pages below: - -#### [/northwind/sql/index.html](https://github.com/ServiceStack/dotnet-app/blob/master/src/apps/rockwind/northwind/sql/index.html) - -A Template Page to render the UI, shortcut links to quickly see the last 10 rows of each table, a ` -
      -
    • Category
    • -
    • Customer
    • -
    • Employee
    • -
    • Order
    • -
    • Product
    • -
    • Supplier
    • -
    - -
    - - - -``` - -#### [/northwind/sql/api.html](https://github.com/ServiceStack/dotnet-app/blob/master/src/apps/rockwind/northwind/sql/api.html) - -All that's left is to implement the API which just needs to check to ensure the SQL does not contain any destructive operations using the -`isUnsafeSql` DB filter, if it doesn't execute the SQL with the `dbSelect` DB Filter, generate a HTML Table with `htmlDump` and return -the partial HTML fragment with `return`: - -```hbs -{{#if isUnsafeSql(sql) }} - {{ `
    Potentially unsafe SQL detected
    ` | return }} -{{/if}} - -{{ sql | dbSelect | htmlDump | return }} -``` - -### Live Development Workflow - -Thanks to the live development workflow of Template Pages, this is the quickest way we've previously been able to implement any of this functionality. -Where all development can happen at runtime with no compilation or builds, yielding a highly productive iterative workflow to implement common functionality -like viewing ad hoc SQL Queries in Excel or even just to rapidly prototype APIs so they can be consumed immediately by Client Applications before -formalizing them into Typed ServiceStack Services where they can take advantage of its rich typed metadata and ecosystem. \ No newline at end of file diff --git a/src/wwwroot/gfm/api-pages/01.html b/src/wwwroot/gfm/api-pages/01.html deleted file mode 100644 index 892c54a..0000000 --- a/src/wwwroot/gfm/api-pages/01.html +++ /dev/null @@ -1,5 +0,0 @@ -
    Plugins.Add(new TemplatePagesFeature 
    -{
    -    ApiPath = "/api"
    -})
    -
    diff --git a/src/wwwroot/gfm/api-pages/01.md b/src/wwwroot/gfm/api-pages/01.md deleted file mode 100644 index e5d7f4f..0000000 --- a/src/wwwroot/gfm/api-pages/01.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -Plugins.Add(new TemplatePagesFeature -{ - ApiPath = "/api" -}) -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/api-pages/02.html b/src/wwwroot/gfm/api-pages/02.html deleted file mode 100644 index e20fa18..0000000 --- a/src/wwwroot/gfm/api-pages/02.html +++ /dev/null @@ -1,22 +0,0 @@ -
    {{ limit ?? 100 | assignTo: limit }}
    -
    -{{ 'select Id, CompanyName, ContactName, ContactTitle, City, Country from Customer' | assignTo: sql }}
    -
    -{{#if !isEmpty(PathArgs)}}
    -   {{ `${sql} where Id = @id` | dbSingle({ id: PathArgs[0] }) 
    -      | return }}
    -{{/if}}
    -
    -{{#if id}}      {{ 'Id = @id'           | addTo: filters }} {{/if}}
    -{{#if city}}    {{ 'City = @city'       | addTo: filters }} {{/if}}
    -{{#if country}} {{ 'Country = @country' | addTo: filters }} {{/if}}
    -
    -{{#if !isEmpty(filters)}}
    -  {{ `${sql} WHERE ${join(filters, ' AND ')}` | assignTo: sql }}
    -{{/if}}
    -
    -{{ `${sql} ORDER BY CompanyName ${sqlLimit(limit)}` | assignTo: sql }}
    -
    -{{ sql | dbSelect({ id, city, country }) 
    -       | return }}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/api-pages/02.md b/src/wwwroot/gfm/api-pages/02.md deleted file mode 100644 index 104e89f..0000000 --- a/src/wwwroot/gfm/api-pages/02.md +++ /dev/null @@ -1,23 +0,0 @@ -```hbs -{{ limit ?? 100 | assignTo: limit }} - -{{ 'select Id, CompanyName, ContactName, ContactTitle, City, Country from Customer' | assignTo: sql }} - -{{#if !isEmpty(PathArgs)}} - {{ `${sql} where Id = @id` | dbSingle({ id: PathArgs[0] }) - | return }} -{{/if}} - -{{#if id}} {{ 'Id = @id' | addTo: filters }} {{/if}} -{{#if city}} {{ 'City = @city' | addTo: filters }} {{/if}} -{{#if country}} {{ 'Country = @country' | addTo: filters }} {{/if}} - -{{#if !isEmpty(filters)}} - {{ `${sql} WHERE ${join(filters, ' AND ')}` | assignTo: sql }} -{{/if}} - -{{ `${sql} ORDER BY CompanyName ${sqlLimit(limit)}` | assignTo: sql }} - -{{ sql | dbSelect({ id, city, country }) - | return }} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/api-pages/03.html b/src/wwwroot/gfm/api-pages/03.html deleted file mode 100644 index 157bae9..0000000 --- a/src/wwwroot/gfm/api-pages/03.html +++ /dev/null @@ -1,34 +0,0 @@ -
    {{ limit ?? 100 | assignTo: limit }}
    -
    -{{ `select p.Id, 
    -           ProductName,
    -           c.CategoryName Category,
    -           s.CompanyName Supplier, 
    -           QuantityPerUnit, 
    -           ${sqlCurrency("UnitPrice")} UnitPrice, 
    -           UnitsInStock, UnitsOnOrder, ReorderLevel   
    -      from Product p
    -           inner join Category c on p.CategoryId = c.Id
    -           inner join Supplier s on p.SupplierId = s.Id
    -     where Discontinued = 0`
    -  | assignTo: sql }}
    -
    -{{#if !isEmpty(PathArgs)}}
    -  {{ `${sql} and p.Id = @id` | dbSingle({ id: PathArgs[0] }) 
    -     | return }}
    -{{/if}}
    -
    -{{#if id}}           {{ 'p.Id = @id'                 | addTo: filters }} {{/if}}
    -{{#if category}}     {{ 'c.CategoryName = @category' | addTo: filters }} {{/if}}
    -{{#if supplier}}     {{ 's.CompanyName = @supplier'  | addTo: filters }} {{/if}}
    -{{#if nameContains}} {{ 'ProductName LIKE @name'     | addTo: filters }} {{/if}}
    -
    -{{#if !isEmpty(filters)}}
    -  {{ `${sql} and ${join(filters, ' and ')}` | assignTo: sql }}
    -{{/if}}
    -
    -{{ `${sql} ORDER BY CompanyName ${sqlLimit(limit)}` | assignTo: sql }}
    -
    -{{ sql | dbSelect({ id, category, supplier, name: `%${nameContains}%` }) 
    -       | return }}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/api-pages/03.md b/src/wwwroot/gfm/api-pages/03.md deleted file mode 100644 index 9ced02e..0000000 --- a/src/wwwroot/gfm/api-pages/03.md +++ /dev/null @@ -1,35 +0,0 @@ -```hbs -{{ limit ?? 100 | assignTo: limit }} - -{{ `select p.Id, - ProductName, - c.CategoryName Category, - s.CompanyName Supplier, - QuantityPerUnit, - ${sqlCurrency("UnitPrice")} UnitPrice, - UnitsInStock, UnitsOnOrder, ReorderLevel - from Product p - inner join Category c on p.CategoryId = c.Id - inner join Supplier s on p.SupplierId = s.Id - where Discontinued = 0` - | assignTo: sql }} - -{{#if !isEmpty(PathArgs)}} - {{ `${sql} and p.Id = @id` | dbSingle({ id: PathArgs[0] }) - | return }} -{{/if}} - -{{#if id}} {{ 'p.Id = @id' | addTo: filters }} {{/if}} -{{#if category}} {{ 'c.CategoryName = @category' | addTo: filters }} {{/if}} -{{#if supplier}} {{ 's.CompanyName = @supplier' | addTo: filters }} {{/if}} -{{#if nameContains}} {{ 'ProductName LIKE @name' | addTo: filters }} {{/if}} - -{{#if !isEmpty(filters)}} - {{ `${sql} and ${join(filters, ' and ')}` | assignTo: sql }} -{{/if}} - -{{ `${sql} ORDER BY CompanyName ${sqlLimit(limit)}` | assignTo: sql }} - -{{ sql | dbSelect({ id, category, supplier, name: `%${nameContains}%` }) - | return }} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/api-pages/04.html b/src/wwwroot/gfm/api-pages/04.html deleted file mode 100644 index b26a9b4..0000000 --- a/src/wwwroot/gfm/api-pages/04.html +++ /dev/null @@ -1,118 +0,0 @@ -
    [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) => new HelloResponse { Result = $"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 -that includes the 1-liner:

    -
    {{ { 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:

    - -

    Or calling it with a JSON HTTP client containing Accept: application/json HTTP Header or with a ?format=json query string will -render the API response in the JSON Format:

    - -

    Alternatively you can force a JSON Response by specifying the Content Type in the return arguments:

    -
    {{ { result: `Hello, ${name}!` } | return({ format: 'json' }) }} 
    -// Equivalent to:
    -{{ { result: `Hello, ${name}!` } | return({ contentType: 'application/json' }) }}
    -

    More API examples showing the versatility of this feature is contained in the new blog.web-app.io which only uses -Templates and Dynamic API Pages to implement all of its functionality.

    -

    -/preview API Page

    -
    -

    Usage: /preview?content={templates}

    -
    -

    The /preview.html API page uses this to force a plain-text response with:

    -
    {{ content  | evalTemplate({use:{plugins:'MarkdownTemplatePlugin'}}) | assignTo:response }}
    -{{ response | return({ contentType:'text/plain' }) }}
    -

    The preview API above is what provides the new Blog Web App's Live Preview feature where it will render any -ServiceStack Templates provided in the content Query String or HTTP Post Form Data, e.g:

    - -

    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 API page shows an example of how easy it is to -create data-driven APIs where you can literally return the response of a parameterized SQL query using the dbSelect filter and returning -the results:

    -
    {{ `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 posts with:

    - -

    Which also benefits from ServiceStack's multiple built-in formats where the same API can be returned in:

    - -

    -/posts/_slug/api Page

    -
    -

    Usage: /posts/{slug}/api

    -
    -

    The /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 })
    -   | assignTo: post 
    -}}
    -{{ post ?? httpResult({ status:404, statusDescription:'Post was not found' }) 
    -   | return }}
    -

    The httpResult filter returns a ServiceStack HttpResult which allows for the following customizations:

    -
    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 unknown arguments like 'X-Powered-By' are returned as HTTP Response Headers.

    -

    Returning the httpResult above behaves similarly to customizing a HTTP response using return arguments:

    -
    {{ post | return({ format:'json', 'X-Powered-By':'ServiceStack Templates' }) }}
    -

    Using the explicit httpResult filter is useful for returning a custom HTTP Response without a Response Body, e.g. the New Post page -uses httpFilter to -redirect back to the Users posts page -after they've successfully created a new Post:

    -
    {{#if success}}
    -    {{ httpResult({ status:301, Location:`/${userName}` }) | return }}
    -{{/if}}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/api-pages/04.md b/src/wwwroot/gfm/api-pages/04.md deleted file mode 100644 index 88064ca..0000000 --- a/src/wwwroot/gfm/api-pages/04.md +++ /dev/null @@ -1,150 +0,0 @@ -```csharp -[Route("/hello/{Name}")] -public class Hello : IReturn -{ - public string Name { get; set; } -} -public class HelloResponse -{ - public string Result { get; set; } -} -public class HelloService : Service -{ - public object Any(Hello request) => new HelloResponse { Result = $"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/NetCoreWebApps/Blog/blob/master/app/hello/_name/index.html) -that includes the 1-liner: - -```hbs -{{ { 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](http://blog.web-app.io/hello/World) - -Or calling it with a JSON HTTP client containing `Accept: application/json` HTTP Header or with a `?format=json` query string will -render the API response in the JSON Format: - - - [/hello/World?format=json](http://blog.web-app.io/hello/World?format=json) - -Alternatively you can force a JSON Response by specifying the Content Type in the return arguments: - -```hbs -{{ { result: `Hello, ${name}!` } | return({ format: 'json' }) }} -// Equivalent to: -{{ { result: `Hello, ${name}!` } | return({ contentType: 'application/json' }) }} -``` - -More API examples showing the versatility of this feature is contained in the new [blog.web-app.io](http://blog.web-app.io) which only uses -Templates and Dynamic API Pages to implement all of its functionality. - -### /preview API Page - -> Usage: /preview?content={templates} - -The [/preview.html](https://github.com/NetCoreWebApps/Blog/blob/master/app/preview.html) API page uses this to force a plain-text response with: - -```hbs -{{ content | evalTemplate({use:{plugins:'MarkdownTemplatePlugin'}}) | assignTo:response }} -{{ response | return({ contentType:'text/plain' }) }} -``` - -The preview API above is what provides the new [Blog Web App's](http://blog.web-app.io/) Live Preview feature where it will render any -ServiceStack Templates provided in the **content** Query String or HTTP Post Form Data, e.g: - -- [/preview?content={{10|times|select:{pow(index,2)},}}](http://blog.web-app.io/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/NetCoreWebApps/blog/blob/master/_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 a parameterized SQL query using the `dbSelect` filter and returning -the results: - -```hbs -{{ `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](http://blog.web-app.io/ServiceStack) posts with: - - - [/ServiceStack/api](http://blog.web-app.io/ServiceStack/api) - -Which also benefits from ServiceStack's multiple built-in formats where the same API can be returned in: - - - [/ServiceStack/api?format=json](http://blog.web-app.io/ServiceStack/api?format=json) - - [/ServiceStack/api?format=csv](http://blog.web-app.io/ServiceStack/api?format=csv) - - [/ServiceStack/api?format=xml](http://blog.web-app.io/ServiceStack/api?format=xml) - - [/ServiceStack/api?format=jsv](http://blog.web-app.io/ServiceStack/api?format=jsv) - -### /posts/_slug/api Page - -> Usage: /posts/{slug}/api - -The [/posts/_slug/api.html](https://github.com/NetCoreWebApps/blog/blob/master/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: - -```hbs -{{ `SELECT * - FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName - WHERE Slug = @slug - ORDER BY p.Created DESC` - | dbSingle({ slug }) - | assignTo: post -}} -{{ post ?? httpResult({ status:404, statusDescription:'Post was not found' }) - | return }} -``` - -The `httpResult` filter returns a ServiceStack `HttpResult` which allows for the following customizations: - -```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 unknown arguments like **'X-Powered-By'** are returned as HTTP Response Headers. - -Returning the `httpResult` above behaves similarly to customizing a HTTP response using return arguments: - -```hbs -{{ post | return({ format:'json', 'X-Powered-By':'ServiceStack Templates' }) }} -``` - -Using the explicit `httpResult` filter is useful for returning a custom HTTP Response without a Response Body, e.g. the **New Post** page -uses `httpFilter` to -[redirect back to the Users posts page](https://github.com/NetCoreWebApps/Blog/blob/e8bb7249192c5797348ced091ad5fd434db9a619/app/posts/new.html#L33) -after they've successfully created a new Post: - -```hbs -{{#if success}} - {{ httpResult({ status:301, Location:`/${userName}` }) | return }} -{{/if}} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/api-reference/01.html b/src/wwwroot/gfm/api-reference/01.html deleted file mode 100644 index 938296d..0000000 --- a/src/wwwroot/gfm/api-reference/01.html +++ /dev/null @@ -1,18 +0,0 @@ -
    var context = new TemplateContext { 
    -    Args = {
    -        [TemplateConstants.MaxQuota] = 10000,
    -        [TemplateConstants.DefaultCulture] = CultureInfo.CurrentCulture,
    -        [TemplateConstants.DefaultDateFormat] = "yyyy-MM-dd",
    -        [TemplateConstants.DefaultDateTimeFormat] = "u",
    -        [TemplateConstants.DefaultTimeFormat] = "h\\:mm\\:ss",
    -        [TemplateConstants.DefaultFileCacheExpiry] = TimeSpan.FromMinutes(1),
    -        [TemplateConstants.DefaultUrlCacheExpiry] = TimeSpan.FromMinutes(1),
    -        [TemplateConstants.DefaultIndent] = "\t",
    -        [TemplateConstants.DefaultNewLine] = Environment.NewLine,
    -        [TemplateConstants.DefaultJsConfig] = "excludetypeinfo",
    -        [TemplateConstants.DefaultStringComparison] = StringComparison.Ordinal,
    -        [TemplateConstants.DefaultTableClassName] = "table",
    -        [TemplateConstants.DefaultErrorClassName] = "alert alert-danger",
    -    }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/api-reference/01.md b/src/wwwroot/gfm/api-reference/01.md deleted file mode 100644 index 9c85bcf..0000000 --- a/src/wwwroot/gfm/api-reference/01.md +++ /dev/null @@ -1,19 +0,0 @@ -```csharp -var context = new TemplateContext { - Args = { - [TemplateConstants.MaxQuota] = 10000, - [TemplateConstants.DefaultCulture] = CultureInfo.CurrentCulture, - [TemplateConstants.DefaultDateFormat] = "yyyy-MM-dd", - [TemplateConstants.DefaultDateTimeFormat] = "u", - [TemplateConstants.DefaultTimeFormat] = "h\\:mm\\:ss", - [TemplateConstants.DefaultFileCacheExpiry] = TimeSpan.FromMinutes(1), - [TemplateConstants.DefaultUrlCacheExpiry] = TimeSpan.FromMinutes(1), - [TemplateConstants.DefaultIndent] = "\t", - [TemplateConstants.DefaultNewLine] = Environment.NewLine, - [TemplateConstants.DefaultJsConfig] = "excludetypeinfo", - [TemplateConstants.DefaultStringComparison] = StringComparison.Ordinal, - [TemplateConstants.DefaultTableClassName] = "table", - [TemplateConstants.DefaultErrorClassName] = "alert alert-danger", - } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/api-reference/02.html b/src/wwwroot/gfm/api-reference/02.html deleted file mode 100644 index fe7d819..0000000 --- a/src/wwwroot/gfm/api-reference/02.html +++ /dev/null @@ -1,10 +0,0 @@ -
    var fs = new FileSystemVirtualFiles("~/template-files".MapProjectPath());
    -foreach (var file in fs.GetAllMatchingFiles("*.html"))
    -{
    -    if (!MyAllowFile(file)) continue;
    -    using (var stream = file.OpenRead())
    -    {
    -        context.VirtualFiles.WriteFile(file.VirtualPath, stream);
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/api-reference/02.md b/src/wwwroot/gfm/api-reference/02.md deleted file mode 100644 index 686be6a..0000000 --- a/src/wwwroot/gfm/api-reference/02.md +++ /dev/null @@ -1,11 +0,0 @@ -```csharp -var fs = new FileSystemVirtualFiles("~/template-files".MapProjectPath()); -foreach (var file in fs.GetAllMatchingFiles("*.html")) -{ - if (!MyAllowFile(file)) continue; - using (var stream = file.OpenRead()) - { - context.VirtualFiles.WriteFile(file.VirtualPath, stream); - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/api-reference/03.html b/src/wwwroot/gfm/api-reference/03.html deleted file mode 100644 index 0d8f675..0000000 --- a/src/wwwroot/gfm/api-reference/03.html +++ /dev/null @@ -1,24 +0,0 @@ -
    public class MarkdownTemplatePlugin : ITemplatePlugin
    -{
    -    public bool RegisterPageFormat { get; set; } = true;
    -
    -    public void Register(TemplateContext context)
    -    {
    -        if (RegisterPageFormat)
    -            context.PageFormats.Add(new MarkdownPageFormat());
    -        
    -        context.FilterTransformers["markdown"] = MarkdownPageFormat.TransformToHtml;
    -        
    -        context.TemplateFilters.Add(new MarkdownTemplateFilter());
    -
    -        TemplateConfig.DontEvaluateBlocksNamed.Add("markdown");
    -        
    -        context.TemplateBlocks.Add(new TemplateMarkdownBlock());
    -    }
    -}
    -

    The MarkdownTemplatePlugin is pre-registered when using the ServiceStack Templates View Engine, for -all other contexts it can be registered and customized with:

    -
    var context = new TemplateContext {
    -    Plugins = { new MarkdownTemplatePlugin { RegisterPageFormat = false } }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/api-reference/03.md b/src/wwwroot/gfm/api-reference/03.md deleted file mode 100644 index e0616a4..0000000 --- a/src/wwwroot/gfm/api-reference/03.md +++ /dev/null @@ -1,29 +0,0 @@ -```csharp -public class MarkdownTemplatePlugin : ITemplatePlugin -{ - public bool RegisterPageFormat { get; set; } = true; - - public void Register(TemplateContext context) - { - if (RegisterPageFormat) - context.PageFormats.Add(new MarkdownPageFormat()); - - context.FilterTransformers["markdown"] = MarkdownPageFormat.TransformToHtml; - - context.TemplateFilters.Add(new MarkdownTemplateFilter()); - - TemplateConfig.DontEvaluateBlocksNamed.Add("markdown"); - - context.TemplateBlocks.Add(new TemplateMarkdownBlock()); - } -} -``` - -The `MarkdownTemplatePlugin` is pre-registered when using the [ServiceStack Templates View Engine](/docs/view-engine), for -all other contexts it can be registered and customized with: - -```csharp -var context = new TemplateContext { - Plugins = { new MarkdownTemplatePlugin { RegisterPageFormat = false } } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/arguments/01.html b/src/wwwroot/gfm/arguments/01.html deleted file mode 100644 index db73f34..0000000 --- a/src/wwwroot/gfm/arguments/01.html +++ /dev/null @@ -1,6 +0,0 @@ -
    var context = new TemplateContext { 
    -    Args = {
    -        ["arg"] = 1
    -    }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/arguments/01.md b/src/wwwroot/gfm/arguments/01.md deleted file mode 100644 index 77921b4..0000000 --- a/src/wwwroot/gfm/arguments/01.md +++ /dev/null @@ -1,7 +0,0 @@ -```csharp -var context = new TemplateContext { - Args = { - ["arg"] = 1 - } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/arguments/02.html b/src/wwwroot/gfm/arguments/02.html deleted file mode 100644 index 47a5659..0000000 --- a/src/wwwroot/gfm/arguments/02.html +++ /dev/null @@ -1,6 +0,0 @@ -
    var context = new PageResult(context.GetPage("page")) { 
    -    Args = {
    -        ["arg"] = 4
    -    }
    -};
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/02.html b/src/wwwroot/gfm/blocks/02.html deleted file mode 100644 index 1e9e5c9..0000000 --- a/src/wwwroot/gfm/blocks/02.html +++ /dev/null @@ -1,8 +0,0 @@ -
    public class TemplateNoopBlock : TemplateBlock
    -{
    -    public override string Name => "noop";
    -
    -    public override Task WriteAsync(TemplateScopeContext scope, PageBlockFragment block, CancellationToken ct)
    -        => Task.CompletedTask;
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/02.md b/src/wwwroot/gfm/blocks/02.md deleted file mode 100644 index f282bc0..0000000 --- a/src/wwwroot/gfm/blocks/02.md +++ /dev/null @@ -1,9 +0,0 @@ -```csharp -public class TemplateNoopBlock : TemplateBlock -{ - public override string Name => "noop"; - - public override Task WriteAsync(TemplateScopeContext scope, PageBlockFragment block, CancellationToken ct) - => Task.CompletedTask; -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/03.html b/src/wwwroot/gfm/blocks/03.html deleted file mode 100644 index 39d5343..0000000 --- a/src/wwwroot/gfm/blocks/03.html +++ /dev/null @@ -1,4 +0,0 @@ -
    var context = new TemplateContext {
    -    TemplateBlocks = { new TemplateNoopBlock() },
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/03.md b/src/wwwroot/gfm/blocks/03.md deleted file mode 100644 index 0b32a37..0000000 --- a/src/wwwroot/gfm/blocks/03.md +++ /dev/null @@ -1,5 +0,0 @@ -```csharp -var context = new TemplateContext { - TemplateBlocks = { new TemplateNoopBlock() }, -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/04.html b/src/wwwroot/gfm/blocks/04.html deleted file mode 100644 index b9b3cfa..0000000 --- a/src/wwwroot/gfm/blocks/04.html +++ /dev/null @@ -1,7 +0,0 @@ -
    var context = new TemplateContext
    -{
    -    ScanTypes = { typeof(TemplateNoopBlock) }
    -};
    -context.Container.AddSingleton<ICacheClient>(() => new MemoryCacheClient());
    -context.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/04.md b/src/wwwroot/gfm/blocks/04.md deleted file mode 100644 index 5fc92b8..0000000 --- a/src/wwwroot/gfm/blocks/04.md +++ /dev/null @@ -1,8 +0,0 @@ -```csharp -var context = new TemplateContext -{ - ScanTypes = { typeof(TemplateNoopBlock) } -}; -context.Container.AddSingleton(() => new MemoryCacheClient()); -context.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/05.html b/src/wwwroot/gfm/blocks/05.html deleted file mode 100644 index 9287396..0000000 --- a/src/wwwroot/gfm/blocks/05.html +++ /dev/null @@ -1,5 +0,0 @@ -
    var context = new TemplateContext
    -{
    -    ScanAssemblies = { typeof(MyBlock).Assembly }
    -};
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/05.md b/src/wwwroot/gfm/blocks/05.md deleted file mode 100644 index 4969acc..0000000 --- a/src/wwwroot/gfm/blocks/05.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -var context = new TemplateContext -{ - ScanAssemblies = { typeof(MyBlock).Assembly } -}; -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/06.html b/src/wwwroot/gfm/blocks/06.html deleted file mode 100644 index c356227..0000000 --- a/src/wwwroot/gfm/blocks/06.html +++ /dev/null @@ -1,13 +0,0 @@ -
    public class TemplateBoldBlock : TemplateBlock
    -{
    -    public override string Name => "bold";
    -    
    -    public override async Task WriteAsync(
    -        TemplateScopeContext scope, PageBlockFragment block, CancellationToken token)
    -    {
    -        await scope.OutputStream.WriteAsync("<b>", token);
    -        await WriteBodyAsync(scope, block, token);
    -        await scope.OutputStream.WriteAsync("</b>", token);
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/06.md b/src/wwwroot/gfm/blocks/06.md deleted file mode 100644 index 6d13cb8..0000000 --- a/src/wwwroot/gfm/blocks/06.md +++ /dev/null @@ -1,14 +0,0 @@ -```csharp -public class TemplateBoldBlock : TemplateBlock -{ - public override string Name => "bold"; - - public override async Task WriteAsync( - TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) - { - await scope.OutputStream.WriteAsync("", token); - await WriteBodyAsync(scope, block, token); - await scope.OutputStream.WriteAsync("", token); - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/07.html b/src/wwwroot/gfm/blocks/07.html deleted file mode 100644 index 0c0a738..0000000 --- a/src/wwwroot/gfm/blocks/07.html +++ /dev/null @@ -1,21 +0,0 @@ -
    public class TemplateWithBlock : TemplateBlock
    -{
    -    public override string Name => "with";
    -
    -    public override async Task WriteAsync(
    -        TemplateScopeContext scope, PageBlockFragment block, CancellationToken token)
    -    {
    -        var result = block.Argument.GetJsExpressionAndEvaluate(scope,
    -            ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression"));
    -
    -        if (result != null)
    -        {
    -            var resultAsMap = result.ToObjectDictionary();
    -
    -            var withScope = scope.ScopeWithParams(resultAsMap);
    -             
    -            await WriteBodyAsync(withScope, block, token);
    -        }
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/07.md b/src/wwwroot/gfm/blocks/07.md deleted file mode 100644 index f1b79ce..0000000 --- a/src/wwwroot/gfm/blocks/07.md +++ /dev/null @@ -1,22 +0,0 @@ -```csharp -public class TemplateWithBlock : TemplateBlock -{ - public override string Name => "with"; - - public override async Task WriteAsync( - TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) - { - var result = block.Argument.GetJsExpressionAndEvaluate(scope, - ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression")); - - if (result != null) - { - var resultAsMap = result.ToObjectDictionary(); - - var withScope = scope.ScopeWithParams(resultAsMap); - - await WriteBodyAsync(withScope, block, token); - } - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/08.md b/src/wwwroot/gfm/blocks/08.md deleted file mode 100644 index 4a3b6fb..0000000 --- a/src/wwwroot/gfm/blocks/08.md +++ /dev/null @@ -1,4 +0,0 @@ -```csharp -block.Argument.ParseJsExpression(out token); -var result = token.Evaluate(scope); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/09.html b/src/wwwroot/gfm/blocks/09.html deleted file mode 100644 index d7cd5de..0000000 --- a/src/wwwroot/gfm/blocks/09.html +++ /dev/null @@ -1,34 +0,0 @@ -
    public class TemplateWithBlock : TemplateBlock
    -{
    -    public override string Name => "with";
    -
    -    public override async Task WriteAsync(
    -        TemplateScopeContext scope, PageBlockFragment block, CancellationToken token)
    -    {
    -        var result = await block.Argument.GetJsExpressionAndEvaluateAsync(scope,
    -            ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression"));
    -
    -        if (result != null)
    -        {
    -            var resultAsMap = result.ToObjectDictionary();
    -
    -            var withScope = scope.ScopeWithParams(resultAsMap);
    -            
    -            await WriteBodyAsync(withScope, block, token);
    -        }
    -        else
    -        {
    -            await WriteElseAsync(scope, block.ElseBlocks, token);
    -        }
    -    }
    -}
    -

    This enables the with block to also evaluate async responses like the async results returned in async Database filters, -it's also able to evaluate custom else statements for rendering different results based on alternate conditions, e.g:

    -
    {{#with dbSingle("select * from Person where id = @id", { id }) }}
    -    Hi {{Name}}, your Age is {{Age}}.
    -{{else if id == 0}}
    -    id is required.
    -{{else}}
    -    No person with id {{id}} exists.
    -{{/with}}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/09.md b/src/wwwroot/gfm/blocks/09.md deleted file mode 100644 index fa52873..0000000 --- a/src/wwwroot/gfm/blocks/09.md +++ /dev/null @@ -1,39 +0,0 @@ -```csharp -public class TemplateWithBlock : TemplateBlock -{ - public override string Name => "with"; - - public override async Task WriteAsync( - TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) - { - var result = await block.Argument.GetJsExpressionAndEvaluateAsync(scope, - ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression")); - - if (result != null) - { - var resultAsMap = result.ToObjectDictionary(); - - var withScope = scope.ScopeWithParams(resultAsMap); - - await WriteBodyAsync(withScope, block, token); - } - else - { - await WriteElseAsync(scope, block.ElseBlocks, token); - } - } -} -``` - -This enables the `with` block to also evaluate async responses like the async results returned in [async Database filters](/docs/db-filters), -it's also able to evaluate custom else statements for rendering different results based on alternate conditions, e.g: - -```js -{{#with dbSingle("select * from Person where id = @id", { id }) }} - Hi {{Name}}, your Age is {{Age}}. -{{else if id == 0}} - id is required. -{{else}} - No person with id {{id}} exists. -{{/with}} -``` diff --git a/src/wwwroot/gfm/blocks/10.html b/src/wwwroot/gfm/blocks/10.html deleted file mode 100644 index 1e1d97c..0000000 --- a/src/wwwroot/gfm/blocks/10.html +++ /dev/null @@ -1,27 +0,0 @@ -
    /// <summary>
    -/// Handlebars.js like if block
    -/// Usages: {{#if a > b}} max {{a}} {{/if}}
    -///         {{#if a > b}} max {{a}} {{else}} max {{b}} {{/if}}
    -///         {{#if a > b}} max {{a}} {{else if b > c}} max {{b}} {{else}} max {{c}} {{/if}}
    -/// </summary>
    -public class TemplateIfBlock : TemplateBlock
    -{
    -    public override string Name => "if";
    -    
    -    public override async Task WriteAsync(
    -        TemplateScopeContext scope, PageBlockFragment block, CancellationToken token)
    -    {
    -        var result = await block.Argument.GetJsExpressionAndEvaluateToBoolAsync(scope,
    -            ifNone: () => throw new NotSupportedException("'if' block does not have a valid expression"));
    -
    -        if (result)
    -        {
    -            await WriteBodyAsync(scope, block, token);
    -        }
    -        else
    -        {
    -            await WriteElseAsync(scope, block.ElseBlocks, token);
    -        }
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/10.md b/src/wwwroot/gfm/blocks/10.md deleted file mode 100644 index b6bef4a..0000000 --- a/src/wwwroot/gfm/blocks/10.md +++ /dev/null @@ -1,28 +0,0 @@ -```csharp -/// -/// Handlebars.js like if block -/// Usages: {{#if a > b}} max {{a}} {{/if}} -/// {{#if a > b}} max {{a}} {{else}} max {{b}} {{/if}} -/// {{#if a > b}} max {{a}} {{else if b > c}} max {{b}} {{else}} max {{c}} {{/if}} -/// -public class TemplateIfBlock : TemplateBlock -{ - public override string Name => "if"; - - public override async Task WriteAsync( - TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) - { - var result = await block.Argument.GetJsExpressionAndEvaluateToBoolAsync(scope, - ifNone: () => throw new NotSupportedException("'if' block does not have a valid expression")); - - if (result) - { - await WriteBodyAsync(scope, block, token); - } - else - { - await WriteElseAsync(scope, block.ElseBlocks, token); - } - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/11.html b/src/wwwroot/gfm/blocks/11.html deleted file mode 100644 index e49b142..0000000 --- a/src/wwwroot/gfm/blocks/11.html +++ /dev/null @@ -1,39 +0,0 @@ -
    /// <summary>
    -/// Handlebars.js like each block
    -/// Usages: {{#each customers}} {{Name}} {{/each}}
    -///         {{#each customers}} {{it.Name}} {{/each}}
    -///         {{#each customers}} Customer {{index + 1}}: {{Name}} {{/each}}
    -///         {{#each numbers}} {{it}} {{else}} no numbers {{/each}}
    -///         {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}}
    -/// </summary>
    -public class TemplateSimpleEachBlock : TemplateBlock
    -{
    -    public override string Name => "each";
    -
    -    public override async Task WriteAsync(
    -        TemplateScopeContext scope, PageBlockFragment block, CancellationToken token)
    -    {
    -        var collection = (IEnumerable) block.Argument.GetJsExpressionAndEvaluate(scope,
    -            ifNone: () => throw new NotSupportedException("'each' block does not have a valid expression"));
    -
    -        var index = 0;
    -        if (collection != null)
    -        {
    -            foreach (var element in collection)
    -            {
    -                var scopeArgs = element.ToObjectDictionary();
    -                scopeArgs["it"] = element;
    -                scopeArgs[nameof(index)] = index++;
    -                
    -                var itemScope = scope.ScopeWithParams(scopeArgs);
    -                await WriteBodyAsync(itemScope, block, token);
    -            }
    -        }
    -        
    -        if (index == 0)
    -        {
    -            await WriteElseAsync(scope, block.ElseBlocks, token);
    -        }
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/11.md b/src/wwwroot/gfm/blocks/11.md deleted file mode 100644 index 3aea013..0000000 --- a/src/wwwroot/gfm/blocks/11.md +++ /dev/null @@ -1,40 +0,0 @@ -```csharp -/// -/// Handlebars.js like each block -/// Usages: {{#each customers}} {{Name}} {{/each}} -/// {{#each customers}} {{it.Name}} {{/each}} -/// {{#each customers}} Customer {{index + 1}}: {{Name}} {{/each}} -/// {{#each numbers}} {{it}} {{else}} no numbers {{/each}} -/// {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}} -/// -public class TemplateSimpleEachBlock : TemplateBlock -{ - public override string Name => "each"; - - public override async Task WriteAsync( - TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) - { - var collection = (IEnumerable) block.Argument.GetJsExpressionAndEvaluate(scope, - ifNone: () => throw new NotSupportedException("'each' block does not have a valid expression")); - - var index = 0; - if (collection != null) - { - foreach (var element in collection) - { - var scopeArgs = element.ToObjectDictionary(); - scopeArgs["it"] = element; - scopeArgs[nameof(index)] = index++; - - var itemScope = scope.ScopeWithParams(scopeArgs); - await WriteBodyAsync(itemScope, block, token); - } - } - - if (index == 0) - { - await WriteElseAsync(scope, block.ElseBlocks, token); - } - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/12.html b/src/wwwroot/gfm/blocks/12.html deleted file mode 100644 index 1661a36..0000000 --- a/src/wwwroot/gfm/blocks/12.html +++ /dev/null @@ -1,13 +0,0 @@ -
    /// <summary>
    -/// Handlebars.js like each block
    -/// Usages: {{#each customers}} {{Name}} {{/each}}
    -///         {{#each customers}} {{it.Name}} {{/each}}
    -///         {{#each num in numbers}} {{num}} {{/each}}
    -///         {{#each num in [1,2,3]}} {{num}} {{/each}}
    -///         {{#each numbers}} {{it}} {{else}} no numbers {{/each}}
    -///         {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}}
    -///         {{#each n in numbers where n > 5}} {{it}} {{else}} no numbers > 5 {{/each}}
    -///         {{#each n in numbers where n > 5 orderby n skip 1 take 2}} {{it}} {{else}}no numbers > 5{{/each}}
    -/// </summary>
    -public class TemplateEachBlock : TemplateBlock { ... }
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/12.md b/src/wwwroot/gfm/blocks/12.md deleted file mode 100644 index 7486e57..0000000 --- a/src/wwwroot/gfm/blocks/12.md +++ /dev/null @@ -1,14 +0,0 @@ -```csharp -/// -/// Handlebars.js like each block -/// Usages: {{#each customers}} {{Name}} {{/each}} -/// {{#each customers}} {{it.Name}} {{/each}} -/// {{#each num in numbers}} {{num}} {{/each}} -/// {{#each num in [1,2,3]}} {{num}} {{/each}} -/// {{#each numbers}} {{it}} {{else}} no numbers {{/each}} -/// {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}} -/// {{#each n in numbers where n > 5}} {{it}} {{else}} no numbers > 5 {{/each}} -/// {{#each n in numbers where n > 5 orderby n skip 1 take 2}} {{it}} {{else}}no numbers > 5{{/each}} -/// -public class TemplateEachBlock : TemplateBlock { ... } -``` diff --git a/src/wwwroot/gfm/blocks/13.html b/src/wwwroot/gfm/blocks/13.html deleted file mode 100644 index 91dca16..0000000 --- a/src/wwwroot/gfm/blocks/13.html +++ /dev/null @@ -1,50 +0,0 @@ -
    /// <summary>
    -/// Special block which captures the raw body as a string fragment
    -///
    -/// Usages: {{#raw}}emit {{ verbatim }} body{{/raw}}
    -///         {{#raw varname}}assigned to varname{{/raw}}
    -///         {{#raw appendTo varname}}appended to varname{{/raw}}
    -/// </summary>
    -public class TemplateRawBlock : TemplateBlock
    -{
    -    public override string Name => "raw";
    -    
    -    public override async Task WriteAsync(
    -        TemplateScopeContext scope, PageBlockFragment block, CancellationToken token)
    -    {
    -        var strFragment = (PageStringFragment)block.Body[0];
    -
    -        if (!block.Argument.IsNullOrWhiteSpace())
    -        {
    -            Capture(scope, block, strFragment);
    -        }
    -        else
    -        {
    -            await scope.OutputStream.WriteAsync(strFragment.Value.Span, token);
    -        }
    -    }
    -
    -    private static void Capture(
    -        TemplateScopeContext scope, PageBlockFragment block, PageStringFragment strFragment)
    -    {
    -        var literal = block.Argument.Span.AdvancePastWhitespace();
    -        bool appendTo = false;
    -        if (literal.StartsWith("appendTo "))
    -        {
    -            appendTo = true;
    -            literal = literal.Advance("appendTo ".Length);
    -        }
    -
    -        literal = literal.ParseVarName(out var name);
    -        var nameString = name.Value();
    -        if (appendTo && scope.PageResult.Args.TryGetValue(nameString, out var oVar)
    -                        && oVar is string existingString)
    -        {
    -            scope.PageResult.Args[nameString] = existingString + strFragment.Value;
    -            return;
    -        }
    -
    -        scope.PageResult.Args[nameString] = strFragment.Value.ToString();
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/13.md b/src/wwwroot/gfm/blocks/13.md deleted file mode 100644 index 4dbc014..0000000 --- a/src/wwwroot/gfm/blocks/13.md +++ /dev/null @@ -1,51 +0,0 @@ -```csharp -/// -/// Special block which captures the raw body as a string fragment -/// -/// Usages: {{#raw}}emit {{ verbatim }} body{{/raw}} -/// {{#raw varname}}assigned to varname{{/raw}} -/// {{#raw appendTo varname}}appended to varname{{/raw}} -/// -public class TemplateRawBlock : TemplateBlock -{ - public override string Name => "raw"; - - public override async Task WriteAsync( - TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) - { - var strFragment = (PageStringFragment)block.Body[0]; - - if (!block.Argument.IsNullOrWhiteSpace()) - { - Capture(scope, block, strFragment); - } - else - { - await scope.OutputStream.WriteAsync(strFragment.Value.Span, token); - } - } - - private static void Capture( - TemplateScopeContext scope, PageBlockFragment block, PageStringFragment strFragment) - { - var literal = block.Argument.Span.AdvancePastWhitespace(); - bool appendTo = false; - if (literal.StartsWith("appendTo ")) - { - appendTo = true; - literal = literal.Advance("appendTo ".Length); - } - - literal = literal.ParseVarName(out var name); - var nameString = name.Value(); - if (appendTo && scope.PageResult.Args.TryGetValue(nameString, out var oVar) - && oVar is string existingString) - { - scope.PageResult.Args[nameString] = existingString + strFragment.Value; - return; - } - - scope.PageResult.Args[nameString] = strFragment.Value.ToString(); - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/14.html b/src/wwwroot/gfm/blocks/14.html deleted file mode 100644 index e821048..0000000 --- a/src/wwwroot/gfm/blocks/14.html +++ /dev/null @@ -1,69 +0,0 @@ -
    /// <summary>
    -/// Captures the output and assigns it to the specified variable.
    -/// Accepts an optional Object Dictionary as scope arguments when evaluating body.
    -/// Effectively is similar 
    -///
    -/// Usages: {{#capture output}} {{#each args}} - [{{it}}](/path?arg={{it}}) {{/each}} {{/capture}}
    -///         {{#capture output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}}
    -///         {{#capture appendTo output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}}
    -/// </summary>
    -public class TemplateCaptureBlock : TemplateBlock
    -{
    -    public override string Name => "capture";
    -
    -    public override async Task WriteAsync(
    -        TemplateScopeContext scope, PageBlockFragment block, CancellationToken token)
    -    {
    -        var (name, scopeArgs, appendTo) = Parse(scope, block);
    -
    -        using (var ms = MemoryStreamFactory.GetStream())
    -        {
    -            var useScope = scope.ScopeWith(scopeArgs, ms);
    -
    -            await WriteBodyAsync(useScope, block, token);
    -
    -            var capturedOutput = ms.ReadToEnd();
    -
    -            if (appendTo && scope.PageResult.Args.TryGetValue(name, out var oVar)
    -                         && oVar is string existingString)
    -            {
    -                scope.PageResult.Args[name] = existingString + capturedOutput;
    -                return;
    -            }
    -        
    -            scope.PageResult.Args[name] = capturedOutput;
    -        }
    -    }
    -
    -    //Extract usages of Span outside of async method 
    -    private (string name, Dictionary<string, object> scopeArgs, bool appendTo) 
    -        Parse(TemplateScopeContext scope, PageBlockFragment block)
    -    {
    -        if (block.Argument.IsNullOrWhiteSpace())
    -            throw new NotSupportedException("'capture' block is missing variable name to assign output to");
    -        
    -        var literal = block.Argument.AdvancePastWhitespace();
    -        bool appendTo = false;
    -        if (literal.StartsWith("appendTo "))
    -        {
    -            appendTo = true;
    -            literal = literal.Advance("appendTo ".Length);
    -        }
    -            
    -        literal = literal.ParseVarName(out var name);
    -        if (name.IsNullOrEmpty())
    -            throw new NotSupportedException("'capture' block is missing variable name to assign output to");
    -
    -        literal = literal.AdvancePastWhitespace();
    -
    -        var argValue = literal.GetJsExpressionAndEvaluate(scope);
    -
    -        var scopeArgs = argValue as Dictionary<string, object>;
    -
    -        if (argValue != null && scopeArgs == null)
    -            throw new NotSupportedException("Any 'capture' argument must be an Object Dictionary");
    -
    -        return (name.ToString(), scopeArgs, appendTo);
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/14.md b/src/wwwroot/gfm/blocks/14.md deleted file mode 100644 index 6f09ef2..0000000 --- a/src/wwwroot/gfm/blocks/14.md +++ /dev/null @@ -1,70 +0,0 @@ -```csharp -/// -/// Captures the output and assigns it to the specified variable. -/// Accepts an optional Object Dictionary as scope arguments when evaluating body. -/// Effectively is similar -/// -/// Usages: {{#capture output}} {{#each args}} - [{{it}}](/path?arg={{it}}) {{/each}} {{/capture}} -/// {{#capture output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}} -/// {{#capture appendTo output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}} -/// -public class TemplateCaptureBlock : TemplateBlock -{ - public override string Name => "capture"; - - public override async Task WriteAsync( - TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) - { - var (name, scopeArgs, appendTo) = Parse(scope, block); - - using (var ms = MemoryStreamFactory.GetStream()) - { - var useScope = scope.ScopeWith(scopeArgs, ms); - - await WriteBodyAsync(useScope, block, token); - - var capturedOutput = ms.ReadToEnd(); - - if (appendTo && scope.PageResult.Args.TryGetValue(name, out var oVar) - && oVar is string existingString) - { - scope.PageResult.Args[name] = existingString + capturedOutput; - return; - } - - scope.PageResult.Args[name] = capturedOutput; - } - } - - //Extract usages of Span outside of async method - private (string name, Dictionary scopeArgs, bool appendTo) - Parse(TemplateScopeContext scope, PageBlockFragment block) - { - if (block.Argument.IsNullOrWhiteSpace()) - throw new NotSupportedException("'capture' block is missing variable name to assign output to"); - - var literal = block.Argument.AdvancePastWhitespace(); - bool appendTo = false; - if (literal.StartsWith("appendTo ")) - { - appendTo = true; - literal = literal.Advance("appendTo ".Length); - } - - literal = literal.ParseVarName(out var name); - if (name.IsNullOrEmpty()) - throw new NotSupportedException("'capture' block is missing variable name to assign output to"); - - literal = literal.AdvancePastWhitespace(); - - var argValue = literal.GetJsExpressionAndEvaluate(scope); - - var scopeArgs = argValue as Dictionary; - - if (argValue != null && scopeArgs == null) - throw new NotSupportedException("Any 'capture' argument must be an Object Dictionary"); - - return (name.ToString(), scopeArgs, appendTo); - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/15.html b/src/wwwroot/gfm/blocks/15.html deleted file mode 100644 index 2face59..0000000 --- a/src/wwwroot/gfm/blocks/15.html +++ /dev/null @@ -1,9 +0,0 @@ -
    {{#capture todoMarkdown { items:[1,2,3] } }}
    -## TODO List
    -{{#each items}}
    -  - Item {{it}}
    -{{/each}}
    -{{/capture}}
    -
    -{{todoMarkdown | markdown}}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/15.md b/src/wwwroot/gfm/blocks/15.md deleted file mode 100644 index dcc2ac7..0000000 --- a/src/wwwroot/gfm/blocks/15.md +++ /dev/null @@ -1,10 +0,0 @@ -```js -{{#capture todoMarkdown { items:[1,2,3] } }} -## TODO List -{{#each items}} - - Item {{it}} -{{/each}} -{{/capture}} - -{{todoMarkdown | markdown}} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/16.html b/src/wwwroot/gfm/blocks/16.html deleted file mode 100644 index 52a5dd6..0000000 --- a/src/wwwroot/gfm/blocks/16.html +++ /dev/null @@ -1,55 +0,0 @@ -
    /// <summary>
    -/// Converts markdown contents to HTML using the configured MarkdownConfig.Transformer.
    -/// If a variable name is specified the HTML output is captured and saved instead. 
    -///
    -/// Usages: {{#markdown}} ## The Heading {{/markdown}}
    -///         {{#markdown content}} ## The Heading {{/markdown}} HTML: {{content}}
    -/// </summary>
    -public class TemplateMarkdownBlock : TemplateBlock
    -{
    -    public override string Name => "markdown";
    -    
    -    public override async Task WriteAsync(
    -        TemplateScopeContext scope, PageBlockFragment block, CancellationToken token)
    -    {
    -        var strFragment = (PageStringFragment)block.Body[0];
    -
    -        if (!block.Argument.IsNullOrWhiteSpace())
    -        {
    -            Capture(scope, block, strFragment);
    -        }
    -        else
    -        {
    -            await scope.OutputStream.WriteAsync(MarkdownConfig.Transform(strFragment.ValueString), token);
    -        }
    -    }
    -
    -    private static void Capture(
    -        TemplateScopeContext scope, PageBlockFragment block, PageStringFragment strFragment)
    -    {
    -        var literal = block.Argument.AdvancePastWhitespace();
    -
    -        literal = literal.ParseVarName(out var name);
    -        var nameString = name.ToString();
    -        scope.PageResult.Args[nameString] = MarkdownConfig.Transform(strFragment.ValueString).ToRawString();
    -    }
    -}
    -

    -Use Alternative Markdown Implementation

    -

    By default ServiceStack uses an interned implementation of MarkdownDeep for rendering markdown, you can get ServiceStack to use an alternate -Markdown implementation by overriding MarkdownConfig.Transformer.

    -

    E.g. to use the richer Markdig implementation, install the Markdig -NuGet package:

    -
    PM> Install-Package Markdig
    -
    -

    Then assign a custom IMarkdownTransformer:

    -
    public class MarkdigTransformer : IMarkdownTransformer
    -{
    -    private Markdig.MarkdownPipeline Pipeline { get; } = 
    -        Markdig.MarkdownExtensions.UseAdvancedExtensions(new Markdig.MarkdownPipelineBuilder()).Build();
    -
    -    public string Transform(string markdown) => Markdig.Markdown.ToHtml(markdown, Pipeline);
    -}
    -
    -MarkdownConfig.Transformer = new MarkdigTransformer();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/16.md b/src/wwwroot/gfm/blocks/16.md deleted file mode 100644 index 9460131..0000000 --- a/src/wwwroot/gfm/blocks/16.md +++ /dev/null @@ -1,62 +0,0 @@ -```csharp -/// -/// Converts markdown contents to HTML using the configured MarkdownConfig.Transformer. -/// If a variable name is specified the HTML output is captured and saved instead. -/// -/// Usages: {{#markdown}} ## The Heading {{/markdown}} -/// {{#markdown content}} ## The Heading {{/markdown}} HTML: {{content}} -/// -public class TemplateMarkdownBlock : TemplateBlock -{ - public override string Name => "markdown"; - - public override async Task WriteAsync( - TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) - { - var strFragment = (PageStringFragment)block.Body[0]; - - if (!block.Argument.IsNullOrWhiteSpace()) - { - Capture(scope, block, strFragment); - } - else - { - await scope.OutputStream.WriteAsync(MarkdownConfig.Transform(strFragment.ValueString), token); - } - } - - private static void Capture( - TemplateScopeContext scope, PageBlockFragment block, PageStringFragment strFragment) - { - var literal = block.Argument.AdvancePastWhitespace(); - - literal = literal.ParseVarName(out var name); - var nameString = name.ToString(); - scope.PageResult.Args[nameString] = MarkdownConfig.Transform(strFragment.ValueString).ToRawString(); - } -} -``` - -### Use Alternative Markdown Implementation - -By default ServiceStack uses an interned implementation of `MarkdownDeep` for rendering markdown, you can get ServiceStack to use an alternate -Markdown implementation by overriding `MarkdownConfig.Transformer`. - -E.g. to use the richer [Markdig](https://github.com/lunet-io/markdig) implementation, install the [Markdig](https://www.nuget.org/packages/Markdig/) -NuGet package: - - PM> Install-Package Markdig - -Then assign a custom `IMarkdownTransformer`: - -```csharp -public class MarkdigTransformer : IMarkdownTransformer -{ - private Markdig.MarkdownPipeline Pipeline { get; } = - Markdig.MarkdownExtensions.UseAdvancedExtensions(new Markdig.MarkdownPipelineBuilder()).Build(); - - public string Transform(string markdown) => Markdig.Markdown.ToHtml(markdown, Pipeline); -} - -MarkdownConfig.Transformer = new MarkdigTransformer(); -``` diff --git a/src/wwwroot/gfm/blocks/18.html b/src/wwwroot/gfm/blocks/18.html deleted file mode 100644 index 019774e..0000000 --- a/src/wwwroot/gfm/blocks/18.html +++ /dev/null @@ -1,7 +0,0 @@ -
    {{#ul {if:hasAccess, each:items, where:'Age > 27', 
    -        class:['nav', !disclaimerAccepted ? 'blur' : ''], id:`menu-${id}`, selected:true} }}
    -    {{#li {class: {alt:isOdd(index), active:Name==highlight} }} {{Name}} {{/li}}
    -{{else}}
    -    <div>no items</div>
    -{{/ul}}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/20.md b/src/wwwroot/gfm/blocks/20.md deleted file mode 100644 index c35985b..0000000 --- a/src/wwwroot/gfm/blocks/20.md +++ /dev/null @@ -1,17 +0,0 @@ -```csharp -var context = new TemplateContext() - .RemovePlugins(x => x is TemplateDefaultBlocks) // Remove default blocks - .RemovePlugins(x => x is TemplateHtmlBlocks) // Remove all html blocks - .Init(); -``` - -Or you can use the `OnAfterPlugins` callback to remove any individual blocks or filters that were added by any plugin. - -E.g. the `capture` block can be removed with: - -```csharp -var context = new TemplateContext { - OnAfterPlugins = ctx => ctx.RemoveBlocks(x => x.Name == "capture") - } - .Init(); -``` diff --git a/src/wwwroot/gfm/code-pages/01.html b/src/wwwroot/gfm/code-pages/01.html deleted file mode 100644 index a70a764..0000000 --- a/src/wwwroot/gfm/code-pages/01.html +++ /dev/null @@ -1,30 +0,0 @@ -
    using System.Linq;
    -using System.Collections.Generic;
    -using ServiceStack;
    -using ServiceStack.Templates;
    -
    -namespace TemplatePages
    -{
    -    [Page("products")]
    -    [PageArg("title", "Products")]
    -    public class ProductsPage : TemplateCodePage
    -    {
    -        string render(Product[] products) => $@"
    -        <table class='table table-striped'>
    -            <thead>
    -                <tr>
    -                    <th></th>
    -                    <th>Name</th>
    -                    <th>Price</th>
    -                </tr>
    -            </thead>
    -            {products.OrderBy(x => x.Category).ThenBy(x => x.ProductName).Map(x => $@"
    -                <tr>
    -                    <th>{x.Category}</th>
    -                    <td>{x.ProductName.HtmlEncode()}</td>
    -                    <td>{x.UnitPrice:C}</td>
    -                </tr>").Join("")}
    -        </table>";
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/01.md b/src/wwwroot/gfm/code-pages/01.md deleted file mode 100644 index d349887..0000000 --- a/src/wwwroot/gfm/code-pages/01.md +++ /dev/null @@ -1,31 +0,0 @@ -```csharp -using System.Linq; -using System.Collections.Generic; -using ServiceStack; -using ServiceStack.Templates; - -namespace TemplatePages -{ - [Page("products")] - [PageArg("title", "Products")] - public class ProductsPage : TemplateCodePage - { - string render(Product[] products) => $@" - - - - - - - - - {products.OrderBy(x => x.Category).ThenBy(x => x.ProductName).Map(x => $@" - - - - - ").Join("")} -
    NamePrice
    {x.Category}{x.ProductName.HtmlEncode()}{x.UnitPrice:C}
    "; - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/02.html b/src/wwwroot/gfm/code-pages/02.html deleted file mode 100644 index 26ed8a7..0000000 --- a/src/wwwroot/gfm/code-pages/02.html +++ /dev/null @@ -1,22 +0,0 @@ -
    using ServiceStack;
    -using ServiceStack.Templates;
    -
    -namespace TemplatePages
    -{
    -    [Route("/products/view")]
    -    public class ViewProducts
    -    {
    -        public string Id { get; set; }
    -    }
    -
    -    public class ProductsServices : Service
    -    {
    -        public object Any(ViewProducts request) =>
    -            new PageResult(Request.GetCodePage("products")) {
    -                Args = {
    -                    ["products"] = TemplateQueryData.Products
    -                }
    -            };
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/02.md b/src/wwwroot/gfm/code-pages/02.md deleted file mode 100644 index 1d7a4e9..0000000 --- a/src/wwwroot/gfm/code-pages/02.md +++ /dev/null @@ -1,23 +0,0 @@ -```csharp -using ServiceStack; -using ServiceStack.Templates; - -namespace TemplatePages -{ - [Route("/products/view")] - public class ViewProducts - { - public string Id { get; set; } - } - - public class ProductsServices : Service - { - public object Any(ViewProducts request) => - new PageResult(Request.GetCodePage("products")) { - Args = { - ["products"] = TemplateQueryData.Products - } - }; - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/03.html b/src/wwwroot/gfm/code-pages/03.html deleted file mode 100644 index 04ad6fa..0000000 --- a/src/wwwroot/gfm/code-pages/03.html +++ /dev/null @@ -1,9 +0,0 @@ -
    public ITemplatePages Pages { get; set; }
    -
    -public object Any(ViewProducts request) 
    -{
    -    var codePage = Pages.GetCodePage("products");
    -    (codePage as IRequiresRequest)?.Request = Request;
    -    return new PageResult(codePage) { ... };
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/03.md b/src/wwwroot/gfm/code-pages/03.md deleted file mode 100644 index 933ca1c..0000000 --- a/src/wwwroot/gfm/code-pages/03.md +++ /dev/null @@ -1,10 +0,0 @@ -```csharp -public ITemplatePages Pages { get; set; } - -public object Any(ViewProducts request) -{ - var codePage = Pages.GetCodePage("products"); - (codePage as IRequiresRequest)?.Request = Request; - return new PageResult(codePage) { ... }; -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/04.html b/src/wwwroot/gfm/code-pages/04.html deleted file mode 100644 index f2b40a4..0000000 --- a/src/wwwroot/gfm/code-pages/04.html +++ /dev/null @@ -1,49 +0,0 @@ -
    using System.Linq;
    -using System.Collections.Generic;
    -using ServiceStack;
    -using ServiceStack.Templates;
    -
    -namespace TemplatePages
    -{
    -    [Page("navLinks")]
    -    public class NavLinksPartial : TemplateCodePage
    -    {
    -        string render(string PathInfo, Dictionary<string, object> links) => $@"
    -        <ul>
    -            {links.Map(entry => $@"<li class='{GetClass(PathInfo, entry.Key)}'>
    -                <a href='{entry.Key}'>{entry.Value}</a>
    -            </li>").Join("")}
    -        </ul>";
    -
    -        string GetClass(string pathInfo, string url) => url == pathInfo ? "active" : ""; 
    -    }
    -
    -    [Page("customerCard")]
    -    public class CustomerCardPartial : TemplateCodePage
    -    {
    -        public ICustomers Customers { get; set; }
    -
    -        string render(string customerId) => renderCustomer(Customers.GetCustomer(customerId));
    -
    -        string renderCustomer(Customer customer) => $@"
    -        <table class='table table-bordered'>
    -            <caption>{customer.CompanyName}</caption>
    -            <thead class='thead-inverse'>
    -                <tr>
    -                    <th>Address</th>
    -                    <th>Phone</th>
    -                    <th>Fax</th>
    -                </tr>
    -            </thead>
    -            <tr>
    -                <td>
    -                    {customer.Address} 
    -                    {customer.City}, {customer.PostalCode}, {customer.Country}
    -                </td>
    -                <td>{customer.Phone}</td>
    -                <td>{customer.Fax}</td>
    -            </tr>
    -        </table>";
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/04.md b/src/wwwroot/gfm/code-pages/04.md deleted file mode 100644 index c89ebc0..0000000 --- a/src/wwwroot/gfm/code-pages/04.md +++ /dev/null @@ -1,50 +0,0 @@ -```csharp -using System.Linq; -using System.Collections.Generic; -using ServiceStack; -using ServiceStack.Templates; - -namespace TemplatePages -{ - [Page("navLinks")] - public class NavLinksPartial : TemplateCodePage - { - string render(string PathInfo, Dictionary links) => $@" - "; - - string GetClass(string pathInfo, string url) => url == pathInfo ? "active" : ""; - } - - [Page("customerCard")] - public class CustomerCardPartial : TemplateCodePage - { - public ICustomers Customers { get; set; } - - string render(string customerId) => renderCustomer(Customers.GetCustomer(customerId)); - - string renderCustomer(Customer customer) => $@" - - - - - - - - - - - - - - -
    {customer.CompanyName}
    AddressPhoneFax
    - {customer.Address} - {customer.City}, {customer.PostalCode}, {customer.Country} - {customer.Phone}{customer.Fax}
    "; - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/05.html b/src/wwwroot/gfm/code-pages/05.html deleted file mode 100644 index 336e4f4..0000000 --- a/src/wwwroot/gfm/code-pages/05.html +++ /dev/null @@ -1,47 +0,0 @@ -
    using System.Linq;
    -using System.Collections.Generic;
    -using ServiceStack;
    -using ServiceStack.Templates;
    -
    -namespace TemplatePages
    -{
    -    [Page("navLinks")]
    -    public class NavLinksPartial : ServiceStackCodePage
    -    {
    -        string render(Dictionary<string, object> links) => $@"
    -        <ul>
    -            {links.Map(entry => $@"<li class='{GetClass(entry.Key)}'>
    -                <a href='{entry.Key}'>{entry.Value}</a>
    -            </li>").Join("")}
    -        </ul>";
    -
    -        string GetClass(string url) => url == Request.PathInfo ? "active" : ""; 
    -    }
    -
    -    [Page("customerCard")]
    -    public class CustomerCardPartial : ServiceStackCodePage
    -    {
    -        string render(string customerId) => renderCustomer(Db.SingleById<Customer>(customerId));
    -
    -        string renderCustomer(Customer customer) => $@"
    -        <table class='table table-bordered'>
    -            <caption>{customer.CompanyName}</caption>
    -            <thead class='thead-inverse'>
    -                <tr>
    -                    <th>Address</th>
    -                    <th>Phone</th>
    -                    <th>Fax</th>
    -                </tr>
    -            </thead>
    -            <tr>
    -                <td>
    -                    {customer.Address} 
    -                    {customer.City}, {customer.PostalCode}, {customer.Country}
    -                </td>
    -                <td>{customer.Phone}</td>
    -                <td>{customer.Fax}</td>
    -            </tr>
    -        </table>";
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/code-pages/05.md b/src/wwwroot/gfm/code-pages/05.md deleted file mode 100644 index 5282102..0000000 --- a/src/wwwroot/gfm/code-pages/05.md +++ /dev/null @@ -1,48 +0,0 @@ -```csharp -using System.Linq; -using System.Collections.Generic; -using ServiceStack; -using ServiceStack.Templates; - -namespace TemplatePages -{ - [Page("navLinks")] - public class NavLinksPartial : ServiceStackCodePage - { - string render(Dictionary links) => $@" - "; - - string GetClass(string url) => url == Request.PathInfo ? "active" : ""; - } - - [Page("customerCard")] - public class CustomerCardPartial : ServiceStackCodePage - { - string render(string customerId) => renderCustomer(Db.SingleById(customerId)); - - string renderCustomer(Customer customer) => $@" - - - - - - - - - - - - - - -
    {customer.CompanyName}
    AddressPhoneFax
    - {customer.Address} - {customer.City}, {customer.PostalCode}, {customer.Country} - {customer.Phone}{customer.Fax}
    "; - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/db-filters/01.html b/src/wwwroot/gfm/db-filters/01.html deleted file mode 100644 index 4d55da7..0000000 --- a/src/wwwroot/gfm/db-filters/01.html +++ /dev/null @@ -1,3 +0,0 @@ -
    container.AddSingleton<IDbConnectionFactory>(() => 
    -    new OrmLiteConnectionFactory(connectionString, SqlServer2012Dialect.Provider));
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/db-filters/01.md b/src/wwwroot/gfm/db-filters/01.md deleted file mode 100644 index 52e1de6..0000000 --- a/src/wwwroot/gfm/db-filters/01.md +++ /dev/null @@ -1,4 +0,0 @@ -```csharp -container.AddSingleton(() => - new OrmLiteConnectionFactory(connectionString, SqlServer2012Dialect.Provider)); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/db-filters/02.html b/src/wwwroot/gfm/db-filters/02.html deleted file mode 100644 index 653bfa6..0000000 --- a/src/wwwroot/gfm/db-filters/02.html +++ /dev/null @@ -1,6 +0,0 @@ -
    var context = new TemplateContext { 
    -    TemplateFilters = {
    -        new TemplateDbFiltersAsync()
    -    }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/db-filters/02.md b/src/wwwroot/gfm/db-filters/02.md deleted file mode 100644 index 3b7bb5c..0000000 --- a/src/wwwroot/gfm/db-filters/02.md +++ /dev/null @@ -1,7 +0,0 @@ -```csharp -var context = new TemplateContext { - TemplateFilters = { - new TemplateDbFiltersAsync() - } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/default-filters/01.html b/src/wwwroot/gfm/default-filters/01.html deleted file mode 100644 index 7bf7a56..0000000 --- a/src/wwwroot/gfm/default-filters/01.html +++ /dev/null @@ -1,6 +0,0 @@ -
    var context = new TemplateContext {
    -    Args = {
    -        [TemplateConstants.DefaultDateFormat] = "yyyy-MM-dd HH:mm:ss"
    -    }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/default-filters/01.md b/src/wwwroot/gfm/default-filters/01.md deleted file mode 100644 index f237260..0000000 --- a/src/wwwroot/gfm/default-filters/01.md +++ /dev/null @@ -1,7 +0,0 @@ -```csharp -var context = new TemplateContext { - Args = { - [TemplateConstants.DefaultDateFormat] = "yyyy-MM-dd HH:mm:ss" - } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/deploying-web-apps/01.md b/src/wwwroot/gfm/deploying-web-apps/01.md deleted file mode 100644 index 319998a..0000000 --- a/src/wwwroot/gfm/deploying-web-apps/01.md +++ /dev/null @@ -1,13 +0,0 @@ -```bash -cat ../apps/bare/app.settings | sed "/debug/s/ .*/ false/" > ../apps/web/web.bare.settings -cat ../apps/blog/app.settings | sed "/debug/s/ .*/ false/" > ../apps/web/web.blog.settings -cat ../apps/redis/app.settings | sed "/debug/s/ .*/ false/" > ../apps/web/web.redis.settings -cat ../apps/rockwind/web.sqlite.settings | sed "/debug/s/ .*/ false/" > ../apps/web/web.rockwind-sqlite.settings -cat ../apps/rockwind-vfs/web.sqlite.settings | sed "/debug/s/ .*/ false/" > ../apps/web/web.rockwind-vfs-sqlite.settings -cat ../apps/plugins/app.settings | sed "/debug/s/ .*/ false/" > ../apps/web/web.plugins.settings -cat ../apps/chat/web.release.settings > ../apps/web/web.chat.settings - -rsync -avz -e 'ssh' ../apps deploy@web-app.io:/home/deploy - -ssh deploy@web-app.io "sudo supervisorctl restart all" -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/deploying-web-apps/02.html b/src/wwwroot/gfm/deploying-web-apps/02.html deleted file mode 100644 index f08d0ac..0000000 --- a/src/wwwroot/gfm/deploying-web-apps/02.html +++ /dev/null @@ -1,13 +0,0 @@ -
    FROM microsoft/dotnet:2.1-sdk AS build-env
    -COPY app /app
    -WORKDIR /app
    -RUN dotnet tool install -g web
    -
    -# Build runtime image
    -FROM microsoft/dotnet:2.1-aspnetcore-runtime
    -WORKDIR /app
    -COPY --from=build-env /app app
    -COPY --from=build-env /root/.dotnet/tools tools
    -ENV ASPNETCORE_URLS http://*:5000
    -ENTRYPOINT ["/app/tools/web", "app/app.settings"]
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/deploying-web-apps/02.md b/src/wwwroot/gfm/deploying-web-apps/02.md deleted file mode 100644 index 487c77e..0000000 --- a/src/wwwroot/gfm/deploying-web-apps/02.md +++ /dev/null @@ -1,14 +0,0 @@ -```dockerfile -FROM microsoft/dotnet:2.1-sdk AS build-env -COPY app /app -WORKDIR /app -RUN dotnet tool install -g web - -# Build runtime image -FROM microsoft/dotnet:2.1-aspnetcore-runtime -WORKDIR /app -COPY --from=build-env /app app -COPY --from=build-env /root/.dotnet/tools tools -ENV ASPNETCORE_URLS http://*:5000 -ENTRYPOINT ["/app/tools/web", "app/app.settings"] -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/deploying-web-apps/03.md b/src/wwwroot/gfm/deploying-web-apps/03.md deleted file mode 100644 index 47e4f69..0000000 --- a/src/wwwroot/gfm/deploying-web-apps/03.md +++ /dev/null @@ -1,15 +0,0 @@ -```bash -#!/bin/bash - -# set environment variables used in deploy.sh and AWS task-definition.json: -export IMAGE_NAME=netcoreapps-rockwind-aws -export IMAGE_VERSION=latest - -export AWS_DEFAULT_REGION=us-east-1 -export AWS_ECS_CLUSTER_NAME=default -export AWS_VIRTUAL_HOST=rockwind-aws.web-app.io - -# set any sensitive information in travis-ci encrypted project settings: -# required: AWS_ACCOUNT_ID, AWS_ACCESS_KEY, AWS_SECRET_KEY -# optional: SERVICESTACK_LICENSE -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/email-templates/01.html b/src/wwwroot/gfm/email-templates/01.html deleted file mode 100644 index d4e5ac3..0000000 --- a/src/wwwroot/gfm/email-templates/01.html +++ /dev/null @@ -1,56 +0,0 @@ -
    using System.Linq;
    -using System.Collections.Generic;
    -using ServiceStack;
    -using ServiceStack.Templates;
    -using ServiceStack.IO;
    -
    -namespace TemplatePages
    -{
    -    [Route("/emails/order-confirmation/preview")]
    -    public class PreviewHtmlEmail : IReturn<PreviewHtmlEmailResponse>
    -    {
    -        public string EmailTemplate { get; set; }
    -        public string HtmlTemplate { get; set; }
    -        public string PreviewCustomerId { get; set; }
    -    }
    -
    -    public class PreviewHtmlEmailResponse 
    -    {
    -        public string HtmlEmail { get; set; }
    -        public string TextEmail { get; set; }
    -    }
    -
    -    public class EmailTemplatesServices : Service
    -    {
    -        public ICustomers Customers { get; set; }
    -
    -        public object Any(PreviewHtmlEmail request)
    -        {
    -            var customer = Customers.GetCustomer(request.PreviewCustomerId) 
    -                ?? Customers.GetAllCustomers().First();
    -
    -            var context = new TemplateContext {
    -                PageFormats = { new MarkdownPageFormat() },
    -                Args = {
    -                    ["customer"] = customer,
    -                    ["order"] = customer.Orders.LastOrDefault(),
    -                }
    -            }.Init();
    -
    -            context.VirtualFiles.WriteFile("email.md", request.EmailTemplate);
    -            context.VirtualFiles.WriteFile("layout.html", request.HtmlTemplate);
    -
    -            var textEmail = new PageResult(context.GetPage("email")).Result;
    -            var htmlEmail = new PageResult(context.GetPage("email")) {
    -                Layout = "layout",
    -                PageTransformers = { MarkdownPageFormat.TransformToHtml }
    -            }.Result;
    -
    -            return new PreviewHtmlEmailResponse {
    -                TextEmail = textEmail,
    -                HtmlEmail = htmlEmail,
    -            };
    -        }
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/email-templates/01.md b/src/wwwroot/gfm/email-templates/01.md deleted file mode 100644 index 6524412..0000000 --- a/src/wwwroot/gfm/email-templates/01.md +++ /dev/null @@ -1,57 +0,0 @@ -```csharp -using System.Linq; -using System.Collections.Generic; -using ServiceStack; -using ServiceStack.Templates; -using ServiceStack.IO; - -namespace TemplatePages -{ - [Route("/emails/order-confirmation/preview")] - public class PreviewHtmlEmail : IReturn - { - public string EmailTemplate { get; set; } - public string HtmlTemplate { get; set; } - public string PreviewCustomerId { get; set; } - } - - public class PreviewHtmlEmailResponse - { - public string HtmlEmail { get; set; } - public string TextEmail { get; set; } - } - - public class EmailTemplatesServices : Service - { - public ICustomers Customers { get; set; } - - public object Any(PreviewHtmlEmail request) - { - var customer = Customers.GetCustomer(request.PreviewCustomerId) - ?? Customers.GetAllCustomers().First(); - - var context = new TemplateContext { - PageFormats = { new MarkdownPageFormat() }, - Args = { - ["customer"] = customer, - ["order"] = customer.Orders.LastOrDefault(), - } - }.Init(); - - context.VirtualFiles.WriteFile("email.md", request.EmailTemplate); - context.VirtualFiles.WriteFile("layout.html", request.HtmlTemplate); - - var textEmail = new PageResult(context.GetPage("email")).Result; - var htmlEmail = new PageResult(context.GetPage("email")) { - Layout = "layout", - PageTransformers = { MarkdownPageFormat.TransformToHtml } - }.Result; - - return new PreviewHtmlEmailResponse { - TextEmail = textEmail, - HtmlEmail = htmlEmail, - }; - } - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/email-templates/02.html b/src/wwwroot/gfm/email-templates/02.html deleted file mode 100644 index e04d444..0000000 --- a/src/wwwroot/gfm/email-templates/02.html +++ /dev/null @@ -1,9 +0,0 @@ -
    <script>
    -$("FORM").ajaxPreview({ 
    -    success: function(r) {
    -        $("#html-preview").html(r.htmlEmail);
    -        $("#text-preview").html(r.textEmail);
    -    } 
    -})
    -</script>
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/error-handling/01.html b/src/wwwroot/gfm/error-handling/01.html deleted file mode 100644 index 2e6d3f7..0000000 --- a/src/wwwroot/gfm/error-handling/01.html +++ /dev/null @@ -1,4 +0,0 @@ -
    new TemplateContext {
    -    SkipExecutingFiltersIfError = true
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/error-handling/01.md b/src/wwwroot/gfm/error-handling/01.md deleted file mode 100644 index cfd721b..0000000 --- a/src/wwwroot/gfm/error-handling/01.md +++ /dev/null @@ -1,5 +0,0 @@ -```csharp -new TemplateContext { - SkipExecutingFiltersIfError = true -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/error-handling/02.html b/src/wwwroot/gfm/error-handling/02.html deleted file mode 100644 index 3a85413..0000000 --- a/src/wwwroot/gfm/error-handling/02.html +++ /dev/null @@ -1,4 +0,0 @@ -
    var context = new TemplateContext {
    -    RenderExpressionExceptions = true
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/error-handling/02.md b/src/wwwroot/gfm/error-handling/02.md deleted file mode 100644 index c12fa93..0000000 --- a/src/wwwroot/gfm/error-handling/02.md +++ /dev/null @@ -1,5 +0,0 @@ -```csharp -var context = new TemplateContext { - RenderExpressionExceptions = true -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/error-handling/03.html b/src/wwwroot/gfm/error-handling/03.html deleted file mode 100644 index 6c20761..0000000 --- a/src/wwwroot/gfm/error-handling/03.html +++ /dev/null @@ -1,28 +0,0 @@ -
    T exec<T>(Func<IDbConnection, T> fn, TemplateScopeContext scope, object options)
    -{
    -    try
    -    {
    -        using (var db = DbFactory.Open())
    -        {
    -            return fn(db);
    -        }
    -    }
    -    catch (Exception ex)
    -    {
    -        throw new StopFilterExecutionException(scope, options, ex);
    -    }
    -}
    -
    -public object dbSelect(TemplateScopeContext scope, string sql, Dictionary<string, object> args) => 
    -    exec(db => db.SqlList<Dictionary<string, object>>(sql, args), scope, null);
    -
    -public object dbSelect(TemplateScopeContext scope, string sql, Dictionary<string, object> args, object op) => 
    -    exec(db => db.SqlList<Dictionary<string, object>>(sql, args), scope, op);
    -
    -
    -public object dbSingle(TemplateScopeContext scope, string sql, Dictionary<string, object> args) =>
    -    exec(db => db.Single<Dictionary<string, object>>(sql, args), scope, null);
    -
    -public object dbSingle(TemplateScopeContext scope, string sql, Dictionary<string, object> args, object op) =>
    -    exec(db => db.Single<Dictionary<string, object>>(sql, args), scope, op);
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/error-handling/03.md b/src/wwwroot/gfm/error-handling/03.md deleted file mode 100644 index a4fd168..0000000 --- a/src/wwwroot/gfm/error-handling/03.md +++ /dev/null @@ -1,29 +0,0 @@ -```csharp -T exec(Func fn, TemplateScopeContext scope, object options) -{ - try - { - using (var db = DbFactory.Open()) - { - return fn(db); - } - } - catch (Exception ex) - { - throw new StopFilterExecutionException(scope, options, ex); - } -} - -public object dbSelect(TemplateScopeContext scope, string sql, Dictionary args) => - exec(db => db.SqlList>(sql, args), scope, null); - -public object dbSelect(TemplateScopeContext scope, string sql, Dictionary args, object op) => - exec(db => db.SqlList>(sql, args), scope, op); - - -public object dbSingle(TemplateScopeContext scope, string sql, Dictionary args) => - exec(db => db.Single>(sql, args), scope, null); - -public object dbSingle(TemplateScopeContext scope, string sql, Dictionary args, object op) => - exec(db => db.Single>(sql, args), scope, op); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/01.html b/src/wwwroot/gfm/filters/01.html deleted file mode 100644 index 649daf1..0000000 --- a/src/wwwroot/gfm/filters/01.html +++ /dev/null @@ -1,2 +0,0 @@ -
    context.TemplateFilters.Insert(0, new MyTemplateFilters());
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/01.md b/src/wwwroot/gfm/filters/01.md deleted file mode 100644 index 4e8d83f..0000000 --- a/src/wwwroot/gfm/filters/01.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -context.TemplateFilters.Insert(0, new MyTemplateFilters()); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/02.html b/src/wwwroot/gfm/filters/02.html deleted file mode 100644 index 2a73486..0000000 --- a/src/wwwroot/gfm/filters/02.html +++ /dev/null @@ -1,2 +0,0 @@ -
    context.TemplateFilters.Clear();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/02.md b/src/wwwroot/gfm/filters/02.md deleted file mode 100644 index f8c52b0..0000000 --- a/src/wwwroot/gfm/filters/02.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -context.TemplateFilters.Clear(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/03.html b/src/wwwroot/gfm/filters/03.html deleted file mode 100644 index 3c50da4..0000000 --- a/src/wwwroot/gfm/filters/03.html +++ /dev/null @@ -1,10 +0,0 @@ -
    class MyFilter : TemplateFilter
    -{
    -    public string echo(string text) => $"{text} {text}";
    -    public double squared(double value) => value * value;
    -    public string greetArg(string key) => $"Hello {Context.Args[key]}";
    -            
    -    public ICacheClient Cache { get; set; } //injected dependency
    -    public string fromCache(string key) => Cache.Get(key);
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/03.md b/src/wwwroot/gfm/filters/03.md deleted file mode 100644 index f460702..0000000 --- a/src/wwwroot/gfm/filters/03.md +++ /dev/null @@ -1,11 +0,0 @@ -```csharp -class MyFilter : TemplateFilter -{ - public string echo(string text) => $"{text} {text}"; - public double squared(double value) => value * value; - public string greetArg(string key) => $"Hello {Context.Args[key]}"; - - public ICacheClient Cache { get; set; } //injected dependency - public string fromCache(string key) => Cache.Get(key); -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/04.html b/src/wwwroot/gfm/filters/04.html deleted file mode 100644 index f8fab65..0000000 --- a/src/wwwroot/gfm/filters/04.html +++ /dev/null @@ -1,9 +0,0 @@ -
    var context = new TemplateContext
    -{
    -    Args =
    -    {
    -        ["contextArg"] = "foo"
    -    },
    -    TemplateFilters = { new MyFilter() }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/04.md b/src/wwwroot/gfm/filters/04.md deleted file mode 100644 index 3d18b0f..0000000 --- a/src/wwwroot/gfm/filters/04.md +++ /dev/null @@ -1,10 +0,0 @@ -```csharp -var context = new TemplateContext -{ - Args = - { - ["contextArg"] = "foo" - }, - TemplateFilters = { new MyFilter() } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/05.html b/src/wwwroot/gfm/filters/05.html deleted file mode 100644 index 29dc961..0000000 --- a/src/wwwroot/gfm/filters/05.html +++ /dev/null @@ -1,2 +0,0 @@ -
    var output = context.EvaluateTemplate("<p>{{ 'contextArg' | greetArg }}</p>");
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/05.md b/src/wwwroot/gfm/filters/05.md deleted file mode 100644 index be3b124..0000000 --- a/src/wwwroot/gfm/filters/05.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -var output = context.EvaluateTemplate("

    {{ 'contextArg' | greetArg }}

    "); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/06.html b/src/wwwroot/gfm/filters/06.html deleted file mode 100644 index 6ca59bc..0000000 --- a/src/wwwroot/gfm/filters/06.html +++ /dev/null @@ -1,5 +0,0 @@ -
    var output = new PageResult(context.OneTimePage("<p>{{ 'hello' | echo }}</p>"))
    -{
    -    TemplateFilters = { new MyFilter() }
    -}.Result;
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/06.md b/src/wwwroot/gfm/filters/06.md deleted file mode 100644 index 3ec79ad..0000000 --- a/src/wwwroot/gfm/filters/06.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -var output = new PageResult(context.OneTimePage("

    {{ 'hello' | echo }}

    ")) -{ - TemplateFilters = { new MyFilter() } -}.Result; -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/07.html b/src/wwwroot/gfm/filters/07.html deleted file mode 100644 index 2f5b78d..0000000 --- a/src/wwwroot/gfm/filters/07.html +++ /dev/null @@ -1,10 +0,0 @@ -
    var context = new TemplateContext
    -{
    -    ScanTypes = { typeof(MyFilter) }
    -};
    -context.Container.AddSingleton<ICacheClient>(() => new MemoryCacheClient());
    -context.Container.Resolve<ICacheClient>().Set("key", "foo");
    -context.Init();
    -
    -var output = context.EvaluateTemplate("<p>{{ 'key' | fromCache }}</p>");
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/07.md b/src/wwwroot/gfm/filters/07.md deleted file mode 100644 index 743dbe0..0000000 --- a/src/wwwroot/gfm/filters/07.md +++ /dev/null @@ -1,11 +0,0 @@ -```csharp -var context = new TemplateContext -{ - ScanTypes = { typeof(MyFilter) } -}; -context.Container.AddSingleton(() => new MemoryCacheClient()); -context.Container.Resolve().Set("key", "foo"); -context.Init(); - -var output = context.EvaluateTemplate("

    {{ 'key' | fromCache }}

    "); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/08.html b/src/wwwroot/gfm/filters/08.html deleted file mode 100644 index 0de54ac..0000000 --- a/src/wwwroot/gfm/filters/08.html +++ /dev/null @@ -1,5 +0,0 @@ -
    var context = new TemplateContext
    -{
    -    ScanAssemblies = { typeof(MyFilter).Assembly }
    -};
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/08.md b/src/wwwroot/gfm/filters/08.md deleted file mode 100644 index be8aadb..0000000 --- a/src/wwwroot/gfm/filters/08.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -var context = new TemplateContext -{ - ScanAssemblies = { typeof(MyFilter).Assembly } -}; -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/09.html b/src/wwwroot/gfm/filters/09.html deleted file mode 100644 index 649daf1..0000000 --- a/src/wwwroot/gfm/filters/09.html +++ /dev/null @@ -1,2 +0,0 @@ -
    context.TemplateFilters.Insert(0, new MyTemplateFilters());
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/09.md b/src/wwwroot/gfm/filters/09.md deleted file mode 100644 index 4e8d83f..0000000 --- a/src/wwwroot/gfm/filters/09.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -context.TemplateFilters.Insert(0, new MyTemplateFilters()); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/10.html b/src/wwwroot/gfm/filters/10.html deleted file mode 100644 index 6662e02..0000000 --- a/src/wwwroot/gfm/filters/10.html +++ /dev/null @@ -1,2 +0,0 @@ -
    double squared(double value) => value * value;
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/10.md b/src/wwwroot/gfm/filters/10.md deleted file mode 100644 index 7524a41..0000000 --- a/src/wwwroot/gfm/filters/10.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -double squared(double value) => value * value; -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/11.html b/src/wwwroot/gfm/filters/11.html deleted file mode 100644 index 8c49f78..0000000 --- a/src/wwwroot/gfm/filters/11.html +++ /dev/null @@ -1,6 +0,0 @@ -
    public object assignTo(TemplateScopeContext scope, object value, string argName) //from filter
    -{
    -    scope.ScopedParams[argName] = value;
    -    return IgnoreResult.Value;
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/11.md b/src/wwwroot/gfm/filters/11.md deleted file mode 100644 index e3a39c1..0000000 --- a/src/wwwroot/gfm/filters/11.md +++ /dev/null @@ -1,7 +0,0 @@ -```csharp -public object assignTo(TemplateScopeContext scope, object value, string argName) //from filter -{ - scope.ScopedParams[argName] = value; - return IgnoreResult.Value; -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/12.html b/src/wwwroot/gfm/filters/12.html deleted file mode 100644 index 200def0..0000000 --- a/src/wwwroot/gfm/filters/12.html +++ /dev/null @@ -1,12 +0,0 @@ -
    public async Task includeFile(TemplateScopeContext scope, string virtualPath)
    -{
    -    var file = scope.Context.VirtualFiles.GetFile(virtualPath);
    -    if (file == null)
    -        throw new FileNotFoundException($"includeFile '{virtualPath}' was not found");
    -
    -    using (var reader = file.OpenRead())
    -    {
    -        await reader.CopyToAsync(scope.OutputStream);
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/filters/12.md b/src/wwwroot/gfm/filters/12.md deleted file mode 100644 index e05c79a..0000000 --- a/src/wwwroot/gfm/filters/12.md +++ /dev/null @@ -1,13 +0,0 @@ -```csharp -public async Task includeFile(TemplateScopeContext scope, string virtualPath) -{ - var file = scope.Context.VirtualFiles.GetFile(virtualPath); - if (file == null) - throw new FileNotFoundException($"includeFile '{virtualPath}' was not found"); - - using (var reader = file.OpenRead()) - { - await reader.CopyToAsync(scope.OutputStream); - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/hot-reloading/01.html b/src/wwwroot/gfm/hot-reloading/01.html deleted file mode 100644 index 373e341..0000000 --- a/src/wwwroot/gfm/hot-reloading/01.html +++ /dev/null @@ -1,2 +0,0 @@ -
    Plugins.Add(new TemplatePagesFeature());
    -
    diff --git a/src/wwwroot/gfm/hot-reloading/01.md b/src/wwwroot/gfm/hot-reloading/01.md deleted file mode 100644 index d4b1c5d..0000000 --- a/src/wwwroot/gfm/hot-reloading/01.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -Plugins.Add(new TemplatePagesFeature()); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/hot-reloading/02.html b/src/wwwroot/gfm/hot-reloading/02.html deleted file mode 100644 index 1f6039a..0000000 --- a/src/wwwroot/gfm/hot-reloading/02.html +++ /dev/null @@ -1,2 +0,0 @@ -
    <i hidden>{{ '/js/hot-loader.js' | ifDebugIncludeScript }}</i>
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/hot-reloading/02.md b/src/wwwroot/gfm/hot-reloading/02.md deleted file mode 100644 index dfb89e1..0000000 --- a/src/wwwroot/gfm/hot-reloading/02.md +++ /dev/null @@ -1,3 +0,0 @@ -```html - -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/info-filters/01.html b/src/wwwroot/gfm/info-filters/01.html deleted file mode 100644 index 9c89bce..0000000 --- a/src/wwwroot/gfm/info-filters/01.html +++ /dev/null @@ -1,5 +0,0 @@ -
    var context = new TemplatePagesFeature 
    -{
    -    TemplateFilters = { new TemplateInfoFilters() }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/info-filters/01.md b/src/wwwroot/gfm/info-filters/01.md deleted file mode 100644 index f3fa6f2..0000000 --- a/src/wwwroot/gfm/info-filters/01.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -var context = new TemplatePagesFeature -{ - TemplateFilters = { new TemplateInfoFilters() } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/info-filters/02.html b/src/wwwroot/gfm/info-filters/02.html deleted file mode 100644 index 5de87a3..0000000 --- a/src/wwwroot/gfm/info-filters/02.html +++ /dev/null @@ -1,4 +0,0 @@ -
    Plugins.Add(new TemplatePagesFeature { 
    -    MetadataDebugAdminRole = RoleNames.AllowAnon
    -})
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/info-filters/02.md b/src/wwwroot/gfm/info-filters/02.md deleted file mode 100644 index de05ebe..0000000 --- a/src/wwwroot/gfm/info-filters/02.md +++ /dev/null @@ -1,5 +0,0 @@ -```csharp -Plugins.Add(new TemplatePagesFeature { - MetadataDebugAdminRole = RoleNames.AllowAnon -}) -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/info-filters/03.html b/src/wwwroot/gfm/info-filters/03.html deleted file mode 100644 index 2d02298..0000000 --- a/src/wwwroot/gfm/info-filters/03.html +++ /dev/null @@ -1,2 +0,0 @@ -
    SetConfig(new HostConfig { AdminAuthSecret = "secret" })
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/info-filters/04.html b/src/wwwroot/gfm/info-filters/04.html deleted file mode 100644 index dc5b7a4..0000000 --- a/src/wwwroot/gfm/info-filters/04.html +++ /dev/null @@ -1,5 +0,0 @@ -
    Plugins.Add(new TemplatePagesFeature { 
    -    MetadataDebugAdminRole = RoleNames.AllowAnyUser,  // Allow Authenticated Users
    -    MetadataDebugAdminRole = RoleNames.AllowAnon,     // Allow anyone
    -})
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/info-filters/04.md b/src/wwwroot/gfm/info-filters/04.md deleted file mode 100644 index 7551b42..0000000 --- a/src/wwwroot/gfm/info-filters/04.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -Plugins.Add(new TemplatePagesFeature { - MetadataDebugAdminRole = RoleNames.AllowAnyUser, // Allow Authenticated Users - MetadataDebugAdminRole = RoleNames.AllowAnon, // Allow anyone -}) -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/installation/01.html b/src/wwwroot/gfm/installation/01.html deleted file mode 100644 index dd5b6ca..0000000 --- a/src/wwwroot/gfm/installation/01.html +++ /dev/null @@ -1,4 +0,0 @@ -
    var context = new TemplateContext().Init();
    -var dynamicPage = context.OneTimePage("The time is now: {{ now | dateFormat('HH:mm:ss') }}"); 
    -var output = new PageResult(dynamicPage).Result;
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/installation/01.md b/src/wwwroot/gfm/installation/01.md deleted file mode 100644 index e7f50ae..0000000 --- a/src/wwwroot/gfm/installation/01.md +++ /dev/null @@ -1,5 +0,0 @@ -```ts -var context = new TemplateContext().Init(); -var dynamicPage = context.OneTimePage("The time is now: {{ now | dateFormat('HH:mm:ss') }}"); -var output = new PageResult(dynamicPage).Result; -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/installation/02.html b/src/wwwroot/gfm/installation/02.html deleted file mode 100644 index b591c27..0000000 --- a/src/wwwroot/gfm/installation/02.html +++ /dev/null @@ -1,2 +0,0 @@ -
    var output = context.EvaluateTemplate("The time is now: {{ now | dateFormat('HH:mm:ss') }}");
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/installation/02.md b/src/wwwroot/gfm/installation/02.md deleted file mode 100644 index 852a956..0000000 --- a/src/wwwroot/gfm/installation/02.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -var output = context.EvaluateTemplate("The time is now: {{ now | dateFormat('HH:mm:ss') }}"); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/installation/03.html b/src/wwwroot/gfm/installation/03.html deleted file mode 100644 index 2d1b2a9..0000000 --- a/src/wwwroot/gfm/installation/03.html +++ /dev/null @@ -1,5 +0,0 @@ -
    public void Configure(Container container)
    -{
    -    Plugins.Add(new TemplatePagesFeature());
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/installation/03.md b/src/wwwroot/gfm/installation/03.md deleted file mode 100644 index 4d5848a..0000000 --- a/src/wwwroot/gfm/installation/03.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -public void Configure(Container container) -{ - Plugins.Add(new TemplatePagesFeature()); -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/01.html b/src/wwwroot/gfm/introduction/01.html deleted file mode 100644 index 19f93cf..0000000 --- a/src/wwwroot/gfm/introduction/01.html +++ /dev/null @@ -1,2 +0,0 @@ -
    var context = new TemplateContext().Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/01.md b/src/wwwroot/gfm/introduction/01.md deleted file mode 100644 index 5fccfa8..0000000 --- a/src/wwwroot/gfm/introduction/01.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -var context = new TemplateContext().Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/02.html b/src/wwwroot/gfm/introduction/02.html deleted file mode 100644 index 4b4d208..0000000 --- a/src/wwwroot/gfm/introduction/02.html +++ /dev/null @@ -1,2 +0,0 @@ -
    var output = context.EvaluateTemplate("{{ 12.34 | currency }}");
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/02.md b/src/wwwroot/gfm/introduction/02.md deleted file mode 100644 index ece1d62..0000000 --- a/src/wwwroot/gfm/introduction/02.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -var output = context.EvaluateTemplate("{{ 12.34 | currency }}"); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/03.html b/src/wwwroot/gfm/introduction/03.html deleted file mode 100644 index e69de29..0000000 diff --git a/src/wwwroot/gfm/introduction/03.md b/src/wwwroot/gfm/introduction/03.md deleted file mode 100644 index f7192f6..0000000 --- a/src/wwwroot/gfm/introduction/03.md +++ /dev/null @@ -1,4 +0,0 @@ -```csharp -context.VirtualFiles.WriteFile("_layout.html", "I am the Layout: {{ page }}"); -context.VirtualFiles.WriteFile("page.html", "I am the Page"); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/04.html b/src/wwwroot/gfm/introduction/04.html deleted file mode 100644 index 99a696b..0000000 --- a/src/wwwroot/gfm/introduction/04.html +++ /dev/null @@ -1,2 +0,0 @@ -
    var pageResult = new PageResult(context.GetPage("page"));
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/04.md b/src/wwwroot/gfm/introduction/04.md deleted file mode 100644 index c9c05e1..0000000 --- a/src/wwwroot/gfm/introduction/04.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -var pageResult = new PageResult(context.GetPage("page")); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/05.html b/src/wwwroot/gfm/introduction/05.html deleted file mode 100644 index 375c4e8..0000000 --- a/src/wwwroot/gfm/introduction/05.html +++ /dev/null @@ -1,2 +0,0 @@ -
    await pageResult.WriteToAsync(responseStream);
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/05.md b/src/wwwroot/gfm/introduction/05.md deleted file mode 100644 index cd7b9a0..0000000 --- a/src/wwwroot/gfm/introduction/05.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -await pageResult.WriteToAsync(responseStream); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/06.html b/src/wwwroot/gfm/introduction/06.html deleted file mode 100644 index 178ba5e..0000000 --- a/src/wwwroot/gfm/introduction/06.html +++ /dev/null @@ -1,2 +0,0 @@ -
    string output = await pageResult.RenderToStringAsync();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/06.md b/src/wwwroot/gfm/introduction/06.md deleted file mode 100644 index d924885..0000000 --- a/src/wwwroot/gfm/introduction/06.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -string output = await pageResult.RenderToStringAsync(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/07.html b/src/wwwroot/gfm/introduction/07.html deleted file mode 100644 index e196229..0000000 --- a/src/wwwroot/gfm/introduction/07.html +++ /dev/null @@ -1,2 +0,0 @@ -
    string output = pageResult.Result;
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/07.md b/src/wwwroot/gfm/introduction/07.md deleted file mode 100644 index aa9d835..0000000 --- a/src/wwwroot/gfm/introduction/07.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -string output = pageResult.Result; -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/08.html b/src/wwwroot/gfm/introduction/08.html deleted file mode 100644 index bcd054e..0000000 --- a/src/wwwroot/gfm/introduction/08.html +++ /dev/null @@ -1,4 +0,0 @@ -
    {{ 'select url from quote where id= @id' | dbScalar({ id }) | urlContents | markdown | assignTo: quote }}
    -
    -{{ quote | replace('Razor', 'Templates') | replace('2010', now.Year) | raw }}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introduction/08.md b/src/wwwroot/gfm/introduction/08.md deleted file mode 100644 index da63a6f..0000000 --- a/src/wwwroot/gfm/introduction/08.md +++ /dev/null @@ -1,5 +0,0 @@ -```js -{{ 'select url from quote where id= @id' | dbScalar({ id }) | urlContents | markdown | assignTo: quote }} - -{{ quote | replace('Razor', 'Templates') | replace('2010', now.Year) | raw }} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introspect-state/01.html b/src/wwwroot/gfm/introspect-state/01.html deleted file mode 100644 index 477273d..0000000 --- a/src/wwwroot/gfm/introspect-state/01.html +++ /dev/null @@ -1,47 +0,0 @@ -
    using System;
    -using System.Linq;
    -using System.IO;
    -using System.Diagnostics;
    -using System.Collections.Generic;
    -using ServiceStack;
    -using ServiceStack.IO;
    -using ServiceStack.Templates;
    -
    -namespace TemplatePages
    -{
    -    [Route("/introspect/state")]
    -    public class IntrospectState 
    -    {
    -        public string Page { get; set; }
    -        public string ProcessInfo { get; set; }
    -        public string DriveInfo { get; set; }
    -    }
    -
    -    public class StateTemplateFilters : TemplateFilter
    -    {
    -        bool HasAccess(Process process)
    -        {
    -            try { return process.TotalProcessorTime >= TimeSpan.Zero; } 
    -            catch (Exception) { return false; }
    -        }
    -
    -        public IEnumerable<Process> processes() => Process.GetProcesses().Where(HasAccess);
    -        public Process processById(int processId) => Process.GetProcessById(processId);
    -        public Process currentProcess() => Process.GetCurrentProcess();
    -        public DriveInfo[] drives() => DriveInfo.GetDrives();
    -    }
    -
    -    public class IntrospectStateServices : Service
    -    {
    -        public object Any(IntrospectState request)
    -        {
    -            var context = new TemplateContext {
    -                ScanTypes = { typeof(StateTemplateFilters) }, //Autowires (if needed)
    -                RenderExpressionExceptions = true
    -            }.Init();
    -
    -            return new PageResult(context.OneTimePage(request.Page));
    -        }
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introspect-state/01.md b/src/wwwroot/gfm/introspect-state/01.md deleted file mode 100644 index 466130b..0000000 --- a/src/wwwroot/gfm/introspect-state/01.md +++ /dev/null @@ -1,48 +0,0 @@ -```csharp -using System; -using System.Linq; -using System.IO; -using System.Diagnostics; -using System.Collections.Generic; -using ServiceStack; -using ServiceStack.IO; -using ServiceStack.Templates; - -namespace TemplatePages -{ - [Route("/introspect/state")] - public class IntrospectState - { - public string Page { get; set; } - public string ProcessInfo { get; set; } - public string DriveInfo { get; set; } - } - - public class StateTemplateFilters : TemplateFilter - { - bool HasAccess(Process process) - { - try { return process.TotalProcessorTime >= TimeSpan.Zero; } - catch (Exception) { return false; } - } - - public IEnumerable processes() => Process.GetProcesses().Where(HasAccess); - public Process processById(int processId) => Process.GetProcessById(processId); - public Process currentProcess() => Process.GetCurrentProcess(); - public DriveInfo[] drives() => DriveInfo.GetDrives(); - } - - public class IntrospectStateServices : Service - { - public object Any(IntrospectState request) - { - var context = new TemplateContext { - ScanTypes = { typeof(StateTemplateFilters) }, //Autowires (if needed) - RenderExpressionExceptions = true - }.Init(); - - return new PageResult(context.OneTimePage(request.Page)); - } - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/introspect-state/02.html b/src/wwwroot/gfm/introspect-state/02.html deleted file mode 100644 index 3d7637e..0000000 --- a/src/wwwroot/gfm/introspect-state/02.html +++ /dev/null @@ -1,9 +0,0 @@ -
    <script>
    -$('form').ajaxPreview({ 
    -    dataType: 'html',
    -    success: function(r) {
    -        $("#output").html(r);
    -    } 
    -})
    -</script>
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introspect-state/03.html b/src/wwwroot/gfm/introspect-state/03.html deleted file mode 100644 index 5de87a3..0000000 --- a/src/wwwroot/gfm/introspect-state/03.html +++ /dev/null @@ -1,4 +0,0 @@ -
    Plugins.Add(new TemplatePagesFeature { 
    -    MetadataDebugAdminRole = RoleNames.AllowAnon
    -})
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/introspect-state/03.md b/src/wwwroot/gfm/introspect-state/03.md deleted file mode 100644 index de05ebe..0000000 --- a/src/wwwroot/gfm/introspect-state/03.md +++ /dev/null @@ -1,5 +0,0 @@ -```csharp -Plugins.Add(new TemplatePagesFeature { - MetadataDebugAdminRole = RoleNames.AllowAnon -}) -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/model-view-controller/01.html b/src/wwwroot/gfm/model-view-controller/01.html deleted file mode 100644 index e7a8a32..0000000 --- a/src/wwwroot/gfm/model-view-controller/01.html +++ /dev/null @@ -1,22 +0,0 @@ -
    using ServiceStack;
    -using ServiceStack.Templates;
    -
    -namespace TemplatePages
    -{
    -    [Route("/customers/{Id}")]
    -    public class ViewCustomer
    -    {
    -        public string Id { get; set; }
    -    }
    -
    -    public class CustomerServices : Service
    -    {
    -        public ITemplatePages Pages { get; set; }
    -
    -        public object Any(ViewCustomer request) =>
    -            new PageResult(Pages.GetPage("examples/customer")) {
    -                Model = TemplateQueryData.GetCustomer(request.Id)
    -            };
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/model-view-controller/01.md b/src/wwwroot/gfm/model-view-controller/01.md deleted file mode 100644 index d968a81..0000000 --- a/src/wwwroot/gfm/model-view-controller/01.md +++ /dev/null @@ -1,23 +0,0 @@ -```csharp -using ServiceStack; -using ServiceStack.Templates; - -namespace TemplatePages -{ - [Route("/customers/{Id}")] - public class ViewCustomer - { - public string Id { get; set; } - } - - public class CustomerServices : Service - { - public ITemplatePages Pages { get; set; } - - public object Any(ViewCustomer request) => - new PageResult(Pages.GetPage("examples/customer")) { - Model = TemplateQueryData.GetCustomer(request.Id) - }; - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/model-view-controller/02.html b/src/wwwroot/gfm/model-view-controller/02.html deleted file mode 100644 index ada5f08..0000000 --- a/src/wwwroot/gfm/model-view-controller/02.html +++ /dev/null @@ -1,8 +0,0 @@ -
    public class CustomerServices : Service
    -{
    -    public object Any(ViewCustomer request) =>
    -        new PageResult(Request.GetPage("examples/customer")) {
    -            Model = TemplateQueryData.GetCustomer(request.Id)
    -        };
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/model-view-controller/02.md b/src/wwwroot/gfm/model-view-controller/02.md deleted file mode 100644 index dca404f..0000000 --- a/src/wwwroot/gfm/model-view-controller/02.md +++ /dev/null @@ -1,9 +0,0 @@ -```csharp -public class CustomerServices : Service -{ - public object Any(ViewCustomer request) => - new PageResult(Request.GetPage("examples/customer")) { - Model = TemplateQueryData.GetCustomer(request.Id) - }; -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/model-view-controller/03.html b/src/wwwroot/gfm/model-view-controller/03.html deleted file mode 100644 index 7f5a364..0000000 --- a/src/wwwroot/gfm/model-view-controller/03.html +++ /dev/null @@ -1,32 +0,0 @@ -
    <!--
    -title: Customer Details
    --->
    -
    -<h2>{{ CompanyName }}</h2>
    -
    -<table class="table table-bordered">
    -    <tr>
    -        <th>Address</th>
    -        <td>
    -            {{ Address }} 
    -            {{ City }}, {{ PostalCode }}, {{ Country }}            
    -        </td>
    -    </tr>
    -    <tr>
    -        <th>Phone</th>
    -        <td>{{ Phone }}</td>
    -    </tr>
    -    <tr>
    -        <th>Fax</th>
    -        <td>{{ Fax }}</td>
    -    </tr>
    -</table>
    -
    -<h4>{{ CompanyName }}'s Orders</h4>
    -
    -<table class="table ">
    -{{#each Orders}}
    -    <tr><td>{{OrderId}}</td><td>{{OrderDate}}</td><td>{{Total | currency}}</td></tr>
    -{{/each}}
    -</table>
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/model-view-controller/03.md b/src/wwwroot/gfm/model-view-controller/03.md deleted file mode 100644 index 4f3915d..0000000 --- a/src/wwwroot/gfm/model-view-controller/03.md +++ /dev/null @@ -1,33 +0,0 @@ -```html - - -

    {{ CompanyName }}

    - - - - - - - - - - - - - - -
    Address - {{ Address }} - {{ City }}, {{ PostalCode }}, {{ Country }} -
    Phone{{ Phone }}
    Fax{{ Fax }}
    - -

    {{ CompanyName }}'s Orders

    - - -{{#each Orders}} - -{{/each}} -
    {{OrderId}}{{OrderDate}}{{Total | currency}}
    -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-aspnet/01.html b/src/wwwroot/gfm/mvc-aspnet/01.html deleted file mode 100644 index 8383be5..0000000 --- a/src/wwwroot/gfm/mvc-aspnet/01.html +++ /dev/null @@ -1,9 +0,0 @@ -
    public class HomeController : Controller
    -{
    -    ITemplatePages pages;
    -    public HomeController(ITemplatePages pages) => this.pages = pages;
    -
    -    public Task<MvcPageResult> Index() =>
    -        new PageResult(pages.GetPage("index")).ToMvcResultAsync();
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-aspnet/01.md b/src/wwwroot/gfm/mvc-aspnet/01.md deleted file mode 100644 index da57c7c..0000000 --- a/src/wwwroot/gfm/mvc-aspnet/01.md +++ /dev/null @@ -1,10 +0,0 @@ -```csharp -public class HomeController : Controller -{ - ITemplatePages pages; - public HomeController(ITemplatePages pages) => this.pages = pages; - - public Task Index() => - new PageResult(pages.GetPage("index")).ToMvcResultAsync(); -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/01.html b/src/wwwroot/gfm/mvc-netcore/01.html deleted file mode 100644 index 3805971..0000000 --- a/src/wwwroot/gfm/mvc-netcore/01.html +++ /dev/null @@ -1,20 +0,0 @@ -
    public class Startup
    -{
    -    public void Configure(IApplicationBuilder app)
    -    {
    -        app.UseServiceStack(new AppHost());
    -    }
    -}
    -
    -public class MyServices : Service {}
    -
    -public class AppHost : AppHostBase
    -{
    -    public AppHost() : base("ServiceStack Template Pages", typeof(MyServices).GetAssembly()) { }
    -
    -    public override void Configure(Container container)
    -    {
    -        Plugins.Add(new TemplatePagesFeature());
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/01.md b/src/wwwroot/gfm/mvc-netcore/01.md deleted file mode 100644 index f561f03..0000000 --- a/src/wwwroot/gfm/mvc-netcore/01.md +++ /dev/null @@ -1,21 +0,0 @@ -```csharp -public class Startup -{ - public void Configure(IApplicationBuilder app) - { - app.UseServiceStack(new AppHost()); - } -} - -public class MyServices : Service {} - -public class AppHost : AppHostBase -{ - public AppHost() : base("ServiceStack Template Pages", typeof(MyServices).GetAssembly()) { } - - public override void Configure(Container container) - { - Plugins.Add(new TemplatePagesFeature()); - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/02.html b/src/wwwroot/gfm/mvc-netcore/02.html deleted file mode 100644 index a24306b..0000000 --- a/src/wwwroot/gfm/mvc-netcore/02.html +++ /dev/null @@ -1,6 +0,0 @@ -
    public class HomeController : Controller
    -{
    -    public ActionResult Index() =>
    -        new PageResult(HostContext.TryResolve<ITemplatePages>().GetPage("index")).ToMvcResult();
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/02.md b/src/wwwroot/gfm/mvc-netcore/02.md deleted file mode 100644 index 4c9d625..0000000 --- a/src/wwwroot/gfm/mvc-netcore/02.md +++ /dev/null @@ -1,7 +0,0 @@ -```csharp -public class HomeController : Controller -{ - public ActionResult Index() => - new PageResult(HostContext.TryResolve().GetPage("index")).ToMvcResult(); -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/03.html b/src/wwwroot/gfm/mvc-netcore/03.html deleted file mode 100644 index 420251b..0000000 --- a/src/wwwroot/gfm/mvc-netcore/03.html +++ /dev/null @@ -1,27 +0,0 @@ -
    public class Startup
    -{
    -    public void ConfigureServices(IServiceCollection services)
    -    {
    -        var context = new TemplatePagesFeature();
    -        services.AddSingleton(context);
    -        services.AddSingleton(context.Pages);
    -    }
    -
    -    public void Configure(IApplicationBuilder app)
    -    {
    -        app.UseServiceStack(new AppHost());
    -    }
    -}
    -
    -public class MyServices : Service {}
    -
    -public class AppHost : AppHostBase
    -{
    -    public AppHost() : base("ServiceStack Template Pages", typeof(MyServices).GetAssembly()) {}
    -
    -    public override void Configure(Container container)
    -    {
    -        Plugins.Add(container.Resolve<TemplatePagesFeature>());
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/03.md b/src/wwwroot/gfm/mvc-netcore/03.md deleted file mode 100644 index 6bfcaee..0000000 --- a/src/wwwroot/gfm/mvc-netcore/03.md +++ /dev/null @@ -1,28 +0,0 @@ -```csharp -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - var context = new TemplatePagesFeature(); - services.AddSingleton(context); - services.AddSingleton(context.Pages); - } - - public void Configure(IApplicationBuilder app) - { - app.UseServiceStack(new AppHost()); - } -} - -public class MyServices : Service {} - -public class AppHost : AppHostBase -{ - public AppHost() : base("ServiceStack Template Pages", typeof(MyServices).GetAssembly()) {} - - public override void Configure(Container container) - { - Plugins.Add(container.Resolve()); - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/04.html b/src/wwwroot/gfm/mvc-netcore/04.html deleted file mode 100644 index bb90c0f..0000000 --- a/src/wwwroot/gfm/mvc-netcore/04.html +++ /dev/null @@ -1,8 +0,0 @@ -
    public class HomeController : Controller
    -{
    -    ITemplatePages pages;
    -    public HomeController(ITemplatePages pages) => this.pages = pages;
    -
    -    public ActionResult Index() => new PageResult(pages.GetPage("index")).ToMvcResult();
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/04.md b/src/wwwroot/gfm/mvc-netcore/04.md deleted file mode 100644 index 6077ed2..0000000 --- a/src/wwwroot/gfm/mvc-netcore/04.md +++ /dev/null @@ -1,9 +0,0 @@ -```csharp -public class HomeController : Controller -{ - ITemplatePages pages; - public HomeController(ITemplatePages pages) => this.pages = pages; - - public ActionResult Index() => new PageResult(pages.GetPage("index")).ToMvcResult(); -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/05.html b/src/wwwroot/gfm/mvc-netcore/05.html deleted file mode 100644 index 835ac0c..0000000 --- a/src/wwwroot/gfm/mvc-netcore/05.html +++ /dev/null @@ -1,25 +0,0 @@ -
    public class Startup
    -{
    -    public void ConfigureServices(IServiceCollection services)
    -    {
    -        var context = new TemplateContext();
    -        services.AddSingleton(context);
    -        services.AddSingleton(context.Pages);
    -    }
    -
    -    public void Configure(IApplicationBuilder app)
    -    {
    -        var context = app.ApplicationServices.GetService<TemplateContext>();
    -        context.VirtualFiles = new FileSystemVirtualFiles(env.WebRootPath);
    -        context.Init();
    -    }
    -}
    -
    -public class HomeController : Controller
    -{
    -    ITemplatePages pages;
    -    public HomeController(ITemplatePages pages) => this.pages = pages;
    -
    -    public ActionResult Index() => new PageResult(pages.GetPage("index")).ToMvcResult();
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/mvc-netcore/05.md b/src/wwwroot/gfm/mvc-netcore/05.md deleted file mode 100644 index 6471787..0000000 --- a/src/wwwroot/gfm/mvc-netcore/05.md +++ /dev/null @@ -1,26 +0,0 @@ -```csharp -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - var context = new TemplateContext(); - services.AddSingleton(context); - services.AddSingleton(context.Pages); - } - - public void Configure(IApplicationBuilder app) - { - var context = app.ApplicationServices.GetService(); - context.VirtualFiles = new FileSystemVirtualFiles(env.WebRootPath); - context.Init(); - } -} - -public class HomeController : Controller -{ - ITemplatePages pages; - public HomeController(ITemplatePages pages) => this.pages = pages; - - public ActionResult Index() => new PageResult(pages.GetPage("index")).ToMvcResult(); -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/page-formats/01.md b/src/wwwroot/gfm/page-formats/01.md deleted file mode 100644 index 30d1f1c..0000000 --- a/src/wwwroot/gfm/page-formats/01.md +++ /dev/null @@ -1,19 +0,0 @@ -```csharp -public class HtmlPageFormat : PageFormat -{ - public HtmlPageFormat() - { - ArgsPrefix = ""; - Extension = "html"; - ContentType = MimeTypes.Html; - EncodeValue = HtmlEncodeValue; - ResolveLayout = HtmlResolveLayout; - OnExpressionException = HtmlExpressionException; - } - public static string HtmlEncodeValue(object value) { /*impl*/ } - public TemplatePage HtmlResolveLayout(TemplatePage page) { /*impl*/ } - public virtual object HtmlExpressionException(PageResult result, Exception ex) { /*impl*/ } - public static async Task HtmlEncodeTransformer(Stream stream) { /*impl*/ } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/page-formats/02.html b/src/wwwroot/gfm/page-formats/02.html deleted file mode 100644 index 1725970..0000000 --- a/src/wwwroot/gfm/page-formats/02.html +++ /dev/null @@ -1,9 +0,0 @@ -
    public class MarkdownPageFormat : PageFormat
    -{
    -    public MarkdownPageFormat()
    -    {
    -        Extension = "md";
    -        ContentType = MimeTypes.MarkdownText;
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/page-formats/02.md b/src/wwwroot/gfm/page-formats/02.md deleted file mode 100644 index 91b9f54..0000000 --- a/src/wwwroot/gfm/page-formats/02.md +++ /dev/null @@ -1,10 +0,0 @@ -```csharp -public class MarkdownPageFormat : PageFormat -{ - public MarkdownPageFormat() - { - Extension = "md"; - ContentType = MimeTypes.MarkdownText; - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/page-formats/03.html b/src/wwwroot/gfm/page-formats/03.html deleted file mode 100644 index 3b03571..0000000 --- a/src/wwwroot/gfm/page-formats/03.html +++ /dev/null @@ -1,4 +0,0 @@ -
    var context = new TemplateContext { 
    -    PageFormats = { new MarkdownPageFormat() }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/page-formats/03.md b/src/wwwroot/gfm/page-formats/03.md deleted file mode 100644 index 2728eab..0000000 --- a/src/wwwroot/gfm/page-formats/03.md +++ /dev/null @@ -1,5 +0,0 @@ -```csharp -var context = new TemplateContext { - PageFormats = { new MarkdownPageFormat() } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/protected-filters/01.html b/src/wwwroot/gfm/protected-filters/01.html deleted file mode 100644 index aad65f3..0000000 --- a/src/wwwroot/gfm/protected-filters/01.html +++ /dev/null @@ -1,5 +0,0 @@ -
    var context = new TemplateContext
    -{
    -    TemplateFilters = { new TemplateProtectedFilters() }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/protected-filters/01.md b/src/wwwroot/gfm/protected-filters/01.md deleted file mode 100644 index 8cf2c8c..0000000 --- a/src/wwwroot/gfm/protected-filters/01.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -var context = new TemplateContext -{ - TemplateFilters = { new TemplateProtectedFilters() } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/redis-filters/01.html b/src/wwwroot/gfm/redis-filters/01.html deleted file mode 100644 index 44fa8e3..0000000 --- a/src/wwwroot/gfm/redis-filters/01.html +++ /dev/null @@ -1,2 +0,0 @@ -
    container.AddSingleton<IRedisClientsManager>(() => new RedisManagerPool(connString));
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/redis-filters/01.md b/src/wwwroot/gfm/redis-filters/01.md deleted file mode 100644 index dbff0cd..0000000 --- a/src/wwwroot/gfm/redis-filters/01.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -container.AddSingleton(() => new RedisManagerPool(connString)); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/redis-filters/02.html b/src/wwwroot/gfm/redis-filters/02.html deleted file mode 100644 index 3a94de9..0000000 --- a/src/wwwroot/gfm/redis-filters/02.html +++ /dev/null @@ -1,6 +0,0 @@ -
    var context = new TemplateContext { 
    -    TemplateFilters = {
    -        new TemplateRedisFilters()
    -    }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/redis-filters/02.md b/src/wwwroot/gfm/redis-filters/02.md deleted file mode 100644 index 392b0ca..0000000 --- a/src/wwwroot/gfm/redis-filters/02.md +++ /dev/null @@ -1,7 +0,0 @@ -```csharp -var context = new TemplateContext { - TemplateFilters = { - new TemplateRedisFilters() - } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/redis-filters/03.html b/src/wwwroot/gfm/redis-filters/03.html deleted file mode 100644 index d3ae710..0000000 --- a/src/wwwroot/gfm/redis-filters/03.html +++ /dev/null @@ -1,8 +0,0 @@ -
    public class RedisSearchResult
    -{
    -    public string Id { get; set; }
    -    public string Type { get; set; }
    -    public long Ttl { get; set; }
    -    public long Size { get; set; }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/redis-filters/03.md b/src/wwwroot/gfm/redis-filters/03.md deleted file mode 100644 index ea1a4bf..0000000 --- a/src/wwwroot/gfm/redis-filters/03.md +++ /dev/null @@ -1,9 +0,0 @@ -```csharp -public class RedisSearchResult -{ - public string Id { get; set; } - public string Type { get; set; } - public long Ttl { get; set; } - public long Size { get; set; } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/sandbox/01.html b/src/wwwroot/gfm/sandbox/01.html deleted file mode 100644 index 2a73486..0000000 --- a/src/wwwroot/gfm/sandbox/01.html +++ /dev/null @@ -1,2 +0,0 @@ -
    context.TemplateFilters.Clear();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/sandbox/01.md b/src/wwwroot/gfm/sandbox/01.md deleted file mode 100644 index f8c52b0..0000000 --- a/src/wwwroot/gfm/sandbox/01.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -context.TemplateFilters.Clear(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/sandbox/02.html b/src/wwwroot/gfm/sandbox/02.html deleted file mode 100644 index 6b874b5..0000000 --- a/src/wwwroot/gfm/sandbox/02.html +++ /dev/null @@ -1,4 +0,0 @@ -
    var context = new TemplateContext {
    -    ExcludeFiltersNamed = { "partial", "selectPartial" }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/sandbox/02.md b/src/wwwroot/gfm/sandbox/02.md deleted file mode 100644 index 0f92f5c..0000000 --- a/src/wwwroot/gfm/sandbox/02.md +++ /dev/null @@ -1,5 +0,0 @@ -```csharp -var context = new TemplateContext { - ExcludeFiltersNamed = { "partial", "selectPartial" } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/sandbox/03.html b/src/wwwroot/gfm/sandbox/03.html deleted file mode 100644 index 8cabfce..0000000 --- a/src/wwwroot/gfm/sandbox/03.html +++ /dev/null @@ -1,6 +0,0 @@ -
    var context = new TemplateContext {
    -    Args = {
    -        [TemplateConstants.MaxQuota] = 1000
    -    }
    -}.Init();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/sandbox/03.md b/src/wwwroot/gfm/sandbox/03.md deleted file mode 100644 index 8a6549c..0000000 --- a/src/wwwroot/gfm/sandbox/03.md +++ /dev/null @@ -1,7 +0,0 @@ -```csharp -var context = new TemplateContext { - Args = { - [TemplateConstants.MaxQuota] = 1000 - } -}.Init(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/01.html b/src/wwwroot/gfm/servicestack-filters/01.html deleted file mode 100644 index a7e78fb..0000000 --- a/src/wwwroot/gfm/servicestack-filters/01.html +++ /dev/null @@ -1,7 +0,0 @@ -
    public class QueryCustomers : QueryDb<Customer>
    -{
    -    public string CustomerId { get; set; }
    -    public string CompanyNameContains { get; set; }
    -    public string[] CountryIn { get; set; }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/01.md b/src/wwwroot/gfm/servicestack-filters/01.md deleted file mode 100644 index 1bd411d..0000000 --- a/src/wwwroot/gfm/servicestack-filters/01.md +++ /dev/null @@ -1,8 +0,0 @@ -```csharp -public class QueryCustomers : QueryDb -{ - public string CustomerId { get; set; } - public string CompanyNameContains { get; set; } - public string[] CountryIn { get; set; } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/02.html b/src/wwwroot/gfm/servicestack-filters/02.html deleted file mode 100644 index f1e47b6..0000000 --- a/src/wwwroot/gfm/servicestack-filters/02.html +++ /dev/null @@ -1,4 +0,0 @@ -
    {{ { id, firstName, lastName, age } | ensureAllArgsNotNull | publishToGateway('StoreContact') }}
    -{{ { id }  | sendToGateway('GetContact') | assignTo: contact }}
    -{{ contact | selectPartial: contact-details }}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/02.md b/src/wwwroot/gfm/servicestack-filters/02.md deleted file mode 100644 index bd90611..0000000 --- a/src/wwwroot/gfm/servicestack-filters/02.md +++ /dev/null @@ -1,5 +0,0 @@ -```js -{{ { id, firstName, lastName, age } | ensureAllArgsNotNull | publishToGateway('StoreContact') }} -{{ { id } | sendToGateway('GetContact') | assignTo: contact }} -{{ contact | selectPartial: contact-details }} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/03.html b/src/wwwroot/gfm/servicestack-filters/03.html deleted file mode 100644 index 7a47fae..0000000 --- a/src/wwwroot/gfm/servicestack-filters/03.html +++ /dev/null @@ -1,5 +0,0 @@ -
    Plugins.Add(new AutoQueryDataFeature { MaxLimit = 100 }
    -    .AddDataSource(ctx => ctx.ServiceSource<GithubRepo>(ctx.Dto.ConvertTo<GetGithubRepos>(), 
    -        HostContext.Cache, TimeSpan.FromMinutes(10)))
    -);
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/03.md b/src/wwwroot/gfm/servicestack-filters/03.md deleted file mode 100644 index a0f4df6..0000000 --- a/src/wwwroot/gfm/servicestack-filters/03.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -Plugins.Add(new AutoQueryDataFeature { MaxLimit = 100 } - .AddDataSource(ctx => ctx.ServiceSource(ctx.Dto.ConvertTo(), - HostContext.Cache, TimeSpan.FromMinutes(10))) -); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/04.html b/src/wwwroot/gfm/servicestack-filters/04.html deleted file mode 100644 index 0d497e7..0000000 --- a/src/wwwroot/gfm/servicestack-filters/04.html +++ /dev/null @@ -1,41 +0,0 @@ -
    public class QueryGitHubRepos : QueryDatao<GithubRepo>
    -{
    -    public string UserName { get; set; }
    -}
    -
    -public class GetGithubRepos : IReturn<List<GithubRepo>>
    -{
    -    public string UserName { get; set; }
    -}
    -
    -public class AutoDataQueryServices : Service
    -{
    -    public object Any(GetGithubRepos request)
    -    {
    -        var map = new Dictionary<int, GithubRepo>();
    -        GetUserRepos(request.UserName).Each(x => map[x.Id] = x);
    -        GetOrgRepos(request.UserName).Each(x => map[x.Id] = x);
    -        GetUserOrgs(request.UserName).Each(org =>
    -            GetOrgRepos(org.Login)
    -                .Each(repo => map[repo.Id] = repo));
    -
    -        return map.Values.ToList();
    -    }
    -
    -    public List<GithubOrg> GetUserOrgs(string githubUsername) => 
    -        GetJson<List<GithubOrg>>($"users/{githubUsername}/orgs");
    -    public List<GithubRepo> GetUserRepos(string githubUsername) => 
    -        GetJson<List<GithubRepo>>($"users/{githubUsername}/repos");
    -    public List<GithubRepo> GetOrgRepos(string githubOrgName) => 
    -        GetJson<List<GithubRepo>>($"orgs/{githubOrgName}/repos");
    -
    -    public T GetJson<T>(string route) 
    -    {
    -        try {
    -            return "https://api.github.com".CombineWith(route)
    -                .GetJsonFromUrl(requestFilter: req => req.UserAgent = nameof(AutoDataQueryServices))
    -                .FromJson<T>();
    -        } catch(Exception) { return default(T); }
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/04.md b/src/wwwroot/gfm/servicestack-filters/04.md deleted file mode 100644 index 24bd495..0000000 --- a/src/wwwroot/gfm/servicestack-filters/04.md +++ /dev/null @@ -1,42 +0,0 @@ -```csharp -public class QueryGitHubRepos : QueryDatao -{ - public string UserName { get; set; } -} - -public class GetGithubRepos : IReturn> -{ - public string UserName { get; set; } -} - -public class AutoDataQueryServices : Service -{ - public object Any(GetGithubRepos request) - { - var map = new Dictionary(); - GetUserRepos(request.UserName).Each(x => map[x.Id] = x); - GetOrgRepos(request.UserName).Each(x => map[x.Id] = x); - GetUserOrgs(request.UserName).Each(org => - GetOrgRepos(org.Login) - .Each(repo => map[repo.Id] = repo)); - - return map.Values.ToList(); - } - - public List GetUserOrgs(string githubUsername) => - GetJson>($"users/{githubUsername}/orgs"); - public List GetUserRepos(string githubUsername) => - GetJson>($"users/{githubUsername}/repos"); - public List GetOrgRepos(string githubOrgName) => - GetJson>($"orgs/{githubOrgName}/repos"); - - public T GetJson(string route) - { - try { - return "https://api.github.com".CombineWith(route) - .GetJsonFromUrl(requestFilter: req => req.UserAgent = nameof(AutoDataQueryServices)) - .FromJson(); - } catch(Exception) { return default(T); } - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/05.html b/src/wwwroot/gfm/servicestack-filters/05.html deleted file mode 100644 index 4d55da7..0000000 --- a/src/wwwroot/gfm/servicestack-filters/05.html +++ /dev/null @@ -1,3 +0,0 @@ -
    container.AddSingleton<IDbConnectionFactory>(() => 
    -    new OrmLiteConnectionFactory(connectionString, SqlServer2012Dialect.Provider));
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/05.md b/src/wwwroot/gfm/servicestack-filters/05.md deleted file mode 100644 index 52e1de6..0000000 --- a/src/wwwroot/gfm/servicestack-filters/05.md +++ /dev/null @@ -1,4 +0,0 @@ -```csharp -container.AddSingleton(() => - new OrmLiteConnectionFactory(connectionString, SqlServer2012Dialect.Provider)); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/06.html b/src/wwwroot/gfm/servicestack-filters/06.html deleted file mode 100644 index 635d4cf..0000000 --- a/src/wwwroot/gfm/servicestack-filters/06.html +++ /dev/null @@ -1,2 +0,0 @@ -
    Plugins.Add(new AutoQueryFeature { MaxLimit = 100 });
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/06.md b/src/wwwroot/gfm/servicestack-filters/06.md deleted file mode 100644 index 56bdd7b..0000000 --- a/src/wwwroot/gfm/servicestack-filters/06.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -Plugins.Add(new AutoQueryFeature { MaxLimit = 100 }); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/07.html b/src/wwwroot/gfm/servicestack-filters/07.html deleted file mode 100644 index dfb5d54..0000000 --- a/src/wwwroot/gfm/servicestack-filters/07.html +++ /dev/null @@ -1,4 +0,0 @@ -
    Plugins.Add(new TemplatePagesFeature {
    -    TemplateFilters = { new TemplateAutoQueryFilters() },
    -});
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/07.md b/src/wwwroot/gfm/servicestack-filters/07.md deleted file mode 100644 index 6dab660..0000000 --- a/src/wwwroot/gfm/servicestack-filters/07.md +++ /dev/null @@ -1,5 +0,0 @@ -```csharp -Plugins.Add(new TemplatePagesFeature { - TemplateFilters = { new TemplateAutoQueryFilters() }, -}); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/08.html b/src/wwwroot/gfm/servicestack-filters/08.html deleted file mode 100644 index a7e78fb..0000000 --- a/src/wwwroot/gfm/servicestack-filters/08.html +++ /dev/null @@ -1,7 +0,0 @@ -
    public class QueryCustomers : QueryDb<Customer>
    -{
    -    public string CustomerId { get; set; }
    -    public string CompanyNameContains { get; set; }
    -    public string[] CountryIn { get; set; }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/servicestack-filters/08.md b/src/wwwroot/gfm/servicestack-filters/08.md deleted file mode 100644 index 1bd411d..0000000 --- a/src/wwwroot/gfm/servicestack-filters/08.md +++ /dev/null @@ -1,8 +0,0 @@ -```csharp -public class QueryCustomers : QueryDb -{ - public string CustomerId { get; set; } - public string CompanyNameContains { get; set; } - public string[] CountryIn { get; set; } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/syntax/01.html b/src/wwwroot/gfm/syntax/01.html deleted file mode 100644 index 0c067f1..0000000 --- a/src/wwwroot/gfm/syntax/01.html +++ /dev/null @@ -1,2 +0,0 @@ -
    public string upper(string text) => text?.ToUpper();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/syntax/01.md b/src/wwwroot/gfm/syntax/01.md deleted file mode 100644 index 053c22b..0000000 --- a/src/wwwroot/gfm/syntax/01.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -public string upper(string text) => text?.ToUpper(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/syntax/02.html b/src/wwwroot/gfm/syntax/02.html deleted file mode 100644 index 6fb5fe5..0000000 --- a/src/wwwroot/gfm/syntax/02.html +++ /dev/null @@ -1,3 +0,0 @@ -
    public string substring(string text, int startIndex) => text.SafeSubstring(startIndex);
    -public string padRight(string text, int totalWidth, char padChar) => text?.PadRight(totalWidth, padChar);
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/syntax/02.md b/src/wwwroot/gfm/syntax/02.md deleted file mode 100644 index 39fc9e7..0000000 --- a/src/wwwroot/gfm/syntax/02.md +++ /dev/null @@ -1,4 +0,0 @@ -```csharp -public string substring(string text, int startIndex) => text.SafeSubstring(startIndex); -public string padRight(string text, int totalWidth, char padChar) => text?.PadRight(totalWidth, padChar); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/01.html b/src/wwwroot/gfm/transformers/01.html deleted file mode 100644 index e69de29..0000000 diff --git a/src/wwwroot/gfm/transformers/01.md b/src/wwwroot/gfm/transformers/01.md deleted file mode 100644 index cd45730..0000000 --- a/src/wwwroot/gfm/transformers/01.md +++ /dev/null @@ -1,22 +0,0 @@ -```csharp -public class MarkdownPageFormat : PageFormat -{ - private static readonly MarkdownDeep.Markdown markdown = new MarkdownDeep.Markdown(); - - public MarkdownPageFormat() - { - Extension = "md"; - ContentType = MimeTypes.MarkdownText; - } - - public static async Task TransformToHtml(Stream markdownStream) - { - using (var reader = new StreamReader(markdownStream)) - { - var md = await reader.ReadToEndAsync(); - var html = markdown.Transform(md); - return MemoryStreamFactory.GetStream(html.ToUtf8Bytes()); - } - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/02.html b/src/wwwroot/gfm/transformers/02.html deleted file mode 100644 index f667e95..0000000 --- a/src/wwwroot/gfm/transformers/02.html +++ /dev/null @@ -1,14 +0,0 @@ -
    var context = new TemplateContext {
    -    PageFormats = { new MarkdownPageFormat() }
    -}.Init();
    -
    -context.VirtualFiles.WriteFile("_layout.md", @"
    -The Header
    -
    -{{ page }}");
    -
    -context.VirtualFiles.WriteFile("page.md",  @"
    -## {{ title }}
    -
    -The Content");
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/02.md b/src/wwwroot/gfm/transformers/02.md deleted file mode 100644 index 1a042ff..0000000 --- a/src/wwwroot/gfm/transformers/02.md +++ /dev/null @@ -1,15 +0,0 @@ -```csharp -var context = new TemplateContext { - PageFormats = { new MarkdownPageFormat() } -}.Init(); - -context.VirtualFiles.WriteFile("_layout.md", @" -The Header - -{{ page }}"); - -context.VirtualFiles.WriteFile("page.md", @" -## {{ title }} - -The Content"); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/03.html b/src/wwwroot/gfm/transformers/03.html deleted file mode 100644 index b88a49c..0000000 --- a/src/wwwroot/gfm/transformers/03.html +++ /dev/null @@ -1,9 +0,0 @@ -
    var result = new PageResult(context.GetPage("page")) 
    -{
    -    Args = { {"title", "The Title"} },
    -    ContentType = MimeTypes.Html,
    -    OutputTransformers = { MarkdownPageFormat.TransformToHtml },
    -};
    -
    -var html = await result.RenderToStringAsync();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/03.md b/src/wwwroot/gfm/transformers/03.md deleted file mode 100644 index c3f880b..0000000 --- a/src/wwwroot/gfm/transformers/03.md +++ /dev/null @@ -1,10 +0,0 @@ -```csharp -var result = new PageResult(context.GetPage("page")) -{ - Args = { {"title", "The Title"} }, - ContentType = MimeTypes.Html, - OutputTransformers = { MarkdownPageFormat.TransformToHtml }, -}; - -var html = await result.RenderToStringAsync(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/04.html b/src/wwwroot/gfm/transformers/04.html deleted file mode 100644 index 9450ef9..0000000 --- a/src/wwwroot/gfm/transformers/04.html +++ /dev/null @@ -1,17 +0,0 @@ -
    var context = new TemplateContext {
    -    PageFormats = { new MarkdownPageFormat() }
    -}.Init();
    -
    -context.VirtualFiles.WriteFile("_layout.html", @"
    -<html>
    -  <title>{{ title }}</title>
    -</head>
    -<body>
    -  {{ page }}
    -</body>");
    -
    -context.VirtualFiles.WriteFile("page.md",  @"
    -## Transformers
    -
    -The Content");
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/04.md b/src/wwwroot/gfm/transformers/04.md deleted file mode 100644 index ee5a4d4..0000000 --- a/src/wwwroot/gfm/transformers/04.md +++ /dev/null @@ -1,18 +0,0 @@ -```csharp -var context = new TemplateContext { - PageFormats = { new MarkdownPageFormat() } -}.Init(); - -context.VirtualFiles.WriteFile("_layout.html", @" - - {{ title }} - - - {{ page }} -"); - -context.VirtualFiles.WriteFile("page.md", @" -## Transformers - -The Content"); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/05.html b/src/wwwroot/gfm/transformers/05.html deleted file mode 100644 index fa2c577..0000000 --- a/src/wwwroot/gfm/transformers/05.html +++ /dev/null @@ -1,9 +0,0 @@ -
    var result = new PageResult(context.GetPage("page")) 
    -{
    -    Args = { {"title", "The Title"} },
    -    ContentType = MimeTypes.Html,
    -    PageTransformers = { MarkdownPageFormat.TransformToHtml },
    -};
    -
    -var html = await result.RenderToStringAsync();
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/05.md b/src/wwwroot/gfm/transformers/05.md deleted file mode 100644 index bc9920a..0000000 --- a/src/wwwroot/gfm/transformers/05.md +++ /dev/null @@ -1,10 +0,0 @@ -```csharp -var result = new PageResult(context.GetPage("page")) -{ - Args = { {"title", "The Title"} }, - ContentType = MimeTypes.Html, - PageTransformers = { MarkdownPageFormat.TransformToHtml }, -}; - -var html = await result.RenderToStringAsync(); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/06.html b/src/wwwroot/gfm/transformers/06.html deleted file mode 100644 index dd71de1..0000000 --- a/src/wwwroot/gfm/transformers/06.html +++ /dev/null @@ -1,18 +0,0 @@ -
    var context = new TemplateContext
    -{
    -    TemplateFilters = { new TemplateProtectedFilters() },
    -    FilterTransformers =
    -    {
    -        ["markdown"] = MarkdownPageFormat.TransformToHtml
    -    }
    -}.Init();
    -
    -context.VirtualFiles.WriteFile("doc.md", "## The Heading
    -
    -The Content");
    -
    -context.VirtualFiles.WriteFile("page.html", "
    -<div id="content">
    -    {{ 'doc.md' | includeFile | markdown }}
    -</div>");
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/06.md b/src/wwwroot/gfm/transformers/06.md deleted file mode 100644 index 99edde5..0000000 --- a/src/wwwroot/gfm/transformers/06.md +++ /dev/null @@ -1,19 +0,0 @@ -```csharp -var context = new TemplateContext -{ - TemplateFilters = { new TemplateProtectedFilters() }, - FilterTransformers = - { - ["markdown"] = MarkdownPageFormat.TransformToHtml - } -}.Init(); - -context.VirtualFiles.WriteFile("doc.md", "## The Heading - -The Content"); - -context.VirtualFiles.WriteFile("page.html", " -
    - {{ 'doc.md' | includeFile | markdown }} -
    "); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/07.html b/src/wwwroot/gfm/transformers/07.html deleted file mode 100644 index 452aaed..0000000 --- a/src/wwwroot/gfm/transformers/07.html +++ /dev/null @@ -1,2 +0,0 @@ -
    var html = new PageResult(context.GetPage("page")).Result;
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/transformers/07.md b/src/wwwroot/gfm/transformers/07.md deleted file mode 100644 index 40565c5..0000000 --- a/src/wwwroot/gfm/transformers/07.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -var html = new PageResult(context.GetPage("page")).Result; -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/01.html b/src/wwwroot/gfm/view-engine/01.html deleted file mode 100644 index 30e2f97..0000000 --- a/src/wwwroot/gfm/view-engine/01.html +++ /dev/null @@ -1,5 +0,0 @@ -
    public void Configure(Container container)
    -{
    -    Plugins.Add(new TemplatePagesFeature());
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/01.md b/src/wwwroot/gfm/view-engine/01.md deleted file mode 100644 index 4d5848a..0000000 --- a/src/wwwroot/gfm/view-engine/01.md +++ /dev/null @@ -1,6 +0,0 @@ -```csharp -public void Configure(Container container) -{ - Plugins.Add(new TemplatePagesFeature()); -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/02.html b/src/wwwroot/gfm/view-engine/02.html deleted file mode 100644 index 7ef77c5..0000000 --- a/src/wwwroot/gfm/view-engine/02.html +++ /dev/null @@ -1,2 +0,0 @@ -
    Plugins.Add(new TemplatePagesFeature { HtmlExtension = "htm" });
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/02.md b/src/wwwroot/gfm/view-engine/02.md deleted file mode 100644 index 15597bb..0000000 --- a/src/wwwroot/gfm/view-engine/02.md +++ /dev/null @@ -1,3 +0,0 @@ -```csharp -Plugins.Add(new TemplatePagesFeature { HtmlExtension = "htm" }); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/03.html b/src/wwwroot/gfm/view-engine/03.html deleted file mode 100644 index 3c7e491..0000000 --- a/src/wwwroot/gfm/view-engine/03.html +++ /dev/null @@ -1,4 +0,0 @@ -
    <!--
    -layout: mobile-layout
    --->
    -
    diff --git a/src/wwwroot/gfm/view-engine/03.md b/src/wwwroot/gfm/view-engine/03.md deleted file mode 100644 index a86ebf8..0000000 --- a/src/wwwroot/gfm/view-engine/03.md +++ /dev/null @@ -1,5 +0,0 @@ -```html - -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/04.html b/src/wwwroot/gfm/view-engine/04.html deleted file mode 100644 index d3b39ad..0000000 --- a/src/wwwroot/gfm/view-engine/04.html +++ /dev/null @@ -1,4 +0,0 @@ -
    <!--
    -layout: templates/mobile-layout
    --->
    -
    diff --git a/src/wwwroot/gfm/view-engine/04.md b/src/wwwroot/gfm/view-engine/04.md deleted file mode 100644 index 9c94528..0000000 --- a/src/wwwroot/gfm/view-engine/04.md +++ /dev/null @@ -1,5 +0,0 @@ -```html - -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/05.html b/src/wwwroot/gfm/view-engine/05.html deleted file mode 100644 index 32a8909..0000000 --- a/src/wwwroot/gfm/view-engine/05.html +++ /dev/null @@ -1,35 +0,0 @@ -
    <nav id="sidebar">
    -<div class="inner">
    -
    -<ul>
    -    <li>
    -        <h5>docs</h5>
    -        <ul>
    -            {{#each docLinks}}
    -            <li {{ {active: matchesPathInfo(Key)} | htmlClass }}><a href="{{Key}}">{{Value}}</a></li>
    -            {{/each}}
    -        </ul>
    -    </li>
    -
    -    <li>
    -        <h5>use cases</h5>
    -        <ul>
    -            {{#each useCaseLinks}}
    -            <li {{ {active: matchesPathInfo(Key)} | htmlClass }}><a href="{{Key}}">{{Value}}</a></li>
    -            {{/each}}
    -        </ul>
    -    </li>
    -
    -    <li>
    -        <h5>linq examples</h5>
    -        <ul>
    -            {{#each linqLinks}}
    -            <li {{ {active: matchesPathInfo(Key)} | htmlClass }}><a href="{{Key}}">{{Value}}</a></li>
    -            {{/each}}
    -        </ul>
    -    </li>
    -</ul>
    -
    -</div>
    -</nav>
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/05.md b/src/wwwroot/gfm/view-engine/05.md deleted file mode 100644 index ddda834..0000000 --- a/src/wwwroot/gfm/view-engine/05.md +++ /dev/null @@ -1,36 +0,0 @@ -```html - -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/06.html b/src/wwwroot/gfm/view-engine/06.html deleted file mode 100644 index 70454cc..0000000 --- a/src/wwwroot/gfm/view-engine/06.html +++ /dev/null @@ -1,6 +0,0 @@ -
    <!--
    -ignore: page
    --->
    -
    -Nothing in this page will be evaluated but will still be rendered inside the closest Layout.
    -
    diff --git a/src/wwwroot/gfm/view-engine/06.md b/src/wwwroot/gfm/view-engine/06.md deleted file mode 100644 index 9ad7093..0000000 --- a/src/wwwroot/gfm/view-engine/06.md +++ /dev/null @@ -1,7 +0,0 @@ -```html - - -Nothing in this page will be evaluated but will still be rendered inside the closest Layout. -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/07.html b/src/wwwroot/gfm/view-engine/07.html deleted file mode 100644 index c048a1d..0000000 --- a/src/wwwroot/gfm/view-engine/07.html +++ /dev/null @@ -1,6 +0,0 @@ -
    <!--
    -ignore: template
    --->
    -
    -This page will not be evaluated nor will it have a Layout.
    -
    diff --git a/src/wwwroot/gfm/view-engine/07.md b/src/wwwroot/gfm/view-engine/07.md deleted file mode 100644 index c4db490..0000000 --- a/src/wwwroot/gfm/view-engine/07.md +++ /dev/null @@ -1,7 +0,0 @@ -```html - - -This page will not be evaluated nor will it have a Layout. -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/08.html b/src/wwwroot/gfm/view-engine/08.html deleted file mode 100644 index 8177528..0000000 --- a/src/wwwroot/gfm/view-engine/08.html +++ /dev/null @@ -1,6 +0,0 @@ -
    <!--
    -layout: none
    --->
    -
    -This page will be evaluated but rendered without a layout.
    -
    diff --git a/src/wwwroot/gfm/view-engine/08.md b/src/wwwroot/gfm/view-engine/08.md deleted file mode 100644 index fd51f26..0000000 --- a/src/wwwroot/gfm/view-engine/08.md +++ /dev/null @@ -1,7 +0,0 @@ -```html - - -This page will be evaluated but rendered without a layout. -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/09.html b/src/wwwroot/gfm/view-engine/09.html deleted file mode 100644 index 7b04116..0000000 --- a/src/wwwroot/gfm/view-engine/09.html +++ /dev/null @@ -1,10 +0,0 @@ -
    public object Any(MyRequest request)
    -{
    -    ...
    -    return new HttpResult(response)
    -    {
    -        View = "CustomPage",
    -        Template = "_custom-layout",
    -    };
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/09.md b/src/wwwroot/gfm/view-engine/09.md deleted file mode 100644 index 296a56f..0000000 --- a/src/wwwroot/gfm/view-engine/09.md +++ /dev/null @@ -1,11 +0,0 @@ -```csharp -public object Any(MyRequest request) -{ - ... - return new HttpResult(response) - { - View = "CustomPage", - Template = "_custom-layout", - }; -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/10.html b/src/wwwroot/gfm/view-engine/10.html deleted file mode 100644 index 463d190..0000000 --- a/src/wwwroot/gfm/view-engine/10.html +++ /dev/null @@ -1,35 +0,0 @@ -
    {{  `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
    -}}
    -
    -{{ dbScalar(`SELECT COUNT(*) FROM Post`) | assignTo: postsCount }}
    -
    -{{#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 later inspected as a normal template argument as seen in -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 page above.

    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/10.md b/src/wwwroot/gfm/view-engine/10.md deleted file mode 100644 index bf36cee..0000000 --- a/src/wwwroot/gfm/view-engine/10.md +++ /dev/null @@ -1,41 +0,0 @@ -```hbs -{{ `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 -}} - -{{ dbScalar(`SELECT COUNT(*) FROM Post`) | assignTo: postsCount }} - -{{#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 later inspected as a normal template argument as seen in -[log.html](https://github.com/NetCoreWebApps/Blog/blob/master/app/log.html): - -```html -
    - Output from init.html: - -
    {{initout | raw}}
    -
    -``` - -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](http://blog.web-app.io/log) page above. \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/11.html b/src/wwwroot/gfm/view-engine/11.html deleted file mode 100644 index ee7d0de..0000000 --- a/src/wwwroot/gfm/view-engine/11.html +++ /dev/null @@ -1,14 +0,0 @@ -
    Plugins.Add(new TemplatePagesFeature {
    -    TemplatesAdminRole = RoleNames.AllowAnyUser, // Allow any Authenticated User to call /templates/admin
    -    //TemplatesAdminRole = RoleNames.AllowAnon,  // Allow anyone
    -    //TemplatesAdminRole = null,                 // Do not register /templates/admin service
    -});
    -

    This also the preferred way to enable the Metadata Debug Template -in production, which is only available in DebugMode and Admin Role by default:

    -
    Plugins.Add(new TemplatePagesFeature {
    -    MetadataDebugAdminRole = RoleNames.Admin,          // Only allow Admin users to call /metadata/debug
    -    //MetadataDebugAdminRole = RoleNames.AllowAnyUser, // Allow Authenticated Users
    -    //MetadataDebugAdminRole = RoleNames.AllowAnon,    // Allow anyone
    -    //MetadataDebugAdminRole = null,                   // Default. Do not register /metadata/debug service
    -});
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/view-engine/11.md b/src/wwwroot/gfm/view-engine/11.md deleted file mode 100644 index 53b5d33..0000000 --- a/src/wwwroot/gfm/view-engine/11.md +++ /dev/null @@ -1,19 +0,0 @@ -```csharp -Plugins.Add(new TemplatePagesFeature { - TemplatesAdminRole = RoleNames.AllowAnyUser, // Allow any Authenticated User to call /templates/admin - //TemplatesAdminRole = RoleNames.AllowAnon, // Allow anyone - //TemplatesAdminRole = null, // Do not register /templates/admin service -}); -``` - -This also the preferred way to enable the [Metadata Debug Template](https://docs.servicestack.net/debugging#metadata-debug-template) -in production, which is only available in `DebugMode` and **Admin** Role by default: - -```csharp -Plugins.Add(new TemplatePagesFeature { - MetadataDebugAdminRole = RoleNames.Admin, // Only allow Admin users to call /metadata/debug - //MetadataDebugAdminRole = RoleNames.AllowAnyUser, // Allow Authenticated Users - //MetadataDebugAdminRole = RoleNames.AllowAnon, // Allow anyone - //MetadataDebugAdminRole = null, // Default. Do not register /metadata/debug service -}); -``` diff --git a/src/wwwroot/gfm/web-apps/01.html b/src/wwwroot/gfm/web-apps/01.html deleted file mode 100644 index 85e2b93..0000000 --- a/src/wwwroot/gfm/web-apps/01.html +++ /dev/null @@ -1,16 +0,0 @@ -
    {{#if id}}
    -    {{ `select o.Id, 
    -            ${sqlConcat(["e.FirstName", "' '", "e.LastName"])} Employee, 
    -            OrderDate, ShipCountry, ShippedDate, 
    -            ${sqlCurrency("sum((d.Unitprice * d.Quantity) - d.discount)")} Total 
    -        from ${sqlQuote("Order")} o
    -            inner join
    -            OrderDetail d on o.Id = d.OrderId
    -            inner join 
    -            Employee e on o.EmployeeId = e.Id
    -        where CustomerId = @id
    -        group by o.Id, EmployeeId, FirstName, LastName, OrderDate, ShipCountry, ShippedDate`
    -        | dbSelect({ id }) 
    -        | assignTo: orders }}
    -{{/if}}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/01.md b/src/wwwroot/gfm/web-apps/01.md deleted file mode 100644 index 4f9d124..0000000 --- a/src/wwwroot/gfm/web-apps/01.md +++ /dev/null @@ -1,17 +0,0 @@ -```js -{{#if id}} - {{ `select o.Id, - ${sqlConcat(["e.FirstName", "' '", "e.LastName"])} Employee, - OrderDate, ShipCountry, ShippedDate, - ${sqlCurrency("sum((d.Unitprice * d.Quantity) - d.discount)")} Total - from ${sqlQuote("Order")} o - inner join - OrderDetail d on o.Id = d.OrderId - inner join - Employee e on o.EmployeeId = e.Id - where CustomerId = @id - group by o.Id, EmployeeId, FirstName, LastName, OrderDate, ShipCountry, ShippedDate` - | dbSelect({ id }) - | assignTo: orders }} -{{/if}} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/02.html b/src/wwwroot/gfm/web-apps/02.html deleted file mode 100644 index 2983429..0000000 --- a/src/wwwroot/gfm/web-apps/02.html +++ /dev/null @@ -1,4 +0,0 @@ -
    <link rel="stylesheet" href="{{ 'assets/css/bootstrap.css' | resolveAsset }}" />
    -
    -<img src="{{ 'splash.jpg' | resolveAsset }}" id="splash" alt="Dave Grohl" />
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/02.md b/src/wwwroot/gfm/web-apps/02.md deleted file mode 100644 index fea25c1..0000000 --- a/src/wwwroot/gfm/web-apps/02.md +++ /dev/null @@ -1,5 +0,0 @@ -```html - - -Dave Grohl -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/03.html b/src/wwwroot/gfm/web-apps/03.html deleted file mode 100644 index f08d0ac..0000000 --- a/src/wwwroot/gfm/web-apps/03.html +++ /dev/null @@ -1,13 +0,0 @@ -
    FROM microsoft/dotnet:2.1-sdk AS build-env
    -COPY app /app
    -WORKDIR /app
    -RUN dotnet tool install -g web
    -
    -# Build runtime image
    -FROM microsoft/dotnet:2.1-aspnetcore-runtime
    -WORKDIR /app
    -COPY --from=build-env /app app
    -COPY --from=build-env /root/.dotnet/tools tools
    -ENV ASPNETCORE_URLS http://*:5000
    -ENTRYPOINT ["/app/tools/web", "app/app.settings"]
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/03.md b/src/wwwroot/gfm/web-apps/03.md deleted file mode 100644 index 487c77e..0000000 --- a/src/wwwroot/gfm/web-apps/03.md +++ /dev/null @@ -1,14 +0,0 @@ -```dockerfile -FROM microsoft/dotnet:2.1-sdk AS build-env -COPY app /app -WORKDIR /app -RUN dotnet tool install -g web - -# Build runtime image -FROM microsoft/dotnet:2.1-aspnetcore-runtime -WORKDIR /app -COPY --from=build-env /app app -COPY --from=build-env /root/.dotnet/tools tools -ENV ASPNETCORE_URLS http://*:5000 -ENTRYPOINT ["/app/tools/web", "app/app.settings"] -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/04.html b/src/wwwroot/gfm/web-apps/04.html deleted file mode 100644 index 8b6d584..0000000 --- a/src/wwwroot/gfm/web-apps/04.html +++ /dev/null @@ -1,6 +0,0 @@ -
    Plugins.Add(new CustomPlugin { ShowProcessLinks = true });
    -Plugins.Add(new OpenApiFeature());
    -Plugins.Add(new PostmanFeature());
    -Plugins.Add(new CorsFeature());
    -Plugins.Add(new ValidationFeature { ScanAppHostAssemblies = true });
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/04.md b/src/wwwroot/gfm/web-apps/04.md deleted file mode 100644 index c2f318a..0000000 --- a/src/wwwroot/gfm/web-apps/04.md +++ /dev/null @@ -1,7 +0,0 @@ -```csharp -Plugins.Add(new CustomPlugin { ShowProcessLinks = true }); -Plugins.Add(new OpenApiFeature()); -Plugins.Add(new PostmanFeature()); -Plugins.Add(new CorsFeature()); -Plugins.Add(new ValidationFeature { ScanAppHostAssemblies = true }); -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/05.html b/src/wwwroot/gfm/web-apps/05.html deleted file mode 100644 index cc24d7b..0000000 --- a/src/wwwroot/gfm/web-apps/05.html +++ /dev/null @@ -1,25 +0,0 @@ -
    public class CustomPlugin : IPlugin
    -{
    -    public bool ShowDrivesLinks { get; set; } = true;
    -    
    -    public bool ShowProcessLinks { get; set; }
    -
    -    public void Register(IAppHost appHost)
    -    {
    -        if (ShowDrivesLinks)
    -        {
    -            var diskFormat = Env.IsWindows ? "NTFS" : "ext2";
    -            appHost.GetPlugin<MetadataFeature>()
    -                .AddPluginLink("/drives", "All Disks")
    -                .AddPluginLink($"/drives?DriveFormatIn={diskFormat}", $"{diskFormat} Disks");
    -        }
    -
    -        if (ShowProcessLinks)
    -        {
    -            appHost.GetPlugin<MetadataFeature>()
    -                .AddPluginLink("/processes", "All Processes")
    -                .AddPluginLink("/process/current", "Current Process");
    -        }
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/05.md b/src/wwwroot/gfm/web-apps/05.md deleted file mode 100644 index 53e2b0a..0000000 --- a/src/wwwroot/gfm/web-apps/05.md +++ /dev/null @@ -1,26 +0,0 @@ -```csharp -public class CustomPlugin : IPlugin -{ - public bool ShowDrivesLinks { get; set; } = true; - - public bool ShowProcessLinks { get; set; } - - public void Register(IAppHost appHost) - { - if (ShowDrivesLinks) - { - var diskFormat = Env.IsWindows ? "NTFS" : "ext2"; - appHost.GetPlugin() - .AddPluginLink("/drives", "All Disks") - .AddPluginLink($"/drives?DriveFormatIn={diskFormat}", $"{diskFormat} Disks"); - } - - if (ShowProcessLinks) - { - appHost.GetPlugin() - .AddPluginLink("/processes", "All Processes") - .AddPluginLink("/process/current", "Current Process"); - } - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/06.html b/src/wwwroot/gfm/web-apps/06.html deleted file mode 100644 index 003a8b5..0000000 --- a/src/wwwroot/gfm/web-apps/06.html +++ /dev/null @@ -1,47 +0,0 @@ -
    public class Startup
    -{
    -    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    -    {
    -        loggerFactory.AddConsole();
    -        var appSettings = new TextFileSettings("~/../../apps/chat/app.settings".MapProjectPath());
    -        app.UseServiceStack(new AppHost(appSettings));
    -    }
    -}
    -
    -public class AppHost : AppHostBase
    -{
    -    public AppHost() : base("Chat Web App", typeof(ServerEventsServices).GetAssembly()) {}
    -    public AppHost(IAppSettings appSettings) : this() => AppSettings = appSettings;
    -
    -    public override void Configure(Container container)
    -    {
    -        Plugins.AddIfNotExists(new TemplatePagesFeature()); //Already added if it's running as a Web App
    -        
    -        Plugins.Add(new ServerEventsFeature());
    -
    -        SetConfig(new HostConfig
    -        {
    -            DefaultContentType = MimeTypes.Json,
    -            AllowSessionIdsInHttpParams = true,
    -        });
    -
    -        this.CustomErrorHttpHandlers.Remove(HttpStatusCode.Forbidden);
    -
    -        //Register all Authentication methods you want to enable for this web app.            
    -        Plugins.Add(new AuthFeature(
    -            () => new AuthUserSession(),
    -            new IAuthProvider[] {
    -                new TwitterAuthProvider(AppSettings),   //Sign-in with Twitter
    -                new FacebookAuthProvider(AppSettings),  //Sign-in with Facebook
    -                new GithubAuthProvider(AppSettings),    //Sign-in with GitHub
    -            }));
    -
    -        container.RegisterAutoWiredAs<MemoryChatHistory, IChatHistory>();
    -
    -        Plugins.Add(new CorsFeature(
    -            allowOriginWhitelist: new[] { "http://localhost", "http://null.jsbin.com" },
    -            allowCredentials: true,
    -            allowedHeaders: "Content-Type, Allow, Authorization"));
    -    }
    -}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/06.md b/src/wwwroot/gfm/web-apps/06.md deleted file mode 100644 index 825ff82..0000000 --- a/src/wwwroot/gfm/web-apps/06.md +++ /dev/null @@ -1,48 +0,0 @@ -```csharp -public class Startup -{ - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - loggerFactory.AddConsole(); - var appSettings = new TextFileSettings("~/../../apps/chat/app.settings".MapProjectPath()); - app.UseServiceStack(new AppHost(appSettings)); - } -} - -public class AppHost : AppHostBase -{ - public AppHost() : base("Chat Web App", typeof(ServerEventsServices).GetAssembly()) {} - public AppHost(IAppSettings appSettings) : this() => AppSettings = appSettings; - - public override void Configure(Container container) - { - Plugins.AddIfNotExists(new TemplatePagesFeature()); //Already added if it's running as a Web App - - Plugins.Add(new ServerEventsFeature()); - - SetConfig(new HostConfig - { - DefaultContentType = MimeTypes.Json, - AllowSessionIdsInHttpParams = true, - }); - - this.CustomErrorHttpHandlers.Remove(HttpStatusCode.Forbidden); - - //Register all Authentication methods you want to enable for this web app. - Plugins.Add(new AuthFeature( - () => new AuthUserSession(), - new IAuthProvider[] { - new TwitterAuthProvider(AppSettings), //Sign-in with Twitter - new FacebookAuthProvider(AppSettings), //Sign-in with Facebook - new GithubAuthProvider(AppSettings), //Sign-in with GitHub - })); - - container.RegisterAutoWiredAs(); - - Plugins.Add(new CorsFeature( - allowOriginWhitelist: new[] { "http://localhost", "http://null.jsbin.com" }, - allowCredentials: true, - allowedHeaders: "Content-Type, Allow, Authorization")); - } -} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/07.html b/src/wwwroot/gfm/web-apps/07.html deleted file mode 100644 index 083578b..0000000 --- a/src/wwwroot/gfm/web-apps/07.html +++ /dev/null @@ -1,22 +0,0 @@ -
    <html>
    -<head>
    -<title>{{ title ?? 'Redis Vue WebApp' }}</title>
    -<i hidden>{{ '/js/hot-loader.js' | ifDebugIncludeScript }}</i>
    -
    -...
    -<link rel="stylesheet" href="../assets/css/bootstrap.css">
    -<link rel="stylesheet" href="../assets/css/default.css">
    -</head>
    -<body>
    -    <h2 id="title"><a href="/"><img src="/assets/img/redis-logo.png" /> {{ title }}</a></h2>
    -
    -    {{ page }}
    -
    -    <script src="../assets/js/html-utils.js"></script>
    -    <script src="../assets/js/vue{{ '.min' | if(!debug) }}.js"></script>
    -    <script src="../assets/js/axios.min.js"></script>
    -    
    -    {{ scripts | raw }} 
    -</body>
    -</html>
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/07.md b/src/wwwroot/gfm/web-apps/07.md deleted file mode 100644 index 324a7b1..0000000 --- a/src/wwwroot/gfm/web-apps/07.md +++ /dev/null @@ -1,23 +0,0 @@ -```html - - -{{ title ?? 'Redis Vue WebApp' }} - - -... - - - - -

    {{ title }}

    - - {{ page }} - - - - - - {{ scripts | raw }} - - -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/08.html b/src/wwwroot/gfm/web-apps/08.html deleted file mode 100644 index ecaccd7..0000000 --- a/src/wwwroot/gfm/web-apps/08.html +++ /dev/null @@ -1,15 +0,0 @@ -
    <script type="text/x-template" id="redis-info-template">
    -<div id="redis-info">
    -  <table class="table table-striped" style="width:450px">
    -    <tbody>
    -    {{#each toList(redisInfo) }}
    -        <tr>
    -          <th>{{ it.Key | replace('_',' ') }}</th>
    -          <td title="{{it.Value}}">{{ it.Value | substringWithEllipsis(32) }}</td>
    -        </tr>
    -    {{/each}}
    -    </tbody>
    -  </table>
    -</div>
    -</script>
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/08.md b/src/wwwroot/gfm/web-apps/08.md deleted file mode 100644 index fac5cb0..0000000 --- a/src/wwwroot/gfm/web-apps/08.md +++ /dev/null @@ -1,16 +0,0 @@ -```html - -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/09.html b/src/wwwroot/gfm/web-apps/09.html deleted file mode 100644 index a7b0ee6..0000000 --- a/src/wwwroot/gfm/web-apps/09.html +++ /dev/null @@ -1,14 +0,0 @@ -
    <script type="text/x-template" id="connection-template">
    -<div id="connection-info" class="container">
    -    {{ continueExecutingFiltersOnError }}
    -    {{#if Request.Verb == "POST" }}
    -        {{ { host, port, db, password } | withoutEmptyValues | redisChangeConnection | end }}
    -        {{#if lastErrorMessage }}
    -            <div class="alert alert-danger">{{lastErrorMessage}}</div>
    -        {{else}}
    -            <div class="alert alert-success">Connection Changed</div>
    -        {{/if}}
    -    {{/if}}
    -</div>
    -</script>
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/09.md b/src/wwwroot/gfm/web-apps/09.md deleted file mode 100644 index 5337bba..0000000 --- a/src/wwwroot/gfm/web-apps/09.md +++ /dev/null @@ -1,15 +0,0 @@ -```html - -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/10.html b/src/wwwroot/gfm/web-apps/10.html deleted file mode 100644 index 3607c71..0000000 --- a/src/wwwroot/gfm/web-apps/10.html +++ /dev/null @@ -1,5 +0,0 @@ -
    {{ limit ?? 100  | assignTo: limit }}
    -
    -{{ `${q ?? ''}*` | redisSearchKeys({ limit }) 
    -                 | return }}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/10.md b/src/wwwroot/gfm/web-apps/10.md deleted file mode 100644 index 98b2ed5..0000000 --- a/src/wwwroot/gfm/web-apps/10.md +++ /dev/null @@ -1,6 +0,0 @@ -```hbs -{{ limit ?? 100 | assignTo: limit }} - -{{ `${q ?? ''}*` | redisSearchKeys({ limit }) - | return }} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/11.html b/src/wwwroot/gfm/web-apps/11.html deleted file mode 100644 index 799a263..0000000 --- a/src/wwwroot/gfm/web-apps/11.html +++ /dev/null @@ -1,11 +0,0 @@ -
    {{ { command } | ensureAllArgsNotEmpty }}
    -
    -{{ ['flush','monitor','brpop','blpop'] | any => contains(lower(command), it)
    -   | assignTo: illegalCommand }}
    -
    -{{ illegalCommand ? throwArgumentException('Command is not allowed.') : null }}
    -
    -{{ command  | redisCall | assignTo: contents }}
    -
    -{{ contents | return({ 'Content-Type': 'text/plain' }) }}
    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/11.md b/src/wwwroot/gfm/web-apps/11.md deleted file mode 100644 index f0f681c..0000000 --- a/src/wwwroot/gfm/web-apps/11.md +++ /dev/null @@ -1,12 +0,0 @@ -```hbs -{{ { command } | ensureAllArgsNotEmpty }} - -{{ ['flush','monitor','brpop','blpop'] | any => contains(lower(command), it) - | assignTo: illegalCommand }} - -{{ illegalCommand ? throwArgumentException('Command is not allowed.') : null }} - -{{ command | redisCall | assignTo: contents }} - -{{ contents | return({ 'Content-Type': 'text/plain' }) }} -``` \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/12.md b/src/wwwroot/gfm/web-apps/12.md deleted file mode 100644 index fab8d52..0000000 --- a/src/wwwroot/gfm/web-apps/12.md +++ /dev/null @@ -1,35 +0,0 @@ -```csharp -public class StartupDep -{ - public string Name { get; } = nameof(StartupDep); -} - -public class StartupPlugin : IPlugin, IStartup -{ - public void Configure(IApplicationBuilder app) {} - - public IServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddSingleton(new StartupDep()); - return null; - } - - public void Register(IAppHost appHost) - { - appHost.GetPlugin() - .AddPluginLink("/startup-dep", "Startup Service"); - } -} - -[Route("/startup-dep")] -public class GetStartupDep : IReturn {} - -public class StartupServices : Service -{ - public StartupDep StartupDep { get; set; } - - [AddHeader(ContentType = MimeTypes.PlainText)] - public object Any(GetStartupDep request) => StartupDep.Name; -} -``` - diff --git a/src/wwwroot/gfm/web-apps/13.html b/src/wwwroot/gfm/web-apps/13.html deleted file mode 100644 index 78615c1..0000000 --- a/src/wwwroot/gfm/web-apps/13.html +++ /dev/null @@ -1,96 +0,0 @@ -

    The Parcel WebApp template is maintained in the following directory structure:

    -
      -
    • -/app - Your Web App's published source code and any plugins
    • -
    • -/client - The Parcel managed Client App where client source code is maintained
    • -
    • -/server - Extend your Web App with an optional server.dll plugin containing additional Server functionality
    • -
    • -/web - The Web App binaries
    • -
    -

    Most development will happen within /client which is automatically published to /app using parcel's CLI that's invoked from the included npm scripts.

    -

    -client

    -

    The difference with templates-webapp is that the client source code is maintained in the /client folder and uses Parcel JS to automatically bundle and publish your App to /app/wwwroot which is updated with live changes during development.

    -

    The client folder also contains the npm package.json which contains all npm scripts required during development.

    -

    If this is the first time using Parcel, you will need to install its global CLI:

    -
    $ npm install -g parcel-bundler
    -
    -

    Then you can run a watched parcel build of your Web App with:

    -
    $ npm run dev
    -
    -

    Parcel is a zero configuration bundler which inspects your .html files to automatically transpile and bundle all your .js and .css assets and other web resources like TypeScript .ts source files into a static .html website synced at /app/wwwroot.

    -

    Then to start the ServiceStack Server to host your Web App run:

    -
    $ npm run server
    -
    -

    Which will host your App at http://localhost:5000 which in debug mode will enable hot reloading -which will automatically reload your web page as it detects any file changes made by parcel.

    -

    -server

    -

    To enable even richer functionality, this Web Apps template is also pre-configured with a custom Server project where you can extend your Web App with Plugins where all Plugins, Services, Filters, etc are automatically wired and made available to your Web App.

    -

    This template includes a simple ServerPlugin.cs which contains an Empty ServerPlugin and Hello Service:

    -
    public class ServerPlugin : IPlugin
    -{
    -    public void Register(IAppHost appHost)
    -    {
    -    }
    -}
    -
    -//[Route("/hello/{Name}")] // Handled by /hello/_name.html API page, uncomment to take over
    -public class Hello : IReturn<HelloResponse>
    -{
    -    public string Name { get; set; }
    -}
    -
    -public class HelloResponse
    -{
    -    public string Result { get; set; }
    -}
    -
    -public class MyServices : Service
    -{
    -    public object Any(Hello request)
    -    {
    -        return new HelloResponse { Result = $"Hi {request.Name} from server.dll" };
    -    }
    -}
    -

    To build the server.csproj project and copy the resulting server.dll to /app/plugins/serer.dll which will require restarting the server to load the latest plugin:

    -
    $ npm run server
    -
    -

    This will automatically load any Plugins, Services, Filters, etc and make them available to your Web App.

    -

    One benefit of creating your APIs with C# ServiceStack Services instead of API Pages is that you can generate TypeScript DTOs with:

    -
    $ npm run dtos
    -
    -

    Which saves generate DTOs for all your ServiceStack Services in dtos.ts which can then be accessed in your TypeScript source code.

    -

    If preferred you can instead develop Server APIs with API Pages, an example is included in /client/hello/_name.html

    -
    {{ { result: `Hi ${name} from /hello/_name.html` } | return }}
    -

    Which as it uses the same data structure as the Hello Service above, can be called with ServiceStack's JsonServiceClient and generated TypeScript DTOs.

    -

    The /client/index.ts shows an example of this where initially the App calls the C# Hello ServiceStack Service:

    -
    import { client } from "./shared";
    -import { Hello, HelloResponse } from "./dtos";
    -
    -const result = document.querySelector("#result")!;
    -
    -document.querySelector("#Name")!.addEventListener("input", async e => {
    -  const value = (e.target as HTMLInputElement).value;
    -  if (value != "") {
    -    const request = new Hello();
    -    request.name = value;
    -    const response = await client.get(request);
    -    // const response = await client.get<HelloResponse>(`/hello/${request.name}`); //call /hello/_name.html
    -    result.innerHTML = response.result;
    -  } else {
    -    result.innerHTML = "";
    -  }
    -});
    -

    But while your App is running you can instead toggle the uncommented the line and hit Ctrl+S to save index.ts which Parcel will automatically transpile and publish to /app/wwwroot that will be detected by Hot Reload to automatically reload the page with the latest changes. Now typing in the text field will display the response from calling the /hello/_name.html API instead.

    -

    -Deployments

    -

    During development Parcel maintains a debug and source-code friendly version of your App. Before deploying you can build an optimized production version of your App with:

    -
    $ npm run build
    -
    -

    Which will bundle and minify all .css, .js and .html assets and publish to /app/wwwroot.

    -

    Then to deploy Web Apps you just need to copy the /app and /web folders to any server with .NET Core 2.1 runtime installed. -The Deploying Web Apps docs.

    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/13.md b/src/wwwroot/gfm/web-apps/13.md deleted file mode 100644 index cfdc435..0000000 --- a/src/wwwroot/gfm/web-apps/13.md +++ /dev/null @@ -1,120 +0,0 @@ -The Parcel WebApp template is maintained in the following directory structure: - - - `/app` - Your Web App's published source code and any plugins - - `/client` - The Parcel managed Client App where client source code is maintained - - `/server` - Extend your Web App with an optional `server.dll` plugin containing additional Server functionality - - `/web` - The Web App binaries - -Most development will happen within `/client` which is automatically published to `/app` using parcel's CLI that's invoked from the included npm scripts. - -### client - -The difference with [templates-webapp](https://github.com/NetCoreTemplates/templates-webapp) is that the client source code is maintained in the `/client` folder and uses [Parcel JS](https://parceljs.org) to automatically bundle and publish your App to `/app/wwwroot` which is updated with live changes during development. - -The client folder also contains the npm [package.json](https://github.com/NetCoreTemplates/parcel-webapp/blob/master/client/package.json) which contains all npm scripts required during development. - -If this is the first time using Parcel, you will need to install its global CLI: - - $ npm install -g parcel-bundler - -Then you can run a watched parcel build of your Web App with: - - $ npm run dev - -Parcel is a zero configuration bundler which inspects your `.html` files to automatically transpile and bundle all your **.js** and **.css** assets and other web resources like TypeScript **.ts** source files into a static `.html` website synced at `/app/wwwroot`. - -Then to start the ServiceStack Server to host your Web App run: - - $ npm run server - -Which will host your App at `http://localhost:5000` which in `debug` mode will enable [hot reloading](http://templates.servicestack.net/docs/hot-reloading) -which will automatically reload your web page as it detects any file changes made by parcel. - -### server - -To enable even richer functionality, this Web Apps template is also pre-configured with a custom Server project where you can extend your Web App with [Plugins](http://templates.servicestack.net/docs/web-apps#plugins) where all `Plugins`, `Services`, `Filters`, etc are automatically wired and made available to your Web App. - -This template includes a simple [ServerPlugin.cs](https://github.com/NetCoreTemplates/parcel-webapp/blob/master/server/ServerPlugin.cs) which contains an Empty `ServerPlugin` and `Hello` Service: - -```csharp -public class ServerPlugin : IPlugin -{ - public void Register(IAppHost appHost) - { - } -} - -//[Route("/hello/{Name}")] // Handled by /hello/_name.html API page, uncomment to take over -public class Hello : IReturn -{ - public string Name { get; set; } -} - -public class HelloResponse -{ - public string Result { get; set; } -} - -public class MyServices : Service -{ - public object Any(Hello request) - { - return new HelloResponse { Result = $"Hi {request.Name} from server.dll" }; - } -} -``` - -To build the `server.csproj` project and copy the resulting `server.dll` to `/app/plugins/serer.dll` which will require restarting the server to load the latest plugin: - - $ npm run server - -This will automatically load any `Plugins`, `Services`, `Filters`, etc and make them available to your Web App. - -One benefit of creating your APIs with C# ServiceStack Services instead of [API Pages](http://templates.servicestack.net/docs/api-pages) is that you can generate TypeScript DTOs with: - - $ npm run dtos - -Which saves generate DTOs for all your ServiceStack Services in [dtos.ts](https://github.com/NetCoreTemplates/parcel-webapp/blob/master/client/dtos.ts) which can then be accessed in your TypeScript source code. - -If preferred you can instead develop Server APIs with API Pages, an example is included in [/client/hello/_name.html](https://github.com/NetCoreTemplates/parcel-webapp/blob/master/client/hello/_name.html) - -```html -{{ { result: `Hi ${name} from /hello/_name.html` } | return }} -``` - -Which as it uses the same data structure as the `Hello` Service above, can be called with ServiceStack's `JsonServiceClient` and generated TypeScript DTOs. - -The [/client/index.ts](https://github.com/NetCoreTemplates/parcel-webapp/blob/master/client/index.ts) shows an example of this where initially the App calls the C# `Hello` ServiceStack Service: - -```ts -import { client } from "./shared"; -import { Hello, HelloResponse } from "./dtos"; - -const result = document.querySelector("#result")!; - -document.querySelector("#Name")!.addEventListener("input", async e => { - const value = (e.target as HTMLInputElement).value; - if (value != "") { - const request = new Hello(); - request.name = value; - const response = await client.get(request); - // const response = await client.get(`/hello/${request.name}`); //call /hello/_name.html - result.innerHTML = response.result; - } else { - result.innerHTML = ""; - } -}); -``` - -But while your App is running you can instead toggle the uncommented the line and hit `Ctrl+S` to save `index.ts` which Parcel will automatically transpile and publish to `/app/wwwroot` that will be detected by Hot Reload to automatically reload the page with the latest changes. Now typing in the text field will display the response from calling the `/hello/_name.html` API instead. - -### Deployments - -During development Parcel maintains a debug and source-code friendly version of your App. Before deploying you can build an optimized production version of your App with: - - $ npm run build - -Which will bundle and minify all `.css`, `.js` and `.html` assets and publish to `/app/wwwroot`. - -Then to deploy Web Apps you just need to copy the `/app` and `/web` folders to any server with .NET Core 2.1 runtime installed. -The [Deploying Web Apps](http://templates.servicestack.net/docs/deploying-web-apps) docs. \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/14.html b/src/wwwroot/gfm/web-apps/14.html deleted file mode 100644 index d504aa7..0000000 --- a/src/wwwroot/gfm/web-apps/14.html +++ /dev/null @@ -1,97 +0,0 @@ -

    -
    -

    Live Demo: blog.web-app.io

    -
    -

    To maximize approachability the /Blog Web App has no C# source code, plugins and uses -no JavaScript or CSS frameworks yielding an enjoyable development experiences as 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 if no database exists, or if preferred can be -changed to use any other popular RDBMS 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 app.settings with the URL where the blog -is hosted and the OAuth Keys for the Twitter OAuth App created at apps.twitter.com.

    -

    -Rich Content

    -

    Whilst most blogging platforms just edit static text, each Post content has access to full Templates language features so they can be used to create -Live Documents or -Render Markdown which is itself just -one of the available blocks where it will render to HTML any content between the markdown blocks:

    -
    {#markdown}}
    -## Markdown Content
    -{​{/markdown}}
    -

    By default the Markdig implementation is used to render Markdown but can also be configured to use an -alternate Markdown provider.

    -

    -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 and edit.html pages shows examples of performing server validation with ServiceStack Templates:

    -
    {{ assignErrorAndContinueExecuting: ex }}
    -
    -{{ 'Title must be between 5 and 200 characters'      
    -   | onlyIf(length(postTitle) < 5 || length(postTitle) > 200) | assignTo: titleError }}
    -{{ 'Content must be between 25 and 64000 characters' 
    -   | onlyIf(length(content) < 25  || length(content) > 64000) | assignTo: contentError }}
    -{{ 'Potentially malicious characters detected'       
    -   | ifNotExists(contentError) | onlyIf(containsXss(content)) | assignTo: contentError }}
    -

    -

    For more info see the docs on 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 below is used to send its 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 API Page which just renders and captures any -Template Content its sent and returns the output:

    -
    {{ content  | evalTemplate({use:{plugins:'MarkdownTemplatePlugin'}}) | assignTo:response }}
    -{{ response | return({ contentType:'text/plain' }) }}
    -

    By default the evalTemplate filter renders Templates in a new TemplateContext which can be customized to utilize any additional -plugins, filters and blocks available in the configured TemplatePagesFeature, -or for full access you can use {use:{context:true}} to evaluate any Template content under the same context that evalTemplate is run on.

    -
    \ No newline at end of file diff --git a/src/wwwroot/gfm/web-apps/14.md b/src/wwwroot/gfm/web-apps/14.md deleted file mode 100644 index aeb46f2..0000000 --- a/src/wwwroot/gfm/web-apps/14.md +++ /dev/null @@ -1,129 +0,0 @@ -[![](https://raw.githubusercontent.com/NetCoreApps/TemplatePages/master/src/wwwroot/assets/img/screenshots/blog.png)](http://blog.web-app.io) - -> Live Demo: [blog.web-app.io](http://blog.web-app.io) - -To maximize approachability the [/Blog](https://github.com/NetCoreWebApps/Blog/tree/master/app) Web App has no C# source code, plugins and uses -no JavaScript or CSS frameworks yielding an enjoyable development experiences as 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](/docs/view-engine#init-pages) if no database exists, or if preferred can be -[changed to use any other popular RDBMS](/docs/web-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 app.settings](#customizable-auth-providers) with the URL where the blog -is hosted and the OAuth Keys for the Twitter OAuth App created at [apps.twitter.com](https://apps.twitter.com). - -### Rich Content - -Whilst most blogging platforms just edit static text, each Post content has access to full Templates language features so they can be used to create -[Live Documents](http://blog.web-app.io/posts/live-document-example) or -[Render Markdown](http://blog.web-app.io/posts/markdown-example) which is itself just -[one of the available blocks](/docs/blocks#markdown) where it will render to HTML any content between the `markdown` blocks: - -```hbs -{#markdown}} -## Markdown Content -{​{/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](http://blog.web-app.io/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: - -![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/blog/editor.png) - -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/NetCoreWebApps/Blog/blob/master/app/posts/new.html) and [edit.html](https://github.com/NetCoreWebApps/Blog/blob/master/app/posts/_slug/edit.html) pages shows examples of performing server validation with ServiceStack Templates: - -```hbs -{{ assignErrorAndContinueExecuting: ex }} - -{{ 'Title must be between 5 and 200 characters' - | onlyIf(length(postTitle) < 5 || length(postTitle) > 200) | assignTo: titleError }} -{{ 'Content must be between 25 and 64000 characters' - | onlyIf(length(content) < 25 || length(content) > 64000) | assignTo: contentError }} -{{ 'Potentially malicious characters detected' - | ifNotExists(contentError) | onlyIf(containsXss(content)) | assignTo: contentError }} -``` - -![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/blog/server-validation.png) - -For more info see the docs on [Error Handling](/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: - -```html - -
    -``` - -The implementation of which is surprisingly simple where the JavaScript snippet in -[default.js](https://github.com/NetCoreWebApps/Blog/blob/master/app/default.js) below is used to send its content on each change: - -```js -// 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 = "
    Live Preview
    "; - 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/NetCoreWebApps/Blog/blob/master/app/preview.html) API Page which just renders and captures any -Template Content its sent and returns the output: - -```hbs -{{ content | evalTemplate({use:{plugins:'MarkdownTemplatePlugin'}}) | assignTo:response }} -{{ response | return({ contentType:'text/plain' }) }} -``` - -By default the `evalTemplate` filter renders Templates in a new `TemplateContext` which can be customized to utilize any additional -`plugins`, `filters` and `blocks` available in the configured [TemplatePagesFeature](/docs/view-engine), -or for full access you can use `{use:{context:true}}` to evaluate any Template content under the same context that `evalTemplate` is run on. diff --git a/src/wwwroot/index.html b/src/wwwroot/index.html deleted file mode 100644 index 423a2dc..0000000 --- a/src/wwwroot/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - -

    - At its core ServiceStack Templates is a simple, fast, and extremely versatile - dynamic templating language for .NET and .NET Core that utilizes an Expressive Syntax to compose high-level - functionality using .NET filters and arguments. -

    - -

    - It's small, lightweight footprint and built-in Hot Reloading - provides a fun, clean and productive alternative to MVC Razor that's easily - integrated into any web framework and runs identically in - every platform ServiceStack runs on, it can also - be returned in ASP.NET MVC and ASP.NET MVC Core - Controllers - in all cases, using the same high-performance implementation - to asynchronously write to a forward-only OutputStream for max performance and maximum potential reuse of your Source Code. -

    - -

    - Templates are lazily loaded and late-bound for Instant Startup, doesn't require any - pre-compilation, have coupling to any external configuration files, build tools, designer tooling or have - any special deployment requirements. - It can be used as a general purpose templating language to enhance any text format and - includes built-in support for .html. -

    - -

    - Templates are evaluated in an Isolated Sandboxed that enables fine-grained control over exactly - what functionality and instances are available to different Templates. They're pre-configured with a comprehensive suite of safe - Default Filters which when running in trusted contexts can easily be granted access to - enhanced functionality. -

    - -

    - Templates are designed to be incrementally adoptable where its initial form is - ideal for non-programmers, - that can gradually adopt more power and functionality when needed where they can leverage existing Services or MVC Controllers - to enable an - MVC programming model or have .html pages upgraded to use - Code Pages where they can utilize the full unlimited power of the C# programming language to - enable precise control over the rendering of pages and partials. Code pages take precedence and are interchangeable wherever - normal .html pages are requested making them a non-invasive alternative whenever advanced functionality is required. -

    - -

    Surrounding Ecosystem

    - -

    - These qualities opens Templates up to a number of new use-cases that's better suited than Razor for maintaining - content-heavy websites, live documents, - Email Templates and can easily - introspect the state of running .NET Apps where they provide - valuable insight at a glance with support for - Adhoc querying. -

    - -

    Web Apps

    - -

    - One use-case made possible by Templates we're extremely excited about is Web Apps - a new approach to - dramatically simplify .NET Web App development and provide the most productive development experience possible whilst maximizing - reuse and component sharing. -

    - -

    - Web Apps leverages Templates to develop entire content-rich, data-driven websites without needing to write any C#, compile projects - or manually refresh pages - resulting in the easiest and fastest way to develop Web Apps in .NET! -

    - -

    Pure Cloud Apps

    - -

    - Web Apps also enable the development of Pure Cloud Apps where the same Web App - can be developed and run entirely on AWS S3 and RDS or Azure Blob Storage and SQL Server by just changing - the app.settings that's deployed with the pre-compiled Web App Binary. -

    - -

    Learn ServiceStack Templates

    - -

    - Get started learning ServiceStack Templates by going through the Interactive Guide and creating - a Starter Project. -

    - -

    - View the source code of this - Template Pages generated website to see how clean a website built with it is. -

    - -

    - Checkout the Live LINQ examples for a fun Live programming experience to explore its querying features, as a quick - preview here's a copy of the first LINQ example you can play with below: -

    - -{{ "linq-preview" | partial({ rows: 7, example: "linq01" }) }} - -

    Free for OSS and Commercial Projects

    - -

    - We believe we've only just scratched the surface of what's possible with Templates and we'd love to see what new - use-cases it can help achieve and help encourage an ecosystem of pluggable and reusable filters, so whilst ServiceStack is a - dual-licensed platform with a - commercial license for closed-source projects, the core of Templates is - being developed in ServiceStack.Common which is an unrestricted - library that's free for OSS and commercial usage. -

    - -

    - ServiceStack's existing free-quota restrictions - only applies if you're using OrmLite, - ServiceStack.Redis or exceed the allowed free-quota of - ServiceStack Services. -

    - -{{ "doc-links" | partial({ order: 0 }) }} diff --git a/src/wwwroot/linq-links.html b/src/wwwroot/linq-links.html deleted file mode 100644 index 3c158aa..0000000 --- a/src/wwwroot/linq-links.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/src/wwwroot/linq-preview.html b/src/wwwroot/linq-preview.html deleted file mode 100644 index 0677549..0000000 --- a/src/wwwroot/linq-preview.html +++ /dev/null @@ -1,9 +0,0 @@ -
    -
    - -
    -
    -
    -
    -
    diff --git a/src/wwwroot/linq/index.html b/src/wwwroot/linq/index.html deleted file mode 100644 index 37066fe..0000000 --- a/src/wwwroot/linq/index.html +++ /dev/null @@ -1,37 +0,0 @@ - - -

    - All LINQ Examples are executed using the same TemplateContext instance below: -

    - -
    
    -    LinqContext = new TemplateContext {
    -        Args = {
    -            [TemplateConstants.DefaultDateFormat] = "yyyy/MM/dd",
    -            ["products"] = TemplateQueryData.Products,
    -            ["customers"] = TemplateQueryData.Customers,
    -            ["comparer"] = new CaseInsensitiveComparer(),
    -            ["anagramComparer"] = new AnagramEqualityComparer(),
    -        }
    -    }.Init();
    -
    - -

    - Which makes available the products and customer data sources to each template, all other data - sources are created within the template that uses them. -

    - -

    - The comparer and anagramComparer are examples of custom .NET objects the templates can't create themselves - so they need to defined externally so they're available to the LINQ examples that need them. -

    - -

    - The Args[DefaultDateFormat] changes the default format the dateFormat filter uses if none is provided. -

    - -{{ "linq-links" | partial({ order }) }} - diff --git a/src/wwwroot/live-pages.html b/src/wwwroot/live-pages.html deleted file mode 100644 index 6a8d287..0000000 --- a/src/wwwroot/live-pages.html +++ /dev/null @@ -1,9 +0,0 @@ -
    -
    -{{ files | toList - | select:
    { it.Key }
    \n }} -
    -
    -
    -
    -
    diff --git a/src/wwwroot/live-template.html b/src/wwwroot/live-template.html deleted file mode 100644 index dcff40f..0000000 --- a/src/wwwroot/live-template.html +++ /dev/null @@ -1,9 +0,0 @@ -
    -
    - -
    -
    -
    -
    -
    diff --git a/src/wwwroot/sidebar.html b/src/wwwroot/sidebar.html deleted file mode 100644 index ca80101..0000000 --- a/src/wwwroot/sidebar.html +++ /dev/null @@ -1,34 +0,0 @@ - \ No newline at end of file diff --git a/src/wwwroot/usecase-links.html b/src/wwwroot/usecase-links.html deleted file mode 100644 index 1e6e3dd..0000000 --- a/src/wwwroot/usecase-links.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/src/wwwroot/usecases/adhoc-querying.html b/src/wwwroot/usecases/adhoc-querying.html deleted file mode 100644 index 6a3bb9f..0000000 --- a/src/wwwroot/usecases/adhoc-querying.html +++ /dev/null @@ -1,67 +0,0 @@ - - -

    - The same qualities that make Templates great at - querying State of a running .NET App - also makes it excel at executing adhoc queries against providers which allow free text queries like - OrmLite's Database Filters which enables access to its - Dynamic Result Set APIs: -

    - - - -{{ 'examples/adhoc-query-db.html' | includeFile | assignTo: template }} - -{{ "live-template" | partial({ rows:20, template }) }} - -
    - -

    Register DB Filters

    - -

    - To access OrmLite's Database Filters install the - OrmLite NuGet package for your RDBMS then add the - TemplateDbFiltersAsync - to your TemplateContext and register its required IDbConnectionFactory dependency in its IOC, e.g. for SQL Server: -

    - -{{ 'gfm/adhoc-querying/01.md' | githubMarkdown }} - -
    - If using ServiceStack's TemplatePagesFeature it's Container has already been reassigned to use - ServiceStack's Funq IOC so it only needs to be registered once in ServiceStack's IOC. -
    - -

    Always protect free text APIs

    - -

    - If you're exposing filters enabling a free text API against a production database it should never be accessible by untrusted parties - so you'll want to at a minimum ensure Services are protected with the [Authenticate] attribute so it's only available to - Authenticated Users and - ideally configure it to use a Read Only connection, e.g. for SQLite: -

    - -{{ 'gfm/adhoc-querying/02.md' | githubMarkdown }} - -

    Populating the database

    - -

    - The database queried above was populated in the - AppHost - where it re-uses the LINQ data sources to create and populate an In Memory SQLite database on Startup: -

    - -{{ 'gfm/adhoc-querying/03.md' | githubMarkdown }} - -

    Order Report Example

    - -{{ 'gfm/adhoc-querying/04.md' | githubMarkdown }} - -{{ "usecase-links" | partial({ order }) }} diff --git a/src/wwwroot/usecases/content-websites.html b/src/wwwroot/usecases/content-websites.html deleted file mode 100644 index a1015e4..0000000 --- a/src/wwwroot/usecases/content-websites.html +++ /dev/null @@ -1,122 +0,0 @@ - - -

    - In its simplest form Templates is just plain HTML marked up with variable place holders, coupled with - partials just being HTML pages themselves and the intuitive Cascading Layout Selection - and using Templates becomes a natural solution for building complete websites without any code. -

    - -

    - The flexibility of Templates allows for several different solutions for generating websites: -

    - -

    Website Sub Directory

    - -

    - A lightweight solution that can be embedded in any existing ASP.NET or ASP.NET Core Web Application is to embed - and maintain an entire Website in a stand-alone sub directory. To showcase an example we've ported the content in - the .NET Core Razor Rockstars into the - /usecases/rockstar-files - sub directory with the entire website viewable from: -

    - -

    /rockstar-files

    - -

    Porting an existing Razor Website

    - -

    - Porting an existing Razor Website is fairly straight-forward, to port Razor Rockstars we needed to rename all - .cshtml files back to .html and replace all their C# code. Using - /cobain page as an example we needed to replace: -

    - -
    @{
    -    Layout = "DeadLayout";
    -    ViewBag.Title = "Kurt Cobain";
    -}
    - -

    - To use the equivalent page metadata: -

    - -
    <!--
    -title: Kurt Cobain
    --->
    - -

    - You won't need to specify a custom layout as Templates will automatically select the closest layout from the - page at: - /rockstar-files/dead/_layout.html. - You'll also no longer need to maintain your Layout pages and partials in /Views/Shared separate from your views as they - all get to all live cohesively in the same logical folder structure. -

    - - -

    - The declarative {{ pass: page }} is used to embed a page in a layout instead of the imperative @RenderBody(). - Likewise the syntax for partials changes to {{ pass: "menu-alive" | partial }} from @Html.Partial("MenuAlive"). - Templates also alleviates the need for bespoke partials like @Html.PartialMarkdown("Content") as it can instead leverage the - flexibility of chaining existing filters to achieve the same result like - {{ pass: "content.md" | includeFile | markdown }}. -

    - -

    - To get a feel for what an equivalent Razor Website looks like compared to Templates checkout: -

    - - - - - - - - - - - - - - - - - - - - - - -
    typebeforeafter
    Layout - /Views/Shared/DeadLayout.cshtml - - /dead/_layout.html -
    Partial - /Views/Shared/MenuAlive.cshtml - - /menu-alive.html -
    Page - /alive/vedder/default.cshtml - - /alive/vedder/index.html -
    - -

    Embedding Remote Content

    - -

    - A useful alternative to embedding static file content in pages is to source the content from a external url. Using the - includeUrlWithCache filter this is easy to do with great - performance where you can embed remote url content, cache it for 1 minute and convert from markdown with: -

    - -
    {{ pass: url | includeUrlWithCache | markdown }}
    - -

    - As seen in /grohl-url/index.html - which is viewable at: -

    - -

    /rockstar-files/alive/grohl-url/

    - -{{ "usecase-links" | partial({ order }) }} diff --git a/src/wwwroot/usecases/index.html b/src/wwwroot/usecases/index.html deleted file mode 100644 index 6d62521..0000000 --- a/src/wwwroot/usecases/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - -

    - The ease-of-use, performance, no precompilation, interactivity and sandbox features of Templates makes it useful for a - wide range of use cases. To help stimulate some potential ideas we've included some example use cases below: -

    - -

    Web Apps

    -

    - Build entire content-rich and data-driven .NET Websites in real-time for each major server platform without compilation. -

    - -

    Content Websites

    -

    - Build self-contained content-heavy and documentation websites without any code. -

    - -

    Email Templates

    -

    - Provide marketers a live editing experience to easily generate plain-text and HTML Templates. -

    - -

    Live Documents

    -

    - Use Templates built-in functionality and live interactivity experience to author and preview smart documents in real-time. -

    - -

    Introspect State

    -

    - Use Templates to execute adhoc queries on the state of a running .NET App. -

    - -

    Adhoc Querying

    -

    - Easily query data sources offering free-text APIs like using SQL to query RDBMS's with - OrmLite. -

    - -{{ "usecase-links" | partial({ order }) }} - diff --git a/src/wwwroot/usecases/introspect-state.html b/src/wwwroot/usecases/introspect-state.html deleted file mode 100644 index 73980a9..0000000 --- a/src/wwwroot/usecases/introspect-state.html +++ /dev/null @@ -1,95 +0,0 @@ - - -

    - Since templates are executable at runtime without precompilation it's a great tool for running - live queries to inspect the state of a running .NET App within a controlled window sandbox. - Here's an example of querying a Server's state: -

    - - - -
    - - -
    -
    - -
    -
    -
    - -
    - -
    - -{{ `` | raw | assignTo: scripts }} - -
    - -

    Implementation

    - -

    - To implement IntrospectStateServices.cs - we created a separate Service using a new TemplateContext instance with a custom set of filters which just exposes the APIs - we want to be able to query: -

    - -

    IntrospectStateServices.cs

    - -{{ 'gfm/introspect-state/01.md' | githubMarkdown }} - -

    Client UI

    - -

    - Then to implement the - Client UI - we just used a FORM containing Bootstrap Tabs that only uses this custom javascript: -

    - -{{ 'gfm/introspect-state/02.md' | githubMarkdown }} - -

    - Which calls the generic ajaxPreview jQuery plugin in - default.js - to make an ajax request on every text box change. -

    - -

    Debug Template

    - -

    - As this feature is an extremely useful way to inspect the state of a remote .NET or .NET Core App it's an embedded feature in - ServiceStack which is automatically registered in DebugMode - which can optionally be made available to everyone with: -

    - -{{ 'gfm/introspect-state/03.md' | githubMarkdown }} - -

    - Which we've done in this App so you can inspect this Servers State from: -

    - -

    /metadata/debug

    - - -{{ "usecase-links" | partial({ order }) }} diff --git a/src/wwwroot/usecases/live-documents.html b/src/wwwroot/usecases/live-documents.html deleted file mode 100644 index f855f5f..0000000 --- a/src/wwwroot/usecases/live-documents.html +++ /dev/null @@ -1,20 +0,0 @@ - - -

    - The lack of pre-compilation and its fast startup and runtime performance makes Templates ideal for maintaining - reactive live documents containing a mix of docs, processing and calculations like creating a personal budget: -

    - -{{ 'examples/monthly-budget.txt' | includeFile | assignTo: template }} -{{ "live-template" | partial({ rows:33, template }) }} - - - -{{ "usecase-links" | partial({ order }) }} diff --git a/src/wwwroot/usecases/rockstar-files/alive/_layout.html b/src/wwwroot/usecases/rockstar-files/alive/_layout.html deleted file mode 100644 index e18d704..0000000 --- a/src/wwwroot/usecases/rockstar-files/alive/_layout.html +++ /dev/null @@ -1,40 +0,0 @@ - - - -Alive Rockstars - {{ title }} - - - - - ← Content Websites - -
    -
    -
    Rockstars still rocking it...
    -

    - -

    -
    - -

    {{ title }}

    - - {{ "menu-alive" | partial }} - -
    - {{ page }} - -
    - - {{ "menu-dead" | partial }} -
    - - - diff --git a/src/wwwroot/usecases/rockstar-files/alive/grohl-url/index.html b/src/wwwroot/usecases/rockstar-files/alive/grohl-url/index.html deleted file mode 100644 index 224cdde..0000000 --- a/src/wwwroot/usecases/rockstar-files/alive/grohl-url/index.html +++ /dev/null @@ -1,37 +0,0 @@ - -
    - - Dave Grohl -

    - David Eric "Dave" Grohl is an American rock musician, multi-instrumentalist, and - singer-songwriter, who is the lead vocalist, guitarist, primary songwriter and founder of - the Foo Fighters, prior to ... Wikipedia -

    - -
    -
    - Born -
    -
    - January 14, 1969 (age 48), Warren -
    -
    - Spouse -
    -
    - Jordyn Blum (m. 2003), Jennifer Youngblood (m. 1993-1997) -
    -
    - Music groups -
    -
    - Foo Fighters, Nirvana, Scream, Killing Joke, Melvana -
    -
    - - {{ "https://raw.githubusercontent.com/NetCoreApps/TemplatePages/master/src/wwwroot/usecases/rockstar-files/alive/grohl/content.md" - | includeUrlWithCache | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/alive/grohl/index.html b/src/wwwroot/usecases/rockstar-files/alive/grohl/index.html deleted file mode 100644 index ac91b12..0000000 --- a/src/wwwroot/usecases/rockstar-files/alive/grohl/index.html +++ /dev/null @@ -1,36 +0,0 @@ - -
    - - Dave Grohl -

    - David Eric "Dave" Grohl is an American rock musician, multi-instrumentalist, and - singer-songwriter, who is the lead vocalist, guitarist, primary songwriter and founder of - the Foo Fighters, prior to ... Wikipedia -

    - -
    -
    - Born -
    -
    - January 14, 1969 (age 48), Warren -
    -
    - Spouse -
    -
    - Jordyn Blum (m. 2003), Jennifer Youngblood (m. 1993-1997) -
    -
    - Music groups -
    -
    - Foo Fighters, Nirvana, Scream, Killing Joke, Melvana -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/alive/love/index.html b/src/wwwroot/usecases/rockstar-files/alive/love/index.html deleted file mode 100644 index c40f204..0000000 --- a/src/wwwroot/usecases/rockstar-files/alive/love/index.html +++ /dev/null @@ -1,36 +0,0 @@ - -
    - - Courtney Love -

    - Courtney Michelle Love is an American singer-songwriter, musician, actress and artist. - Love initially gained notoriety in the Los Angeles indie rock scene with her band Hole, - which she formed in 1989 with Eric Erlandson. -

    - -
    -
    - Born -
    -
    - July 9, 1964 (age 53), San Francisco -
    -
    - Height -
    -
    - 5' 10" (1.77 m) -
    -
    - Albums -
    -
    - Nobody's Daughter, America's Sweetheart -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/alive/springsteen/index.html b/src/wwwroot/usecases/rockstar-files/alive/springsteen/index.html deleted file mode 100644 index 19c797a..0000000 --- a/src/wwwroot/usecases/rockstar-files/alive/springsteen/index.html +++ /dev/null @@ -1,35 +0,0 @@ - -
    - - Bruce Springsteen -

    - Bruce Frederick Joseph Springsteen, nicknamed "The Boss", is an American singer-songwriter - and multi-instrumentalist who records and tours with the E Street Band. -

    - -
    -
    - Born -
    -
    - September 23, 1949 (age 67), Long Branch -
    -
    - Spouse -
    -
    - Patti Scialfa (m. 1991), Julianne Phillips (m. 1985-1989) -
    -
    - Music groups -
    -
    - E Street Band, U.S.A. for Africa, Steel Mill -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/alive/vedder/index.html b/src/wwwroot/usecases/rockstar-files/alive/vedder/index.html deleted file mode 100644 index 7944daa..0000000 --- a/src/wwwroot/usecases/rockstar-files/alive/vedder/index.html +++ /dev/null @@ -1,35 +0,0 @@ - -
    - - Eddie Vedder -

    - Eddie Vedder is an American musician and singer-songwriter who is best known for being the - lead singer and one of three guitarists of the alternative rock band Pearl Jam. -

    - -
    -
    - Born -
    -
    - December 23, 1964 (age 52), Evanston -
    -
    - Spouse -
    -
    - Jill McCormick (m. 2010), Beth Liebling (m. 1994-2000) -
    -
    - Music groups -
    -
    - Pearl Jam, Temple of the Dog, Bad Radio, Hovercraft -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/dead/_layout.html b/src/wwwroot/usecases/rockstar-files/dead/_layout.html deleted file mode 100644 index fdbeded..0000000 --- a/src/wwwroot/usecases/rockstar-files/dead/_layout.html +++ /dev/null @@ -1,40 +0,0 @@ - - - -Dead Rockstars - {{ title }} - - - - - ← Content Websites - -
    -
    -
    Rocking the grave...
    -

    - -

    -
    - -

    {{ title }}

    - - {{ "menu-dead" | partial }} - -
    - {{ page }} - -
    - - {{ "menu-alive" | partial }} -
    - - - diff --git a/src/wwwroot/usecases/rockstar-files/dead/cobain/index.html b/src/wwwroot/usecases/rockstar-files/dead/cobain/index.html deleted file mode 100644 index 95bfe3c..0000000 --- a/src/wwwroot/usecases/rockstar-files/dead/cobain/index.html +++ /dev/null @@ -1,41 +0,0 @@ - -
    - - Kurt Cobain -

    - Kurt Donald Cobain was an American singer-songwriter, musician and artist, best known as the - lead singer and guitarist of the grunge band Nirvana. -

    - -
    -
    - Born -
    -
    - February 20, 1967, Aberdeen -
    -
    - Died -
    -
    - April 5, 1994, Seattle -
    -
    - Spouse -
    -
    - Courtney Love (m. 1992-1994) -
    -
    - Music groups -
    -
    - Nirvana -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/dead/hendrix/index.html b/src/wwwroot/usecases/rockstar-files/dead/hendrix/index.html deleted file mode 100644 index 7992dda..0000000 --- a/src/wwwroot/usecases/rockstar-files/dead/hendrix/index.html +++ /dev/null @@ -1,30 +0,0 @@ - -
    - - Jimi Hendrix -

    - James Marshall "Jimi" Hendrix was an American musician and singer-songwriter. - He is widely considered to be the greatest electric guitarist in music history and one of - the most influential musicians of ... -

    - -
    -
    - Born -
    -
    - November 27, 1942, Seattle -
    -
    - Died -
    -
    - September 18, 1970, London -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    \ No newline at end of file diff --git a/src/wwwroot/usecases/rockstar-files/dead/jackson/index.html b/src/wwwroot/usecases/rockstar-files/dead/jackson/index.html deleted file mode 100644 index 10270f9..0000000 --- a/src/wwwroot/usecases/rockstar-files/dead/jackson/index.html +++ /dev/null @@ -1,36 +0,0 @@ - -
    - - Michael Jackson -

    - Michael Joseph Jackson was an American recording artist, entertainer, and businessman. - Often referred to as the King of Pop, or by his initials MJ, Jackson is recognized as the - most successful entertainer of all time by Guinness World Records. -

    - -
    -
    - Born -
    -
    - August 29, 1958, Gary -
    -
    - Died -
    -
    - June 25, 2009, UCLA Medical Center -
    -
    - Albums -
    -
    - Thriller, Immortal, Bad, Number Ones, Dangerous -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/dead/joplin/index.html b/src/wwwroot/usecases/rockstar-files/dead/joplin/index.html deleted file mode 100644 index b1e3ad5..0000000 --- a/src/wwwroot/usecases/rockstar-files/dead/joplin/index.html +++ /dev/null @@ -1,36 +0,0 @@ - -
    - - Janis Joplin -

    - Janis Lyn Joplin was an American singer and songwriter from Port Arthur, Texas. As a youth - Joplin was ridiculed by her fellow students due to her unconventional appearance and - personal beliefs. -

    - -
    -
    - Born -
    -
    - January 19, 1943, Port Arthur -
    -
    - Died -
    -
    - October 4, 1970, Los Angeles -
    -
    - Nicknames -
    -
    - Beat Weeds, Pearl -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/dead/presley/index.html b/src/wwwroot/usecases/rockstar-files/dead/presley/index.html deleted file mode 100644 index 707970d..0000000 --- a/src/wwwroot/usecases/rockstar-files/dead/presley/index.html +++ /dev/null @@ -1,36 +0,0 @@ - -
    - - Elvis Presley -

    - Elvis Aaron Presley was one of the most popular American singers of the 20th century. - A cultural icon, he is commonly known by the single name Elvis. He is often referred to - as the "King of Rock and Roll" or simply "the King". -

    - -
    -
    - Born -
    -
    - January 8, 1935, Tupelo -
    -
    - Died -
    -
    - August 16, 1977, Memphis -
    -
    - Spouse -
    -
    - Priscilla Presley (m. 1967-1973) -
    -
    - - {{ "content.md" | includeFile | markdown }} - -
    diff --git a/src/wwwroot/usecases/rockstar-files/menu-alive.html b/src/wwwroot/usecases/rockstar-files/menu-alive.html deleted file mode 100644 index 9b9c7d4..0000000 --- a/src/wwwroot/usecases/rockstar-files/menu-alive.html +++ /dev/null @@ -1,14 +0,0 @@ -{{ { - "/usecases/rockstar-files/alive/grohl/": "Dave Grohl", - "/usecases/rockstar-files/alive/vedder/": "Eddie Vedder", - "/usecases/rockstar-files/alive/springsteen/": "Bruce Springsteen", - "/usecases/rockstar-files/alive/love/": "Courtney Love" -} | assignTo: links }} - - diff --git a/src/wwwroot/usecases/rockstar-files/menu-dead.html b/src/wwwroot/usecases/rockstar-files/menu-dead.html deleted file mode 100644 index e22a39d..0000000 --- a/src/wwwroot/usecases/rockstar-files/menu-dead.html +++ /dev/null @@ -1,15 +0,0 @@ -{{ { - "/usecases/rockstar-files/dead/cobain/": "Kurt Cobain", - "/usecases/rockstar-files/dead/hendrix/": "Jimi Hendrix", - "/usecases/rockstar-files/dead/jackson/": "Michael Jackson", - "/usecases/rockstar-files/dead/joplin/": "Janis Joplin", - "/usecases/rockstar-files/dead/presley/": "Elvis Presley" -} | assignTo: links }} - - diff --git a/wip/script-net.ss b/wip/script-net.ss new file mode 100644 index 0000000..935659c --- /dev/null +++ b/wip/script-net.ss @@ -0,0 +1,19 @@ +Type Examples + +{{#function info(o) }} + `${o.getType().typeQualifiedName()} '${o}'` |> raw |> return +{{/function}} + +```code +'DateTime'.new() | info +'int'.new() | info +'Int32'.new() | info +'System.Text.StringBuilder'.new() | info +'Dictionary'.new() | info + +'Ints'.new([1,2]) +'Ints'.new([1.0,2.0]) +'KeyValuePair'.new(['A',1]) +'KeyValuePair'.new(['A',1]) + +``` diff --git a/wwwroot/_layout.html b/wwwroot/_layout.html new file mode 100644 index 0000000..15f8fd5 --- /dev/null +++ b/wwwroot/_layout.html @@ -0,0 +1,76 @@ + + + + + {{ title ?? "Script" }} + + + + + + + + + + + + +
    + +
    + + {{ "sidebar" |> partial }} + +
    +

    {{ title }}

    + {{ page }} +
    + +
    + +

    made with by ServiceStack

    + + + + + + + + {{ scripts |> raw }} + + + + + + + + \ No newline at end of file diff --git a/wwwroot/_net-usage-partial.html b/wwwroot/_net-usage-partial.html new file mode 100644 index 0000000..135a4a8 --- /dev/null +++ b/wwwroot/_net-usage-partial.html @@ -0,0 +1,29 @@ +{{ 'gfm/introduction/11.md' |> githubMarkdown }} + +

    + Where you can customize the pure sandboxed ScriptContext your Script is executed within by extending it with: +

    + + diff --git a/wwwroot/apps-links.html b/wwwroot/apps-links.html new file mode 100644 index 0000000..4c6664c --- /dev/null +++ b/wwwroot/apps-links.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/src/wwwroot/assets/css/bootstrap.css b/wwwroot/assets/css/bootstrap.css similarity index 100% rename from src/wwwroot/assets/css/bootstrap.css rename to wwwroot/assets/css/bootstrap.css diff --git a/src/wwwroot/assets/css/default.css b/wwwroot/assets/css/default.css similarity index 89% rename from src/wwwroot/assets/css/default.css rename to wwwroot/assets/css/default.css index a03f77d..a8fab6c 100644 --- a/src/wwwroot/assets/css/default.css +++ b/wwwroot/assets/css/default.css @@ -82,8 +82,8 @@ a:hover { top: 63px; left: 0; bottom: 0; - overflow-x: hidden; - overflow-y: auto; + overflow-x: scroll; + overflow-y: scroll; -webkit-overflow-scrolling: touch; -ms-overflow-style: none; } @@ -189,6 +189,9 @@ blockquote { border-left: 0.25rem solid #eceeef; background: #f8f8f8; } +blockquote > p { + margin: 0; +} caption { caption-side: top; text-align: center; @@ -197,6 +200,9 @@ caption { .html-sm { font-size: .75rem; } +.no-pre, .no-pre .output { + white-space: normal; +} .html { font-size: .8rem; } @@ -231,8 +237,28 @@ caption { display: none; } -::-webkit-scrollbar { width:7px; height:5px } -::-webkit-scrollbar-thumb { background-color:#ccc; } +#sidebar::-webkit-scrollbar +{ + width:10px; + height:15px; +} +#sidebar::-webkit-scrollbar-thumb +{ + background-color: #f8f8f8; +} + +.youtube-play { + background: no-repeat url(/assets/img/youtube-play.png); + display: block; + position: absolute; + opacity: 0.8; +} +.youtube-play:hover { + background: no-repeat url(/assets/img/youtube-play-hover.png); + opacity: 1; + background-color: rgba(255,255,255,.1); + cursor: pointer; +} @media (max-width: 1366px) { #content { diff --git a/src/wwwroot/assets/css/gfm.css b/wwwroot/assets/css/gfm.css similarity index 98% rename from src/wwwroot/assets/css/gfm.css rename to wwwroot/assets/css/gfm.css index 272ab8e..15a3881 100644 --- a/src/wwwroot/assets/css/gfm.css +++ b/wwwroot/assets/css/gfm.css @@ -761,3 +761,18 @@ .gfm .vg { color: #dd7700 } /* Name.Variable.Global */ .gfm .vi { color: #3333bb } /* Name.Variable.Instance */ .gfm .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */ + +/* Custom */ + +.gfm blockquote p { + padding: .5em 0; +} +#content .gfm h2, #content .gfm h3, #content .gfm h4 { + padding: 0; +} +.gfm .h-link { + cursor: pointer; +} +.gfm h2.h-link:hover:after, .gfm h3.h-link:hover:after, .gfm h4.h-link:hover:after { + content: " \00B6"; +} \ No newline at end of file diff --git a/src/wwwroot/assets/img/favicon.png b/wwwroot/assets/img/favicon.png similarity index 85% rename from src/wwwroot/assets/img/favicon.png rename to wwwroot/assets/img/favicon.png index 08e81b4..b8f2b8a 100644 Binary files a/src/wwwroot/assets/img/favicon.png and b/wwwroot/assets/img/favicon.png differ diff --git a/wwwroot/assets/img/logo.svg b/wwwroot/assets/img/logo.svg new file mode 100644 index 0000000..6a0c6f2 --- /dev/null +++ b/wwwroot/assets/img/logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/wwwroot/assets/img/multi-langs.svg b/wwwroot/assets/img/multi-langs.svg new file mode 100644 index 0000000..13ea05d --- /dev/null +++ b/wwwroot/assets/img/multi-langs.svg @@ -0,0 +1,3 @@ + + +
    page text {{ 1 + 2 + 3 }}
    page text {{ 1 + 2 + 3 }}
    ```lisp
    (+ 1 2 3)
    ```
    [Not supported by viewer]
    ```code
    1 + 2 + 3
    ```
    [Not supported by viewer]
    more text and whitespace
    more text and whitespace
    {|lisp (* 1 2 3) |}
    {|lisp (* 1 2 3) |}
    template
    template
    code
    code
    lisp
    lisp
    fragment
    fragment
    fragment (JsStatement[])
    fragment (JsStatement[])
    fragment
    fragment
    fragment (SExpression[])
    fragment (SExpression[])
    fragment
    fragment
    fragment (SExpression[])
    fragment (SExpression[])
    template
    template
    code
    code
    lisp
    lisp
    page output
    page output
    PageResult
    PageResult
    \ No newline at end of file diff --git a/wwwroot/assets/img/query-github.png b/wwwroot/assets/img/query-github.png new file mode 100644 index 0000000..1c71bc1 Binary files /dev/null and b/wwwroot/assets/img/query-github.png differ diff --git a/wwwroot/assets/img/sandbox.svg b/wwwroot/assets/img/sandbox.svg new file mode 100644 index 0000000..51ee742 --- /dev/null +++ b/wwwroot/assets/img/sandbox.svg @@ -0,0 +1,3 @@ + + +
    #Script
    #Script
    methods
    methods
    blocks
    blocks
    arguments
    arguments
    #Script
    #Script
    methods
    methods
    blocks
    blocks
    arguments
    arguments
    #Script
    #Script
    methods
    methods
    blocks
    blocks
    arguments
    arguments
    ScriptAssemblies
    ScriptAssemblies
    ScriptTypes
    ScriptTypes
    AllowScriptingOfAllTypes=true
    AllowScriptingOfAllTypes=true
    .dll
    .dll
    .dll
    .dll
    .dll
    .dll
    .dll
    .dll
    netfx / netcoreapp
    netfx / netcoreapp
    1. Default
    1. Default
    2. Script Assemblies
    2. Script Assemblies
    3. Script all .NET
    3. Script all .NET
    \ No newline at end of file diff --git a/wwwroot/assets/img/screenshot.png b/wwwroot/assets/img/screenshot.png new file mode 100644 index 0000000..aa54607 Binary files /dev/null and b/wwwroot/assets/img/screenshot.png differ diff --git a/wwwroot/assets/img/screenshots/app-init.png b/wwwroot/assets/img/screenshots/app-init.png new file mode 100644 index 0000000..8dfda0c Binary files /dev/null and b/wwwroot/assets/img/screenshots/app-init.png differ diff --git a/src/wwwroot/assets/img/screenshots/bare.png b/wwwroot/assets/img/screenshots/bare.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/bare.png rename to wwwroot/assets/img/screenshots/bare.png diff --git a/src/wwwroot/assets/img/screenshots/blog.png b/wwwroot/assets/img/screenshots/blog.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/blog.png rename to wwwroot/assets/img/screenshots/blog.png diff --git a/src/wwwroot/assets/img/screenshots/chat.png b/wwwroot/assets/img/screenshots/chat.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/chat.png rename to wwwroot/assets/img/screenshots/chat.png diff --git a/src/wwwroot/assets/img/screenshots/metadata-debug.png b/wwwroot/assets/img/screenshots/metadata-debug.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/metadata-debug.png rename to wwwroot/assets/img/screenshots/metadata-debug.png diff --git a/src/wwwroot/assets/img/screenshots/plugins.png b/wwwroot/assets/img/screenshots/plugins.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/plugins.png rename to wwwroot/assets/img/screenshots/plugins.png diff --git a/src/wwwroot/assets/img/screenshots/redis-html.png b/wwwroot/assets/img/screenshots/redis-html.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/redis-html.png rename to wwwroot/assets/img/screenshots/redis-html.png diff --git a/src/wwwroot/assets/img/screenshots/redis.png b/wwwroot/assets/img/screenshots/redis.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/redis.png rename to wwwroot/assets/img/screenshots/redis.png diff --git a/src/wwwroot/assets/img/screenshots/rockwind.png b/wwwroot/assets/img/screenshots/rockwind.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/rockwind.png rename to wwwroot/assets/img/screenshots/rockwind.png diff --git a/src/wwwroot/assets/img/screenshots/spirals.png b/wwwroot/assets/img/screenshots/spirals.png similarity index 100% rename from src/wwwroot/assets/img/screenshots/spirals.png rename to wwwroot/assets/img/screenshots/spirals.png diff --git a/wwwroot/assets/img/screenshots/ssvs-bootstrap.png b/wwwroot/assets/img/screenshots/ssvs-bootstrap.png new file mode 100644 index 0000000..368af19 Binary files /dev/null and b/wwwroot/assets/img/screenshots/ssvs-bootstrap.png differ diff --git a/wwwroot/assets/img/youtube-play-hover.png b/wwwroot/assets/img/youtube-play-hover.png new file mode 100644 index 0000000..bddb0ef Binary files /dev/null and b/wwwroot/assets/img/youtube-play-hover.png differ diff --git a/wwwroot/assets/img/youtube-play.png b/wwwroot/assets/img/youtube-play.png new file mode 100644 index 0000000..066433e Binary files /dev/null and b/wwwroot/assets/img/youtube-play.png differ diff --git a/src/wwwroot/assets/js/bootstrap.min.js b/wwwroot/assets/js/bootstrap.min.js similarity index 100% rename from src/wwwroot/assets/js/bootstrap.min.js rename to wwwroot/assets/js/bootstrap.min.js diff --git a/src/wwwroot/assets/js/customers.json b/wwwroot/assets/js/customers.json similarity index 100% rename from src/wwwroot/assets/js/customers.json rename to wwwroot/assets/js/customers.json diff --git a/wwwroot/assets/js/default.js b/wwwroot/assets/js/default.js new file mode 100644 index 0000000..a65431f --- /dev/null +++ b/wwwroot/assets/js/default.js @@ -0,0 +1,231 @@ +$('.lang-select').each(function(){ + var qs = queryStringParams(); + var lang = qs['lang'] || 'template'; + var el = $(this); + el.find("input[type=radio]").on("change", function(){ + qs['lang'] = this.id === 'template' ? null : this.id; + var url = location.href.split('?')[0]; + for (var k in qs) { + if (!qs[k]) continue; + url += url.indexOf('?' >= 0) ? "?" : "&"; + url += k + "=" + encodeURIComponent(qs[k]); + } + location.href = url; + }); + el.find("input[type=radio]").each(function() { + this.checked = this.id === lang; + if (this.checked) + $(this).parents("label").addClass('active'); + else + $(this).parents("label").removeClass('active'); + }) +}) + +$(".live-pages").each(function(){ + + var el = $(this) + el.find("textarea").on("input", function(){ + var page = el.data("page") + var files = {} + + el.find(".files section").each(function(){ + var name = $.trim($(this).find("h5").html()) + var contents = $(this).find("textarea").val() + files[name] = contents + }) + + var request = { files: files, page: page } + + $.ajax({ + type: "POST", + url: "/pages/eval", + data: JSON.stringify(request), + contentType: "application/json", + dataType: "html" + }).done(function(data){ + el.removeClass('error').find(".output").html(data) + }).fail(function(jqxhr){ handleError(el, jqxhr) }) + }) + .trigger("input") + +}) + +$(".live-template").each(function(){ + + var el = $(this) + el.find("textarea").on("input", function(){ + + var request = { template: el.find("textarea").val() } + + $.ajax({ + type: "POST", + url: "/template/eval" + location.search, + data: JSON.stringify(request), + contentType: "application/json", + dataType: "html" + }).done(function(data){ + el.removeClass('error').find(".output").html(data) + }).fail(function(jqxhr){ handleError(el, jqxhr) }) + }) + .trigger("input") + +}) + +$(".linq-preview").each(function(){ + var files = {} + + var el = $(this) + var lang = $(this).data('lang'); + el.find("textarea").on("input", function(){ + var files = {} + + el.find(".files section").each(function(){ + var name = $.trim($(this).find("h5").html()) + var contents = $(this).find("textarea").val() + files[name] = contents + }) + + var request = { code: el.find(".template textarea").val(), files: files } + var qsLang = lang ? "?lang=" + lang : ""; + + $.ajax({ + type: "POST", + url: "/linq/eval" + qsLang, + data: JSON.stringify(request), + contentType: "application/json", + dataType: "html" + }).done(function(data){ + el.removeClass('error').find(".output").html(data); + }).fail(function(jqxhr){ handleError(el, jqxhr) }) + }) + .trigger("input") + +}) + +$("#content h2,#content h3,#content h4").each(function(){ + var el = $(this); + + var text = el.html(); + if (text.indexOf("<") >= 0) return; + + if (!el.attr('id')) { + var safeName = text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9_-]+/g,"") + el.attr('id', safeName) + } + + el.on('click', function(){ + var id = el.attr('id') + location.href = "#" + id + }) +}) + +$.fn.ajaxPreview = function(opt) { + var inputs = this.find("input,textarea"); + inputs.on("input", function(){ + var f = $(this).closest("form"); + var data = {}; + inputs.each(function(){ data[this.name] = this.value }) + $.ajax({ + url: f.attr('action'), + method: "POST", + data: JSON.stringify(data), + contentType: 'application/json', + dataType: opt.dataType || 'json', + success: opt.success, + error: opt.error || function(jqxhr,status,errMsg) { handleError(el, jqxhr) } + }) + }) + .first().trigger("input") + + return this.each(function(){ + $(this).submit(function(e){ e.preventDefault() }) + }); +} + +function handleError(el, jqxhr) { + try { + console.log('template error:', jqxhr.status, jqxhr.statusText) + el.addClass('error') + var errorResponse = JSON.parse(jqxhr.responseText); + var status = errorResponse.responseStatus; + if (status) { + el.find('.output').html('
    ' + status.errorCode + ' ' + status.message +
    +             '\n\nStackTrace:\n' + status.stackTrace + '
    ') + } + } catch(e) { + el.find('.output').html('
    ' + jqxhr.status + ' ' + jqxhr.statusText + '
    ') + } +} + +function queryStringParams(qs) { + qs = (qs || document.location.search).split('+').join(' ') + var params = {}, tokens, re = /[?&]?([^=]+)=([^&]*)/g + while (tokens = re.exec(qs)) { + params[tokens[1]] = tokens[2]; + } + return params; +} + + +function video(url) { + return ''; +} +function playVideo(el,id) { + var url = 'https://www.youtube.com/embed/' + id + '?autoplay=1'; + $(el).parent().html(video(url)); +} + +function splitOnFirst(s, c) { + if (!s) return [s]; + var pos = s.indexOf(c); + return pos >= 0 ? [s.substring(0, pos), s.substring(pos + 1)] : [s]; +} +function splitOnLast(s, c) { + if (!s) return [s]; + var pos = s.lastIndexOf(c); + return pos >= 0 + ? [s.substring(0, pos), s.substring(pos + 1)] + : [s]; +} +function preloadImage(src) { + var img = new Image(); + img.src = src; +} + +function bindYouTubePlay(img) { + var a = img.parent(); + var p = a.parent(); + if (p[0] && p[0].tagName === 'P') { + var id = splitOnFirst(splitOnLast(a.attr('href'),'/')[1],'?')[0]; + var width = Math.floor(img[0].offsetWidth/2 - 43); + var height = Math.floor(img[0].offsetHeight/2 - 31); + var html = ``; + p.prepend(html); + } +} + +function bindYouTubeImages() { + preloadImage('/images/youtube-play-hover.png'); + $("a[href^='https://youtu.be/']>img").each(function() { + if (this.complete) { + bindYouTubePlay($(this)); + } else { + this.onload = function() { bindYouTubePlay($(this)); } + } + }); +} + +$(bindYouTubeImages); + +$(function(){ + $("a[name*='app://']").each(function() { + this.href = 'app://' + splitOnFirst(this.getAttribute('name'), '://')[1]; + }); + $(".gfm h2 .anchor,.gfm h3 .anchor,.gfm h4 .anchor").each(function() { + var href = this.href; + if (!href) return; + var h = $(this).parent(); + h.addClass('h-link'); + h.on('click', function() { location.href = href; }); + }) +}); diff --git a/src/wwwroot/assets/js/jquery-3.2.1.min.js b/wwwroot/assets/js/jquery-3.2.1.min.js similarity index 100% rename from src/wwwroot/assets/js/jquery-3.2.1.min.js rename to wwwroot/assets/js/jquery-3.2.1.min.js diff --git a/wwwroot/code/linq01-langs.ss b/wwwroot/code/linq01-langs.ss new file mode 100644 index 0000000..a151874 --- /dev/null +++ b/wwwroot/code/linq01-langs.ss @@ -0,0 +1,11 @@ +{{ var numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] }} + +code: +```code +#each numbers where it < 5 + it +/each +``` + +lisp: +{|lisp (joinln (where #(< % 5) numbers)) |} \ No newline at end of file diff --git a/wwwroot/code/linq01.l b/wwwroot/code/linq01.l new file mode 100644 index 0000000..3f5b748 --- /dev/null +++ b/wwwroot/code/linq01.l @@ -0,0 +1,3 @@ +"Numbers < 5:" +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (joinln (where #(< % 5) numbers))) diff --git a/wwwroot/code/linq01.sc b/wwwroot/code/linq01.sc new file mode 100644 index 0000000..92937fe --- /dev/null +++ b/wwwroot/code/linq01.sc @@ -0,0 +1,5 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`Numbers < 5:` +#each numbers where it < 5 + it +/each diff --git a/wwwroot/code/linq01.ss b/wwwroot/code/linq01.ss new file mode 100644 index 0000000..3a51c82 --- /dev/null +++ b/wwwroot/code/linq01.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +Numbers < 5: +{{#each numbers where it < 5}} + {{it}} +{{/each}} diff --git a/wwwroot/code/linq02.l b/wwwroot/code/linq02.l new file mode 100644 index 0000000..cf01178 --- /dev/null +++ b/wwwroot/code/linq02.l @@ -0,0 +1,4 @@ +(let ( (sold-out-products (where #(= 0 (.UnitsInStock %)) products-list)) ) + (println "Sold out products:") + (doseq (p sold-out-products) + (println (.ProductName p) " is sold out") )) \ No newline at end of file diff --git a/wwwroot/code/linq02.sc b/wwwroot/code/linq02.sc new file mode 100644 index 0000000..1af9b12 --- /dev/null +++ b/wwwroot/code/linq02.sc @@ -0,0 +1,4 @@ +`Sold out products:` +#each products where UnitsInStock == 0 + `${ProductName} is sold out!` +/each diff --git a/wwwroot/code/linq02.ss b/wwwroot/code/linq02.ss new file mode 100644 index 0000000..ebccd86 --- /dev/null +++ b/wwwroot/code/linq02.ss @@ -0,0 +1,4 @@ +Sold out products: +{{#each products where UnitsInStock == 0}} + {{ ProductName }} is sold out! +{{/each}} diff --git a/wwwroot/code/linq03.l b/wwwroot/code/linq03.l new file mode 100644 index 0000000..d76c7a5 --- /dev/null +++ b/wwwroot/code/linq03.l @@ -0,0 +1,3 @@ +(println "In-stock products that cost more than 3.00:") +(doseq (p (where #(and (> (.UnitsInStock %) 0) (> (.UnitPrice %) 3)) products-list)) + (println (.ProductName p) " is in stock and costs more than 3.00")) \ No newline at end of file diff --git a/wwwroot/code/linq03.sc b/wwwroot/code/linq03.sc new file mode 100644 index 0000000..b77aebd --- /dev/null +++ b/wwwroot/code/linq03.sc @@ -0,0 +1,4 @@ +`In-stock products that cost more than 3.00:` +#each products where UnitsInStock > 0 && UnitPrice > 3.00 + `${ProductName} is in stock and costs more than 3.00.` +/each \ No newline at end of file diff --git a/src/wwwroot/code/linq03.txt b/wwwroot/code/linq03.ss similarity index 100% rename from src/wwwroot/code/linq03.txt rename to wwwroot/code/linq03.ss diff --git a/wwwroot/code/linq04-customer.ss b/wwwroot/code/linq04-customer.ss new file mode 100644 index 0000000..5886a9b --- /dev/null +++ b/wwwroot/code/linq04-customer.ss @@ -0,0 +1,2 @@ +Customer {{ it.CustomerId }} {{ it.CompanyName |> raw }} +{{ it.Orders |> selectPartial: order }} \ No newline at end of file diff --git a/wwwroot/code/linq04-order.ss b/wwwroot/code/linq04-order.ss new file mode 100644 index 0000000..1a640de --- /dev/null +++ b/wwwroot/code/linq04-order.ss @@ -0,0 +1 @@ + Order {{ it.OrderId }}: {{ it.OrderDate |> dateFormat }} diff --git a/wwwroot/code/linq04-partials.ss b/wwwroot/code/linq04-partials.ss new file mode 100644 index 0000000..e7c3ea5 --- /dev/null +++ b/wwwroot/code/linq04-partials.ss @@ -0,0 +1,3 @@ +{{ customers |> where => it.Region == 'WA' |> to => waCustomers }} +Customers from Washington and their orders: +{{ waCustomers |> selectPartial: customer }} \ No newline at end of file diff --git a/wwwroot/code/linq04.l b/wwwroot/code/linq04.l new file mode 100644 index 0000000..dddc8f4 --- /dev/null +++ b/wwwroot/code/linq04.l @@ -0,0 +1,5 @@ +(println "Customers from Washington and their orders:") +(doseq (c (where #(= (.Region %) "WA") customers-list)) + (println "Customer " (.CustomerId c) ": " (.CompanyName c) ": ") + (doseq (o (.Orders c)) + (println " Order " (.OrderId o) ": " (.OrderDate o)) )) \ No newline at end of file diff --git a/wwwroot/code/linq04.sc b/wwwroot/code/linq04.sc new file mode 100644 index 0000000..152fce2 --- /dev/null +++ b/wwwroot/code/linq04.sc @@ -0,0 +1,7 @@ +`Customers from Washington and their orders:` +#each c in customers where c.Region == 'WA' +` Customer ${c.CustomerId} ${c.CompanyName}` + #each c.Orders + ` Order ${OrderId}: ${OrderDate.dateFormat()}` + /each +/each \ No newline at end of file diff --git a/wwwroot/code/linq04.ss b/wwwroot/code/linq04.ss new file mode 100644 index 0000000..799b905 --- /dev/null +++ b/wwwroot/code/linq04.ss @@ -0,0 +1,7 @@ +Customers from Washington and their orders: +{{#each c in customers where c.Region == 'WA'}} +Customer {{ c.CustomerId }} {{ c.CompanyName |> raw }} +{{#each c.Orders}} + Order {{ OrderId }}: {{ OrderDate |> dateFormat }} +{{/each}} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq05.l b/wwwroot/code/linq05.l new file mode 100644 index 0000000..e2c3453 --- /dev/null +++ b/wwwroot/code/linq05.l @@ -0,0 +1,4 @@ +(let ( (digits ["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"]) ) + (println "Short digits:") + (doseq (d (filter-index #(< (length %1) %2) digits)) + (println "The word " d " is shorter than its value"))) \ No newline at end of file diff --git a/wwwroot/code/linq05.sc b/wwwroot/code/linq05.sc new file mode 100644 index 0000000..163bf7e --- /dev/null +++ b/wwwroot/code/linq05.sc @@ -0,0 +1,5 @@ +['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => digits +`Short digits:` +#each d in digits where d.Length < index + `The word ${d} is shorter than its value.` +/each diff --git a/wwwroot/code/linq05.ss b/wwwroot/code/linq05.ss new file mode 100644 index 0000000..8cff9dd --- /dev/null +++ b/wwwroot/code/linq05.ss @@ -0,0 +1,5 @@ +{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => digits }} +Short digits: +{{#each d in digits where d.Length < index}} + The word {{d}} is shorter than its value. +{{/each}} diff --git a/wwwroot/code/linq06.l b/wwwroot/code/linq06.l new file mode 100644 index 0000000..9d2ea79 --- /dev/null +++ b/wwwroot/code/linq06.l @@ -0,0 +1,3 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "Numbers + 1:") + (joinln (map inc numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq06.sc b/wwwroot/code/linq06.sc new file mode 100644 index 0000000..55564d1 --- /dev/null +++ b/wwwroot/code/linq06.sc @@ -0,0 +1,5 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`Numbers + 1:` +#each numbers + it + 1 +/each \ No newline at end of file diff --git a/wwwroot/code/linq06.ss b/wwwroot/code/linq06.ss new file mode 100644 index 0000000..a3e3fe0 --- /dev/null +++ b/wwwroot/code/linq06.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +Numbers + 1: +{{#each numbers}} + {{ it + 1 }} +{{/each}} diff --git a/wwwroot/code/linq07.l b/wwwroot/code/linq07.l new file mode 100644 index 0000000..f8b8ca2 --- /dev/null +++ b/wwwroot/code/linq07.l @@ -0,0 +1,2 @@ +(println "Product Names:") +(joinln (map .ProductName products-list)) \ No newline at end of file diff --git a/wwwroot/code/linq07.sc b/wwwroot/code/linq07.sc new file mode 100644 index 0000000..2ecc5f4 --- /dev/null +++ b/wwwroot/code/linq07.sc @@ -0,0 +1,4 @@ +`Product Names:` +#each products + ProductName +/each \ No newline at end of file diff --git a/wwwroot/code/linq07.ss b/wwwroot/code/linq07.ss new file mode 100644 index 0000000..077731a --- /dev/null +++ b/wwwroot/code/linq07.ss @@ -0,0 +1,4 @@ +Product Names: +{{#each products}} + {{ProductName}} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq08.l b/wwwroot/code/linq08.l new file mode 100644 index 0000000..5859d6e --- /dev/null +++ b/wwwroot/code/linq08.l @@ -0,0 +1,4 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) + (strings ["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"]) ) + (println "Number strings:") + (joinln (map #(nth strings %) numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq08.sc b/wwwroot/code/linq08.sc new file mode 100644 index 0000000..15b7ef3 --- /dev/null +++ b/wwwroot/code/linq08.sc @@ -0,0 +1,6 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => strings +`Number strings:` +#each n in numbers + strings[n] +/each \ No newline at end of file diff --git a/wwwroot/code/linq08.ss b/wwwroot/code/linq08.ss new file mode 100644 index 0000000..290dbe0 --- /dev/null +++ b/wwwroot/code/linq08.ss @@ -0,0 +1,6 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => strings }} +Number strings: +{{#each n in numbers}} +{{strings[n]}} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq09.l b/wwwroot/code/linq09.l new file mode 100644 index 0000000..52adddc --- /dev/null +++ b/wwwroot/code/linq09.l @@ -0,0 +1,3 @@ +(let ( (words ["aPPLE" "BlUeBeRrY" "cHeRry"]) ) + (doseq (ul (map #(it { :lower (lower-case %) :upper (upper-case %) } ) words)) + (println "Uppercase: " (:upper ul) ", Lowercase: " (:lower ul)))) \ No newline at end of file diff --git a/wwwroot/code/linq09.sc b/wwwroot/code/linq09.sc new file mode 100644 index 0000000..f1956d2 --- /dev/null +++ b/wwwroot/code/linq09.sc @@ -0,0 +1,5 @@ +['aPPLE', 'BlUeBeRrY', 'cHeRry'] |> to => words +words |> map => { Uppercase: it.upper(), Lowercase: it.lower() } |> to => upperLowerWords +#each ul in upperLowerWords + `Uppercase: ${ul.Uppercase}, Lowercase: ${ul.Lowercase}` +/each \ No newline at end of file diff --git a/wwwroot/code/linq09.ss b/wwwroot/code/linq09.ss new file mode 100644 index 0000000..9d59608 --- /dev/null +++ b/wwwroot/code/linq09.ss @@ -0,0 +1,5 @@ +{{ ['aPPLE', 'BlUeBeRrY', 'cHeRry'] |> to => words }} +{{ words |> map => {Uppercase: it.upper(), Lowercase: it.lower()} |> to => upperLowerWords }} +{{#each ul in upperLowerWords}} +{{ `Uppercase: ${ul.Uppercase}, Lowercase: ${ul.Lowercase}` }} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq10.l b/wwwroot/code/linq10.l new file mode 100644 index 0000000..30e4393 --- /dev/null +++ b/wwwroot/code/linq10.l @@ -0,0 +1,4 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) + (strings ["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"]) ) + (doseq (d (map #(it { :digit (nth strings %) :even (even? %) }) numbers)) + (println "The digit " (:digit d) " is " (if (:even d) "even" "odd")) )) \ No newline at end of file diff --git a/wwwroot/code/linq10.sc b/wwwroot/code/linq10.sc new file mode 100644 index 0000000..4990c86 --- /dev/null +++ b/wwwroot/code/linq10.sc @@ -0,0 +1,6 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => strings +numbers |> map => { Digit: strings[it], Even: it % 2 == 0 } |> to => digitOddEvens +#each digitOddEvens + `The digit ${Digit} is ${Even ? "even" : "odd"}.` +/each \ No newline at end of file diff --git a/wwwroot/code/linq10.ss b/wwwroot/code/linq10.ss new file mode 100644 index 0000000..187eef8 --- /dev/null +++ b/wwwroot/code/linq10.ss @@ -0,0 +1,6 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => strings}} +{{ numbers |> map => { Digit: strings[it], Even: it % 2 == 0 } |> to => digitOddEvens }} +{{#each digitOddEvens}} +The digit {{Digit}} is {{Even ? "even" : "odd"}}. +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq100.l b/wwwroot/code/linq100.l new file mode 100644 index 0000000..e9dab8c --- /dev/null +++ b/wwwroot/code/linq100.l @@ -0,0 +1,4 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) + (i 0) ) + (setq q (map #(it (f++ i)) numbers)) + (doseq (v q) (println "v = " v ", i = " i))) \ No newline at end of file diff --git a/wwwroot/code/linq100.sc b/wwwroot/code/linq100.sc new file mode 100644 index 0000000..8ed6800 --- /dev/null +++ b/wwwroot/code/linq100.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +0 |> to => i +numbers |> let => { i: i + 1 } |> toList |> map => `v = ${index + 1}, i = ${i}` |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq100.ss b/wwwroot/code/linq100.ss new file mode 100644 index 0000000..0819dda --- /dev/null +++ b/wwwroot/code/linq100.ss @@ -0,0 +1,3 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ 0 |> to => i }} +{{ numbers |> let => { i: i + 1 } |> toList |> select: v = {index + 1}, i = {i}\n }} \ No newline at end of file diff --git a/wwwroot/code/linq101.l b/wwwroot/code/linq101.l new file mode 100644 index 0000000..0d2f605 --- /dev/null +++ b/wwwroot/code/linq101.l @@ -0,0 +1,11 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + + (defn low-numbers [] (where #(<= % 3) numbers)) + + (println "First run numbers <= 3:") + (doseq (n (low-numbers)) (println n)) + + (setq numbers (map #(- %) numbers)) + + (println "Second run numbers <= 3") + (doseq (n (low-numbers)) (println n))) \ No newline at end of file diff --git a/wwwroot/code/linq101.sc b/wwwroot/code/linq101.sc new file mode 100644 index 0000000..2ed0e9a --- /dev/null +++ b/wwwroot/code/linq101.sc @@ -0,0 +1,12 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +numbers |> where => it <= 3 |> to => lowNumbers + +`First run numbers <= 3:` +lowNumbers |> joinln +10 |> times |> do => numbers[index] = -numbers[index] + +`Second run numbers <= 3:` +lowNumbers |> joinln + +`Contents of numbers:` +numbers |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq101.ss b/wwwroot/code/linq101.ss new file mode 100644 index 0000000..ba9dff2 --- /dev/null +++ b/wwwroot/code/linq101.ss @@ -0,0 +1,11 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ numbers + |> where => it <= 3 + |> to => lowNumbers }} +First run numbers <= 3: +{{ lowNumbers |> joinln }} +{{ 10 |> times |> do => numbers[index] = -numbers[index] }} +Second run numbers <= 3: +{{ lowNumbers |> joinln }} +Contents of numbers: +{{ numbers |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq11.l b/wwwroot/code/linq11.l new file mode 100644 index 0000000..be466cf --- /dev/null +++ b/wwwroot/code/linq11.l @@ -0,0 +1,8 @@ +(let ( (product-infos (map #(it { + :ProductName (.ProductName %) + :Category (.Category %) + :Price (.UnitPrice %) }) + products-list)) ) + (println "Product Info:") + (doseq (p product-infos) + (println (:ProductName p) " is in the category " (:Category p) " and costs " (:Price p)))) \ No newline at end of file diff --git a/wwwroot/code/linq11.sc b/wwwroot/code/linq11.sc new file mode 100644 index 0000000..7fd5bf5 --- /dev/null +++ b/wwwroot/code/linq11.sc @@ -0,0 +1,5 @@ +`Product Info:` +products |> map => { it.ProductName, it.Category, Price: it.UnitPrice } |> to => productInfos +#each productInfos +`${ProductName} is in the Category ${Category} and costs ${Price.currency()} per unit.` +/each \ No newline at end of file diff --git a/wwwroot/code/linq11.ss b/wwwroot/code/linq11.ss new file mode 100644 index 0000000..d6e3f1e --- /dev/null +++ b/wwwroot/code/linq11.ss @@ -0,0 +1,6 @@ +Product Info: +{{ products |> map => { it.ProductName, it.Category, Price: it.UnitPrice } + |> to => productInfos }} +{{#each productInfos}} +{{ProductName}} is in the Category {{Category}} and costs {{Price |> currency}} per unit. +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq12.l b/wwwroot/code/linq12.l new file mode 100644 index 0000000..665a187 --- /dev/null +++ b/wwwroot/code/linq12.l @@ -0,0 +1,5 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) + (i 0) ) + (println "Number: In-place?") + (doseq (n (map #(it { :num % :in-place (= % (f++ i)) }) numbers)) + (println (:num n) ": " (if (:in-place n) 'true 'false)) )) \ No newline at end of file diff --git a/wwwroot/code/linq12.sc b/wwwroot/code/linq12.sc new file mode 100644 index 0000000..2d23251 --- /dev/null +++ b/wwwroot/code/linq12.sc @@ -0,0 +1,5 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`Number: In-place?` +#each n in numbers + `${n}: ${ n == index }` +/each diff --git a/wwwroot/code/linq12.ss b/wwwroot/code/linq12.ss new file mode 100644 index 0000000..2790b32 --- /dev/null +++ b/wwwroot/code/linq12.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +Number: In-place? +{{#each n in numbers}} + {{n}}: {{ n == index }} +{{/each}} diff --git a/wwwroot/code/linq13.l b/wwwroot/code/linq13.l new file mode 100644 index 0000000..a21a9f1 --- /dev/null +++ b/wwwroot/code/linq13.l @@ -0,0 +1,4 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) + (digits ["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"]) ) + (println "Numbers < 5:") + (joinln (map #(nth digits %) (where #(< % 5) numbers)))) \ No newline at end of file diff --git a/wwwroot/code/linq13.sc b/wwwroot/code/linq13.sc new file mode 100644 index 0000000..fc8574b --- /dev/null +++ b/wwwroot/code/linq13.sc @@ -0,0 +1,6 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => digits +`Numbers < 5:` +#each numbers where it < 5 + digits[it] +/each diff --git a/wwwroot/code/linq13.ss b/wwwroot/code/linq13.ss new file mode 100644 index 0000000..95e9414 --- /dev/null +++ b/wwwroot/code/linq13.ss @@ -0,0 +1,6 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] |> to =>digits }} +Numbers < 5: +{{#each numbers where it < 5}} + {{ digits[it] }} +{{/each}} diff --git a/wwwroot/code/linq14.l b/wwwroot/code/linq14.l new file mode 100644 index 0000000..aa3b3ee --- /dev/null +++ b/wwwroot/code/linq14.l @@ -0,0 +1,5 @@ +(let ( (numbers-a [0 2 4 5 6 8 9]) + (numbers-b [1 3 5 7 8]) ) + (println "Pairs where a < b:") + (doseq (pair (zip-where #(< %1 %2) #(it { :a %1 :b %2 }) numbers-a numbers-b)) + (println (:a pair) " is less than " (:b pair)))) \ No newline at end of file diff --git a/wwwroot/code/linq14.sc b/wwwroot/code/linq14.sc new file mode 100644 index 0000000..a70357d --- /dev/null +++ b/wwwroot/code/linq14.sc @@ -0,0 +1,8 @@ +[0, 2, 4, 5, 6, 8, 9] |> to => numbersA +[1, 3, 5, 7, 8] |> to => numbersB +`Pairs where a < b:` +{{ numbersA.zip(numbersB) + |> let => { a: it[0], b: it[1] } + |> where => a < b + |> map => `${a} is less than ${b}` |> joinln +}} \ No newline at end of file diff --git a/wwwroot/code/linq14.ss b/wwwroot/code/linq14.ss new file mode 100644 index 0000000..b33cdd0 --- /dev/null +++ b/wwwroot/code/linq14.ss @@ -0,0 +1,8 @@ +{{ [0, 2, 4, 5, 6, 8, 9] |> to => numbersA }} +{{ [1, 3, 5, 7, 8] |> to => numbersB }} +Pairs where a < b: +{{ numbersA.zip(numbersB) + |> let => { a: it[0], b: it[1] } + |> where => a < b + |> map => `${a} is less than ${b}` |> joinln +}} \ No newline at end of file diff --git a/wwwroot/code/linq15.l b/wwwroot/code/linq15.l new file mode 100644 index 0000000..c3f91d4 --- /dev/null +++ b/wwwroot/code/linq15.l @@ -0,0 +1,8 @@ +(let ( (orders (flatmap (fn [c] + (map #(it { + :customer-id (.CustomerId c) + :order-id (.OrderId %) + :total (.Total %) }) + (where #(< (.Total %) 500) (.Orders c)) )) + customers-list)) ) + (htmldump orders)) \ No newline at end of file diff --git a/wwwroot/code/linq15.sc b/wwwroot/code/linq15.sc new file mode 100644 index 0000000..c7272f0 --- /dev/null +++ b/wwwroot/code/linq15.sc @@ -0,0 +1,5 @@ +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => o.Total < 500 + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq15.ss b/wwwroot/code/linq15.ss new file mode 100644 index 0000000..c7272f0 --- /dev/null +++ b/wwwroot/code/linq15.ss @@ -0,0 +1,5 @@ +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => o.Total < 500 + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq16.l b/wwwroot/code/linq16.l new file mode 100644 index 0000000..3406007 --- /dev/null +++ b/wwwroot/code/linq16.l @@ -0,0 +1,9 @@ +(let ( (orders (flatmap (fn [c] + (map-where #(> (.OrderDate %) (DateTime. 1998 1 1)) + #(it { + :customer-id (.CustomerId c) + :order-id (.OrderId %) + :order-date (.OrderDate %) }) + (.Orders c) )) + customers-list) )) + (htmldump orders)) \ No newline at end of file diff --git a/wwwroot/code/linq16.sc b/wwwroot/code/linq16.sc new file mode 100644 index 0000000..c8b1c74 --- /dev/null +++ b/wwwroot/code/linq16.sc @@ -0,0 +1,5 @@ +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => o.OrderDate >= '1998-01-01' + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq16.ss b/wwwroot/code/linq16.ss new file mode 100644 index 0000000..c8b1c74 --- /dev/null +++ b/wwwroot/code/linq16.ss @@ -0,0 +1,5 @@ +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => o.OrderDate >= '1998-01-01' + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq17.l b/wwwroot/code/linq17.l new file mode 100644 index 0000000..cc669fc --- /dev/null +++ b/wwwroot/code/linq17.l @@ -0,0 +1,8 @@ +(htmldump (flatmap (fn [c] + (map-where #(>= (.Total %) 2000) + #(it { + :customer-id (.CustomerId c) + :order-id (.OrderId %) + :total (.Total %) }) + (.Orders c) )) + customers-list)) diff --git a/wwwroot/code/linq17.sc b/wwwroot/code/linq17.sc new file mode 100644 index 0000000..e0543af --- /dev/null +++ b/wwwroot/code/linq17.sc @@ -0,0 +1,5 @@ +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => o.Total >= 2000 + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq17.ss b/wwwroot/code/linq17.ss new file mode 100644 index 0000000..e0543af --- /dev/null +++ b/wwwroot/code/linq17.ss @@ -0,0 +1,5 @@ +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => o.Total >= 2000 + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq18.l b/wwwroot/code/linq18.l new file mode 100644 index 0000000..45022df --- /dev/null +++ b/wwwroot/code/linq18.l @@ -0,0 +1,8 @@ +(let ( (cutoff-date (DateTime. 1997 1 1)) ) + (htmldump (flatmap (fn [c] + (map-where #(>= (.OrderDate %) cutoff-date) + #(it { + :customer-id (.CustomerId c) + :order-id (.OrderId %) }) + (.Orders c) )) + (where #(= (.Region %) "WA") customers-list)))) \ No newline at end of file diff --git a/wwwroot/code/linq18.sc b/wwwroot/code/linq18.sc new file mode 100644 index 0000000..ccbaaf6 --- /dev/null +++ b/wwwroot/code/linq18.sc @@ -0,0 +1,8 @@ +'1997-01-01' |> to => cutoffDate +{{ customers + |> where => it.Region == 'WA' + |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => o.OrderDate >= cutoffDate + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq18.ss b/wwwroot/code/linq18.ss new file mode 100644 index 0000000..f9a00ef --- /dev/null +++ b/wwwroot/code/linq18.ss @@ -0,0 +1,8 @@ +{{ '1997-01-01' |> to => cutoffDate }} +{{ customers + |> where => it.Region == 'WA' + |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => o.OrderDate >= cutoffDate + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq19.l b/wwwroot/code/linq19.l new file mode 100644 index 0000000..29e670f --- /dev/null +++ b/wwwroot/code/linq19.l @@ -0,0 +1,7 @@ +(let ( (customer-orders (map + #(it (str "Customer #" (:i %) " has an order with OrderID " (.OrderId (:o %)))) + (flatten (map-index (fn [c i] (map #(it { + :o % + :i (1+ i) }) + (.Orders c))) customers-list)))) ) + (joinln customer-orders)) \ No newline at end of file diff --git a/wwwroot/code/linq19.sc b/wwwroot/code/linq19.sc new file mode 100644 index 0000000..d5bfc6b --- /dev/null +++ b/wwwroot/code/linq19.sc @@ -0,0 +1,5 @@ +{{ customers + |> let => { cust: it, custIndex: index } + |> zip => cust.Orders + |> let => { o: it[1] } + |> map => `Customer #${custIndex + 1} has an order with OrderID ${o.OrderId}` |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq19.ss b/wwwroot/code/linq19.ss new file mode 100644 index 0000000..d5bfc6b --- /dev/null +++ b/wwwroot/code/linq19.ss @@ -0,0 +1,5 @@ +{{ customers + |> let => { cust: it, custIndex: index } + |> zip => cust.Orders + |> let => { o: it[1] } + |> map => `Customer #${custIndex + 1} has an order with OrderID ${o.OrderId}` |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq20.l b/wwwroot/code/linq20.l new file mode 100644 index 0000000..872941f --- /dev/null +++ b/wwwroot/code/linq20.l @@ -0,0 +1,3 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "First 3 numbers:") + (joinln (take 3 numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq20.sc b/wwwroot/code/linq20.sc new file mode 100644 index 0000000..82fd2fb --- /dev/null +++ b/wwwroot/code/linq20.sc @@ -0,0 +1,5 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`First 3 numbers:` +#each numbers take 3 + it +/each diff --git a/wwwroot/code/linq20.ss b/wwwroot/code/linq20.ss new file mode 100644 index 0000000..ab2c926 --- /dev/null +++ b/wwwroot/code/linq20.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +First 3 numbers: +{{#each numbers take 3}} + {{it}} +{{/each}} diff --git a/wwwroot/code/linq21.l b/wwwroot/code/linq21.l new file mode 100644 index 0000000..99335b0 --- /dev/null +++ b/wwwroot/code/linq21.l @@ -0,0 +1,9 @@ +(println " First 3 orders in WA:") +(htmldump (take 3 + (flatmap (fn [c] + (map #(it { + :customer-id (.CustomerId c) + :order-id (.OrderId %) + :order-date (.OrderDate %) }) + (.Orders c) )) + (where #(= (.Region %) "WA") customers-list) )) ) \ No newline at end of file diff --git a/wwwroot/code/linq21.sc b/wwwroot/code/linq21.sc new file mode 100644 index 0000000..c3f0f47 --- /dev/null +++ b/wwwroot/code/linq21.sc @@ -0,0 +1,7 @@ +` First 3 orders in WA:` +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => c.Region == 'WA' + |> take(3) + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq21.ss b/wwwroot/code/linq21.ss new file mode 100644 index 0000000..106503b --- /dev/null +++ b/wwwroot/code/linq21.ss @@ -0,0 +1,7 @@ + First 3 orders in WA: +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => c.Region == 'WA' + |> take(3) + |> map => o + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq22.l b/wwwroot/code/linq22.l new file mode 100644 index 0000000..ea9b414 --- /dev/null +++ b/wwwroot/code/linq22.l @@ -0,0 +1,3 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "All but first 4 numbers:") + (joinln (skip 4 numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq22.sc b/wwwroot/code/linq22.sc new file mode 100644 index 0000000..6073d27 --- /dev/null +++ b/wwwroot/code/linq22.sc @@ -0,0 +1,5 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`All but first 4 numbers:` +#each numbers skip 4 + it +/each diff --git a/wwwroot/code/linq22.ss b/wwwroot/code/linq22.ss new file mode 100644 index 0000000..76d215e --- /dev/null +++ b/wwwroot/code/linq22.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +All but first 4 numbers: +{{#each numbers skip 4}} +{{it}} +{{/each}} diff --git a/wwwroot/code/linq23.l b/wwwroot/code/linq23.l new file mode 100644 index 0000000..ef38f48 --- /dev/null +++ b/wwwroot/code/linq23.l @@ -0,0 +1,9 @@ +(println "All but first 2 orders in WA:") +(htmldump (skip 2 + (flatmap (fn [c] + (map #(it { + :customer-id (.CustomerId c) + :order-id (.OrderId %) + :order-date (.OrderDate %) }) + (.Orders c) )) + (where #(= (.Region %) "WA") customers-list) )) ) \ No newline at end of file diff --git a/wwwroot/code/linq23.sc b/wwwroot/code/linq23.sc new file mode 100644 index 0000000..55e7883 --- /dev/null +++ b/wwwroot/code/linq23.sc @@ -0,0 +1,7 @@ +` All but first 2 orders in WA:` +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => c.Region == 'WA' + |> skip(2) + |> map => { c.CustomerId, o.OrderId, o.OrderDate } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq23.ss b/wwwroot/code/linq23.ss new file mode 100644 index 0000000..0468ba7 --- /dev/null +++ b/wwwroot/code/linq23.ss @@ -0,0 +1,7 @@ + All but first 2 orders in WA: +{{ customers |> zip => it.Orders + |> let => { c: it[0], o: it[1] } + |> where => c.Region == 'WA' + |> skip(2) + |> map => { c.CustomerId, o.OrderId, o.OrderDate } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq24.l b/wwwroot/code/linq24.l new file mode 100644 index 0000000..9851706 --- /dev/null +++ b/wwwroot/code/linq24.l @@ -0,0 +1,3 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "First numbers less than 6:") + (joinln (take-while #(< % 6) numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq24.sc b/wwwroot/code/linq24.sc new file mode 100644 index 0000000..0684405 --- /dev/null +++ b/wwwroot/code/linq24.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`First numbers less than 6:` +numbers |> takeWhile => it < 6 |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq24.ss b/wwwroot/code/linq24.ss new file mode 100644 index 0000000..9714909 --- /dev/null +++ b/wwwroot/code/linq24.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +First numbers less than 6: +{{ numbers + |> takeWhile => it < 6 + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq25.l b/wwwroot/code/linq25.l new file mode 100644 index 0000000..cb55341 --- /dev/null +++ b/wwwroot/code/linq25.l @@ -0,0 +1,4 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0] ) + (i 0) ) + (println "First numbers not less than their position:") + (joinln (take-while #(>= % (f++ i)) numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq25.sc b/wwwroot/code/linq25.sc new file mode 100644 index 0000000..93357ed --- /dev/null +++ b/wwwroot/code/linq25.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`First numbers not less than their position:` +numbers |> takeWhile => it >= index |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq25.ss b/wwwroot/code/linq25.ss new file mode 100644 index 0000000..ac90d5e --- /dev/null +++ b/wwwroot/code/linq25.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +First numbers not less than their position: +{{ numbers + |> takeWhile => it >= index + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq26.l b/wwwroot/code/linq26.l new file mode 100644 index 0000000..d4851a5 --- /dev/null +++ b/wwwroot/code/linq26.l @@ -0,0 +1,3 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "All elements starting from first element divisible by 3:") + (joinln (skip-while #(not= (mod % 3) 0) numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq26.sc b/wwwroot/code/linq26.sc new file mode 100644 index 0000000..74771af --- /dev/null +++ b/wwwroot/code/linq26.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`All elements starting from first element divisible by 3:` +numbers |> skipWhile => it % 3 != 0 |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq26.ss b/wwwroot/code/linq26.ss new file mode 100644 index 0000000..ed93b85 --- /dev/null +++ b/wwwroot/code/linq26.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +All elements starting from first element divisible by 3: +{{ numbers + |> skipWhile => it % 3 != 0 + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq27.l b/wwwroot/code/linq27.l new file mode 100644 index 0000000..d0710d5 --- /dev/null +++ b/wwwroot/code/linq27.l @@ -0,0 +1,4 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) + (i 0) ) + (println "All elements starting from first element less than its position:") + (joinln (skip-while #(>= % (f++ i)) numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq27.sc b/wwwroot/code/linq27.sc new file mode 100644 index 0000000..5d558be --- /dev/null +++ b/wwwroot/code/linq27.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +`All elements starting from first element less than its position:` +numbers |> skipWhile => it >= index |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq27.ss b/wwwroot/code/linq27.ss new file mode 100644 index 0000000..45ecf10 --- /dev/null +++ b/wwwroot/code/linq27.ss @@ -0,0 +1,5 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +All elements starting from first element less than its position: +{{ numbers + |> skipWhile => it >= index + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq28.l b/wwwroot/code/linq28.l new file mode 100644 index 0000000..e5f76eb --- /dev/null +++ b/wwwroot/code/linq28.l @@ -0,0 +1,3 @@ +(let ( (words ["cherry" "apple" "blueberry"]) ) + (println "The sorted list of words:") + (joinln (sort words))) \ No newline at end of file diff --git a/wwwroot/code/linq28.sc b/wwwroot/code/linq28.sc new file mode 100644 index 0000000..065d764 --- /dev/null +++ b/wwwroot/code/linq28.sc @@ -0,0 +1,5 @@ +['cherry', 'apple', 'blueberry'] |> to => words +`The sorted list of words:` +#each words orderby it + it +/each diff --git a/wwwroot/code/linq28.ss b/wwwroot/code/linq28.ss new file mode 100644 index 0000000..799c19d --- /dev/null +++ b/wwwroot/code/linq28.ss @@ -0,0 +1,5 @@ +{{ ['cherry', 'apple', 'blueberry'] |> to => words }} +The sorted list of words: +{{#each words orderby it}} +{{it}} +{{/each}} diff --git a/wwwroot/code/linq29.l b/wwwroot/code/linq29.l new file mode 100644 index 0000000..7d4b1ea --- /dev/null +++ b/wwwroot/code/linq29.l @@ -0,0 +1,3 @@ +(let ( (words ["cherry" "apple" "blueberry"]) ) + (println "The sorted list of words (by length):") + (joinln (sort-by count words))) \ No newline at end of file diff --git a/wwwroot/code/linq29.sc b/wwwroot/code/linq29.sc new file mode 100644 index 0000000..2557cd1 --- /dev/null +++ b/wwwroot/code/linq29.sc @@ -0,0 +1,5 @@ +['cherry', 'apple', 'blueberry'] |> to => words +`The sorted list of words (by length):` +#each words orderby it.Length + it +/each diff --git a/wwwroot/code/linq29.ss b/wwwroot/code/linq29.ss new file mode 100644 index 0000000..09e312f --- /dev/null +++ b/wwwroot/code/linq29.ss @@ -0,0 +1,5 @@ +{{ ['cherry', 'apple', 'blueberry'] |> to => words }} +The sorted list of words (by length): +{{#each words orderby it.Length}} +{{it}} +{{/each}} diff --git a/wwwroot/code/linq30.l b/wwwroot/code/linq30.l new file mode 100644 index 0000000..08884b5 --- /dev/null +++ b/wwwroot/code/linq30.l @@ -0,0 +1 @@ +(htmldump (sort-by .ProductName products-list)) \ No newline at end of file diff --git a/wwwroot/code/linq30.sc b/wwwroot/code/linq30.sc new file mode 100644 index 0000000..3a5a6e7 --- /dev/null +++ b/wwwroot/code/linq30.sc @@ -0,0 +1 @@ +products |> orderBy => it.ProductName |> htmlDump \ No newline at end of file diff --git a/wwwroot/code/linq30.ss b/wwwroot/code/linq30.ss new file mode 100644 index 0000000..2194e0c --- /dev/null +++ b/wwwroot/code/linq30.ss @@ -0,0 +1,3 @@ +{{ products + |> orderBy => it.ProductName + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq31.l b/wwwroot/code/linq31.l new file mode 100644 index 0000000..75d60e2 --- /dev/null +++ b/wwwroot/code/linq31.l @@ -0,0 +1,2 @@ +(let ( (words ["aPPLE" "AbAcUs" "bRaNcH" "BlUeBeRrY" "ClOvEr" "cHeRry"]) ) + (joinln (sort-by it (CaseInsensitiveComparer.) words))) \ No newline at end of file diff --git a/wwwroot/code/linq31.sc b/wwwroot/code/linq31.sc new file mode 100644 index 0000000..d3dd15b --- /dev/null +++ b/wwwroot/code/linq31.sc @@ -0,0 +1,2 @@ +['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] |> to => words + words |> orderBy(o => o, { comparer }) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq31.ss b/wwwroot/code/linq31.ss new file mode 100644 index 0000000..78e278f --- /dev/null +++ b/wwwroot/code/linq31.ss @@ -0,0 +1,4 @@ +{{ ['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] |> to => words }} +{{ words + |> orderBy(o => o, { comparer }) + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq32.l b/wwwroot/code/linq32.l new file mode 100644 index 0000000..0d41987 --- /dev/null +++ b/wwwroot/code/linq32.l @@ -0,0 +1,3 @@ +(let ( (dbls [1.7 2.3 1.9 4.1 2.9]) ) + (println "The doubles from highest to lowest:") + (joinln (reverse (sort dbls))) ) \ No newline at end of file diff --git a/wwwroot/code/linq32.sc b/wwwroot/code/linq32.sc new file mode 100644 index 0000000..42dd375 --- /dev/null +++ b/wwwroot/code/linq32.sc @@ -0,0 +1,5 @@ +[1.7, 2.3, 1.9, 4.1, 2.9] |> to => doubles +`The doubles from highest to lowest:` +#each doubles orderby it descending + it +/each diff --git a/wwwroot/code/linq32.ss b/wwwroot/code/linq32.ss new file mode 100644 index 0000000..eebb6cc --- /dev/null +++ b/wwwroot/code/linq32.ss @@ -0,0 +1,5 @@ +{{ [1.7, 2.3, 1.9, 4.1, 2.9] |> to => doubles }} +The doubles from highest to lowest: +{{#each doubles orderby it descending}} +{{it}} +{{/each}} diff --git a/wwwroot/code/linq33.l b/wwwroot/code/linq33.l new file mode 100644 index 0000000..527fc91 --- /dev/null +++ b/wwwroot/code/linq33.l @@ -0,0 +1 @@ +(htmldump (reverse (sort-by .UnitsInStock products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq33.sc b/wwwroot/code/linq33.sc new file mode 100644 index 0000000..c6100ab --- /dev/null +++ b/wwwroot/code/linq33.sc @@ -0,0 +1 @@ +products |> orderByDescending => it.UnitsInStock |> htmlDump \ No newline at end of file diff --git a/wwwroot/code/linq33.ss b/wwwroot/code/linq33.ss new file mode 100644 index 0000000..23a3c71 --- /dev/null +++ b/wwwroot/code/linq33.ss @@ -0,0 +1,3 @@ +{{ products + |> orderByDescending => it.UnitsInStock + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq34.l b/wwwroot/code/linq34.l new file mode 100644 index 0000000..a1ee4b4 --- /dev/null +++ b/wwwroot/code/linq34.l @@ -0,0 +1,2 @@ +(let ( (words ["aPPLE" "AbAcUs" "bRaNcH" "BlUeBeRrY" "ClOvEr" "cHeRry"]) ) + (joinln (order-by [{ :comparer (CaseInsensitiveComparer.) :desc true }] words))) \ No newline at end of file diff --git a/wwwroot/code/linq34.sc b/wwwroot/code/linq34.sc new file mode 100644 index 0000000..018f0bd --- /dev/null +++ b/wwwroot/code/linq34.sc @@ -0,0 +1,2 @@ +['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] |> to => words +words |> orderByDescending(o => o, { comparer }) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq34.ss b/wwwroot/code/linq34.ss new file mode 100644 index 0000000..f3c2dc7 --- /dev/null +++ b/wwwroot/code/linq34.ss @@ -0,0 +1,4 @@ +{{ ['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] |> to => words }} +{{ words + |> orderByDescending(o => o, { comparer }) + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq35.l b/wwwroot/code/linq35.l new file mode 100644 index 0000000..aa10f39 --- /dev/null +++ b/wwwroot/code/linq35.l @@ -0,0 +1,4 @@ +(let ( (digits ["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"]) + (i 0) ) + (println "Sorted digits:") + (joinln (order-by [#(count %) it] digits ))) \ No newline at end of file diff --git a/wwwroot/code/linq35.sc b/wwwroot/code/linq35.sc new file mode 100644 index 0000000..d2a52cd --- /dev/null +++ b/wwwroot/code/linq35.sc @@ -0,0 +1,3 @@ +['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => digits +`Sorted digits:` +digits |> orderBy => it.length |> thenBy => it |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq35.ss b/wwwroot/code/linq35.ss new file mode 100644 index 0000000..e36dc89 --- /dev/null +++ b/wwwroot/code/linq35.ss @@ -0,0 +1,6 @@ +{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] |> to =>digits }} +Sorted digits: +{{ digits + |> orderBy => it.length + |> thenBy => it + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq36.l b/wwwroot/code/linq36.l new file mode 100644 index 0000000..c125a48 --- /dev/null +++ b/wwwroot/code/linq36.l @@ -0,0 +1,2 @@ +(let ( (words ["aPPLE" "AbAcUs" "bRaNcH" "BlUeBeRrY" "ClOvEr" "cHeRry"]) ) + (joinln (order-by [#(count %) { :comparer (CaseInsensitiveComparer.) }] words))) \ No newline at end of file diff --git a/wwwroot/code/linq36.sc b/wwwroot/code/linq36.sc new file mode 100644 index 0000000..80b4e13 --- /dev/null +++ b/wwwroot/code/linq36.sc @@ -0,0 +1,2 @@ +['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] |> to => words +words |> orderBy => it.length |> thenBy(w => w, { comparer }) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq36.ss b/wwwroot/code/linq36.ss new file mode 100644 index 0000000..27d6360 --- /dev/null +++ b/wwwroot/code/linq36.ss @@ -0,0 +1,5 @@ +{{ ['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] |> to => words }} +{{ words + |> orderBy => it.length + |> thenBy(w => w, { comparer }) + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq37.l b/wwwroot/code/linq37.l new file mode 100644 index 0000000..5d3f43e --- /dev/null +++ b/wwwroot/code/linq37.l @@ -0,0 +1 @@ +(htmldump (order-by [ #(.Category %) { :key #(.UnitPrice %) :desc true } ] products-list)) \ No newline at end of file diff --git a/wwwroot/code/linq37.sc b/wwwroot/code/linq37.sc new file mode 100644 index 0000000..c36bc0f --- /dev/null +++ b/wwwroot/code/linq37.sc @@ -0,0 +1 @@ +products |> orderBy => it.Category |> thenByDescending => it.UnitPrice |> htmlDump \ No newline at end of file diff --git a/wwwroot/code/linq37.ss b/wwwroot/code/linq37.ss new file mode 100644 index 0000000..d09dc4e --- /dev/null +++ b/wwwroot/code/linq37.ss @@ -0,0 +1,4 @@ +{{ products + |> orderBy => it.Category + |> thenByDescending => it.UnitPrice + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq38.l b/wwwroot/code/linq38.l new file mode 100644 index 0000000..b39161e --- /dev/null +++ b/wwwroot/code/linq38.l @@ -0,0 +1,2 @@ +(let ( (words ["aPPLE" "AbAcUs" "bRaNcH" "BlUeBeRrY" "ClOvEr" "cHeRry"]) ) + (joinln (order-by [ #(count %) { :comparer (CaseInsensitiveComparer.) :desc true } ] words))) \ No newline at end of file diff --git a/wwwroot/code/linq38.sc b/wwwroot/code/linq38.sc new file mode 100644 index 0000000..826e657 --- /dev/null +++ b/wwwroot/code/linq38.sc @@ -0,0 +1,2 @@ +['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] |> to => words +words |> orderBy => it.length |> thenByDescending(w => w, { comparer }) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq38.ss b/wwwroot/code/linq38.ss new file mode 100644 index 0000000..1878f46 --- /dev/null +++ b/wwwroot/code/linq38.ss @@ -0,0 +1,5 @@ +{{ ['aPPLE', 'AbAcUs', 'bRaNcH', 'BlUeBeRrY', 'ClOvEr', 'cHeRry'] |> to => words }} +{{ words + |> orderBy => it.length + |> thenByDescending(w => w, { comparer }) + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq39.l b/wwwroot/code/linq39.l new file mode 100644 index 0000000..11d055b --- /dev/null +++ b/wwwroot/code/linq39.l @@ -0,0 +1,3 @@ +(let ( (digits ["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"]) ) + (println "A backwards list of the digits with a second character of 'i':") + (joinln (reverse (where #(= (:1 %) (:0 "i")) digits)))) \ No newline at end of file diff --git a/wwwroot/code/linq39.sc b/wwwroot/code/linq39.sc new file mode 100644 index 0000000..99fca42 --- /dev/null +++ b/wwwroot/code/linq39.sc @@ -0,0 +1,3 @@ +['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => digits +`A backwards list of the digits with a second character of 'i':` +digits |> where => it[1] == 'i' |> reverse |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq39.ss b/wwwroot/code/linq39.ss new file mode 100644 index 0000000..76d5873 --- /dev/null +++ b/wwwroot/code/linq39.ss @@ -0,0 +1,6 @@ +{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] |> to =>digits }} +A backwards list of the digits with a second character of 'i': +{{ digits + |> where => it[1] == 'i' + |> reverse + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq40.l b/wwwroot/code/linq40.l new file mode 100644 index 0000000..04de9db --- /dev/null +++ b/wwwroot/code/linq40.l @@ -0,0 +1,7 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) + (number-groups) ) + (setq number-groups + (map #(it { :remainder (.Key %) :numbers % }) (group-by #(mod % 5) numbers))) + (doseq (g number-groups) + (println "Numbers with a remainder of " (:remainder g) " when divided by 5:") + (println (joinln (:numbers g))) )) \ No newline at end of file diff --git a/wwwroot/code/linq40.sc b/wwwroot/code/linq40.sc new file mode 100644 index 0000000..1e293a2 --- /dev/null +++ b/wwwroot/code/linq40.sc @@ -0,0 +1,7 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => nums +{{ nums + |> groupBy => it % 5 + |> let => { remainder: it.Key, nums: it } + |> map => `Numbers with a remainder of ${remainder} when divided by 5:\n${nums.join('\n')}` + |> joinln +}} \ No newline at end of file diff --git a/wwwroot/code/linq40.ss b/wwwroot/code/linq40.ss new file mode 100644 index 0000000..493c607 --- /dev/null +++ b/wwwroot/code/linq40.ss @@ -0,0 +1,7 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => nums }} +{{ nums + |> groupBy => it % 5 + |> let => { remainder: it.Key, nums: it } + |> map => `Numbers with a remainder of ${remainder} when divided by 5:\n${nums.join('\n')}` + |> joinln +}} \ No newline at end of file diff --git a/wwwroot/code/linq41.l b/wwwroot/code/linq41.l new file mode 100644 index 0000000..1d25754 --- /dev/null +++ b/wwwroot/code/linq41.l @@ -0,0 +1,7 @@ +(let ( (words ["blueberry" "chimpanzee" "abacus" "banana" "apple" "cheese"]) + (word-groups) ) + (setq word-groups + (map #(it {:first-letter (.Key %) :words % }) (group-by #(:0 %) words) )) + (doseq (g word-groups) + (println "Words that start with the letter: " (:first-letter g)) + (println (joinln (:words g))) )) \ No newline at end of file diff --git a/wwwroot/code/linq41.sc b/wwwroot/code/linq41.sc new file mode 100644 index 0000000..c02387c --- /dev/null +++ b/wwwroot/code/linq41.sc @@ -0,0 +1,6 @@ +['blueberry', 'chimpanzee', 'abacus', 'banana', 'apple', 'cheese'] |> to => words +words |> groupBy => it[0] |> map => { firstLetter: it.Key, words: it } |> to => groups +#each groups + `Words that start with the letter '${firstLetter}':` + words |> joinln +/each diff --git a/wwwroot/code/linq41.ss b/wwwroot/code/linq41.ss new file mode 100644 index 0000000..4b535c5 --- /dev/null +++ b/wwwroot/code/linq41.ss @@ -0,0 +1,6 @@ +{{ ['blueberry', 'chimpanzee', 'abacus', 'banana', 'apple', 'cheese'] |> to => words }} +{{ words |> groupBy => it[0] |> map => { firstLetter: it.Key, words: it } |> to => groups }} +{{#each groups}} +Words that start with the letter '{{firstLetter}}': +{{ words |> joinln }} +{{/each}} diff --git a/wwwroot/code/linq42.l b/wwwroot/code/linq42.l new file mode 100644 index 0000000..f744461 --- /dev/null +++ b/wwwroot/code/linq42.l @@ -0,0 +1 @@ +(htmldump (map #(it {:category (.Key %), :products % }) (group-by :category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq42.sc b/wwwroot/code/linq42.sc new file mode 100644 index 0000000..5fde557 --- /dev/null +++ b/wwwroot/code/linq42.sc @@ -0,0 +1 @@ +products |> groupBy => it.Category |> let => { Category: it.Key, Products: it } |> htmlDump \ No newline at end of file diff --git a/wwwroot/code/linq42.ss b/wwwroot/code/linq42.ss new file mode 100644 index 0000000..ddffe13 --- /dev/null +++ b/wwwroot/code/linq42.ss @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> let => { Category: it.Key, Products: it } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq43.l b/wwwroot/code/linq43.l new file mode 100644 index 0000000..390c67c --- /dev/null +++ b/wwwroot/code/linq43.l @@ -0,0 +1,12 @@ +(let ( (customer-order-groups + (map (fn [c] { + :company-name (.CompanyName c) + :year-groups (map (fn [yg] { + :year (.Key yg) + :month-groups (map #(it { + :month (.Key %) + :orders % }) + (group-by #(.Month (.OrderDate %)) yg)) }) + (group-by #(it (.Year (.OrderDate %))) (.Orders c))) }) + customers-list)) ) + (htmldump customer-order-groups)) \ No newline at end of file diff --git a/wwwroot/code/linq43.sc b/wwwroot/code/linq43.sc new file mode 100644 index 0000000..06bef6e --- /dev/null +++ b/wwwroot/code/linq43.sc @@ -0,0 +1,12 @@ +{{ customers |> map => { + CompanyName: it.CompanyName, + YearGroups: it.Orders.groupBy(it => it.OrderDate.Year).map(yg => + { + Year: yg.Key, + MonthGroups: yg.groupBy(o => o.OrderDate.Month).map(mg => + { Month: mg.Key, Orders: mg } + ) + } + ) + } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq43.ss b/wwwroot/code/linq43.ss new file mode 100644 index 0000000..06bef6e --- /dev/null +++ b/wwwroot/code/linq43.ss @@ -0,0 +1,12 @@ +{{ customers |> map => { + CompanyName: it.CompanyName, + YearGroups: it.Orders.groupBy(it => it.OrderDate.Year).map(yg => + { + Year: yg.Key, + MonthGroups: yg.groupBy(o => o.OrderDate.Month).map(mg => + { Month: mg.Key, Orders: mg } + ) + } + ) + } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq44.l b/wwwroot/code/linq44.l new file mode 100644 index 0000000..53119d1 --- /dev/null +++ b/wwwroot/code/linq44.l @@ -0,0 +1,3 @@ +(let ( (anagrams ["from " " salt" " earn " " last " " near " " form "]) ) + (doseq (x (group-by .Trim { :comparer (AnagramEqualityComparer.) } anagrams)) + (println (/json x)) )) \ No newline at end of file diff --git a/wwwroot/code/linq44.sc b/wwwroot/code/linq44.sc new file mode 100644 index 0000000..cbdb9d4 --- /dev/null +++ b/wwwroot/code/linq44.sc @@ -0,0 +1,4 @@ +['from ', ' salt', ' earn ', ' last ', ' near ', ' form '] |> to => anagrams +#each anagrams.groupBy(w => w.trim(), { comparer: anagramComparer }) + it |> json +/each diff --git a/wwwroot/code/linq44.ss b/wwwroot/code/linq44.ss new file mode 100644 index 0000000..e3672f9 --- /dev/null +++ b/wwwroot/code/linq44.ss @@ -0,0 +1,4 @@ +{{ ['from ', ' salt', ' earn ', ' last ', ' near ', ' form '] |> to => anagrams }} +{{#each anagrams.groupBy(w => w.trim(), { comparer: anagramComparer }) }} +{{it |> json}} +{{/each}} diff --git a/wwwroot/code/linq45.l b/wwwroot/code/linq45.l new file mode 100644 index 0000000..db14ac7 --- /dev/null +++ b/wwwroot/code/linq45.l @@ -0,0 +1,3 @@ +(let ( (anagrams ["from " " salt" " earn " " last " " near " " form "]) ) + (doseq (x (group-by .Trim { :comparer (AnagramEqualityComparer.) :map /upper } anagrams)) + (println (/json x)) )) \ No newline at end of file diff --git a/wwwroot/code/linq45.sc b/wwwroot/code/linq45.sc new file mode 100644 index 0000000..ac2991d --- /dev/null +++ b/wwwroot/code/linq45.sc @@ -0,0 +1,4 @@ +['from ', ' salt', ' earn ', ' last ', ' near ', ' form '] |> to => anagrams +#each anagrams.groupBy(w => w.trim(), { map: a => a.upper(), comparer: anagramComparer }) + it |> json +/each \ No newline at end of file diff --git a/wwwroot/code/linq45.ss b/wwwroot/code/linq45.ss new file mode 100644 index 0000000..934cef5 --- /dev/null +++ b/wwwroot/code/linq45.ss @@ -0,0 +1,4 @@ +{{ ['from ', ' salt', ' earn ', ' last ', ' near ', ' form '] |> to => anagrams }} +{{#each anagrams.groupBy(w => w.trim(), { map: a => a.upper(), comparer: anagramComparer }) }} +{{it |> json}} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq46.l b/wwwroot/code/linq46.l new file mode 100644 index 0000000..89958d0 --- /dev/null +++ b/wwwroot/code/linq46.l @@ -0,0 +1,3 @@ +(let ( (factors-of-300 [2, 2, 3, 5, 5]) ) + (println "Prime factors of 300:") + (joinln (/distinct factors-of-300))) \ No newline at end of file diff --git a/wwwroot/code/linq46.sc b/wwwroot/code/linq46.sc new file mode 100644 index 0000000..a550c1f --- /dev/null +++ b/wwwroot/code/linq46.sc @@ -0,0 +1,3 @@ +[2, 2, 3, 5, 5] |> to => factorsOf300 +`Prime factors of 300:` +factorsOf300.distinct() |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq46.ss b/wwwroot/code/linq46.ss new file mode 100644 index 0000000..ce2c16a --- /dev/null +++ b/wwwroot/code/linq46.ss @@ -0,0 +1,3 @@ +{{ [2, 2, 3, 5, 5] |> to => factorsOf300 }} +Prime factors of 300: +{{ factorsOf300.distinct() |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq47.l b/wwwroot/code/linq47.l new file mode 100644 index 0000000..cf3948a --- /dev/null +++ b/wwwroot/code/linq47.l @@ -0,0 +1,2 @@ +(println "Category names:") +(joinln (/distinct (map .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq47.sc b/wwwroot/code/linq47.sc new file mode 100644 index 0000000..558b6d0 --- /dev/null +++ b/wwwroot/code/linq47.sc @@ -0,0 +1,2 @@ +`Category names:` +products |> map => it.Category |> distinct |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq47.ss b/wwwroot/code/linq47.ss new file mode 100644 index 0000000..a77cc4f --- /dev/null +++ b/wwwroot/code/linq47.ss @@ -0,0 +1,5 @@ +Category names: +{{ products + |> map => it.Category + |> distinct + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq48.l b/wwwroot/code/linq48.l new file mode 100644 index 0000000..5005f95 --- /dev/null +++ b/wwwroot/code/linq48.l @@ -0,0 +1,4 @@ +(let ( (numbers-a [0 2 4 5 6 8 9]) + (numbers-b [1 3 5 7 8]) ) + (println "Unique numbers from both arrays:") + (joinln (/union numbers-a numbers-b))) \ No newline at end of file diff --git a/wwwroot/code/linq48.sc b/wwwroot/code/linq48.sc new file mode 100644 index 0000000..2e62400 --- /dev/null +++ b/wwwroot/code/linq48.sc @@ -0,0 +1,6 @@ +[ 0, 2, 4, 5, 6, 8, 9 ] |> to => numbersA +[ 1, 3, 5, 7, 8 ] |> to => numbersB +`Unique numbers from both arrays:` +#each numbersA.union(numbersB) + it +/each \ No newline at end of file diff --git a/wwwroot/code/linq48.ss b/wwwroot/code/linq48.ss new file mode 100644 index 0000000..4cee036 --- /dev/null +++ b/wwwroot/code/linq48.ss @@ -0,0 +1,6 @@ +{{ [ 0, 2, 4, 5, 6, 8, 9 ] |> to => numbersA }} +{{ [ 1, 3, 5, 7, 8 ] |> to => numbersB }} +Unique numbers from both arrays: +{{#each numbersA.union(numbersB)}} + {{it}} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq49.l b/wwwroot/code/linq49.l new file mode 100644 index 0000000..740f77d --- /dev/null +++ b/wwwroot/code/linq49.l @@ -0,0 +1,5 @@ +(let ( (product-first-chars (map #(:0 (.ProductName %)) products-list)) + (customer-first-chars (map #(:0 (.CompanyName %)) customers-list)) ) + + (println "Unique first letters from Product names and Customer names:") + (joinln (/union product-first-chars customer-first-chars))) \ No newline at end of file diff --git a/wwwroot/code/linq49.sc b/wwwroot/code/linq49.sc new file mode 100644 index 0000000..ed00797 --- /dev/null +++ b/wwwroot/code/linq49.sc @@ -0,0 +1,6 @@ +products |> map => it.ProductName[0] |> to => productFirstChars +customers |> map => it.CompanyName[0] |> to => customerFirstChars +`Unique first letters from Product names and Customer names:` +#each productFirstChars.union(customerFirstChars) + it +/each \ No newline at end of file diff --git a/wwwroot/code/linq49.ss b/wwwroot/code/linq49.ss new file mode 100644 index 0000000..1ea6653 --- /dev/null +++ b/wwwroot/code/linq49.ss @@ -0,0 +1,10 @@ +{{ products + |> map => it.ProductName[0] + |> to => productFirstChars }} +{{ customers + |> map => it.CompanyName[0] + |> to => customerFirstChars }} +Unique first letters from Product names and Customer names: +{{#each productFirstChars.union(customerFirstChars) }} + {{it}} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq50.l b/wwwroot/code/linq50.l new file mode 100644 index 0000000..b08300b --- /dev/null +++ b/wwwroot/code/linq50.l @@ -0,0 +1,4 @@ +(let ( (numbers-a [0 2 4 5 6 8 9]) + (numbers-b [1 3 5 7 8]) ) + (println "Common numbers shared by both arrays:") + (joinln (/intersect numbers-a numbers-b))) \ No newline at end of file diff --git a/wwwroot/code/linq50.sc b/wwwroot/code/linq50.sc new file mode 100644 index 0000000..dea7b32 --- /dev/null +++ b/wwwroot/code/linq50.sc @@ -0,0 +1,4 @@ +[ 0, 2, 4, 5, 6, 8, 9 ] |> to => numbersA +[ 1, 3, 5, 7, 8 ] |> to => numbersB +`Common numbers shared by both arrays:` +numbersA.intersect(numbersB) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq50.ss b/wwwroot/code/linq50.ss new file mode 100644 index 0000000..0fee992 --- /dev/null +++ b/wwwroot/code/linq50.ss @@ -0,0 +1,4 @@ +{{ [ 0, 2, 4, 5, 6, 8, 9 ] |> to => numbersA }} +{{ [ 1, 3, 5, 7, 8 ] |> to => numbersB }} +Common numbers shared by both arrays: +{{ numbersA.intersect(numbersB) |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq51.l b/wwwroot/code/linq51.l new file mode 100644 index 0000000..d8f3757 --- /dev/null +++ b/wwwroot/code/linq51.l @@ -0,0 +1,4 @@ +(let ( (product-first-chars (map #(:0 (.ProductName %)) products-list)) + (customer-first-chars (map #(:0 (.CompanyName %)) customers-list)) ) + (println "Common first letters from Product names and Customer names:") + (joinln (/intersect product-first-chars customer-first-chars))) \ No newline at end of file diff --git a/wwwroot/code/linq51.sc b/wwwroot/code/linq51.sc new file mode 100644 index 0000000..f522831 --- /dev/null +++ b/wwwroot/code/linq51.sc @@ -0,0 +1,6 @@ +products |> map => it.ProductName[0] |> to => productFirstChars +customers |> map => it.CompanyName[0] |> to => customerFirstChars +`Common first letters from Product names and Customer names:` +#each productFirstChars.intersect(customerFirstChars) + it +/each \ No newline at end of file diff --git a/wwwroot/code/linq51.ss b/wwwroot/code/linq51.ss new file mode 100644 index 0000000..36a5359 --- /dev/null +++ b/wwwroot/code/linq51.ss @@ -0,0 +1,10 @@ +{{ products + |> map => it.ProductName[0] + |> to => productFirstChars }} +{{ customers + |> map => it.CompanyName[0] + |> to => customerFirstChars }} +Common first letters from Product names and Customer names: +{{#each productFirstChars.intersect(customerFirstChars) }} + {{it}} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq52.l b/wwwroot/code/linq52.l new file mode 100644 index 0000000..b6d57ea --- /dev/null +++ b/wwwroot/code/linq52.l @@ -0,0 +1,4 @@ +(let ( (numbers-a [0 2 4 5 6 8 9]) + (numbers-b [1 3 5 7 8]) ) + (println "Numbers in first array but not second array:") + (joinln (/except numbers-a numbers-b))) \ No newline at end of file diff --git a/wwwroot/code/linq52.sc b/wwwroot/code/linq52.sc new file mode 100644 index 0000000..f174ee6 --- /dev/null +++ b/wwwroot/code/linq52.sc @@ -0,0 +1,4 @@ +[ 0, 2, 4, 5, 6, 8, 9 ] |> to => numbersA +[ 1, 3, 5, 7, 8 ] |> to => numbersB +`Numbers in first array but not second array:` +numbersA.except(numbersB) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq52.ss b/wwwroot/code/linq52.ss new file mode 100644 index 0000000..7f200b7 --- /dev/null +++ b/wwwroot/code/linq52.ss @@ -0,0 +1,4 @@ +{{ [ 0, 2, 4, 5, 6, 8, 9 ] |> to => numbersA }} +{{ [ 1, 3, 5, 7, 8 ] |> to => numbersB }} +Numbers in first array but not second array: +{{ numbersA.except(numbersB) |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq53.l b/wwwroot/code/linq53.l new file mode 100644 index 0000000..8ad499d --- /dev/null +++ b/wwwroot/code/linq53.l @@ -0,0 +1,4 @@ +(let ( (product-first-chars (map #(:0 (.ProductName %)) products-list)) + (customer-first-chars (map #(:0 (.CompanyName %)) customers-list)) ) + (println "First letters from Product names, but not from Customer names:") + (joinln (/except product-first-chars customer-first-chars))) \ No newline at end of file diff --git a/wwwroot/code/linq53.sc b/wwwroot/code/linq53.sc new file mode 100644 index 0000000..e877ac6 --- /dev/null +++ b/wwwroot/code/linq53.sc @@ -0,0 +1,6 @@ +products |> map => it.ProductName[0] |> to => productFirstChars +customers |> map => it.CompanyName[0] |> to => customerFirstChars +`First letters from Product names, but not from Customer names:` +#each productFirstChars.except(customerFirstChars) + it +/each \ No newline at end of file diff --git a/wwwroot/code/linq53.ss b/wwwroot/code/linq53.ss new file mode 100644 index 0000000..67899cd --- /dev/null +++ b/wwwroot/code/linq53.ss @@ -0,0 +1,10 @@ +{{ products + |> map => it.ProductName[0] + |> to => productFirstChars }} +{{ customers + |> map => it.CompanyName[0] + |> to => customerFirstChars }} +First letters from Product names, but not from Customer names: +{{#each productFirstChars.except(customerFirstChars) }} + {{it}} +{{/each}} \ No newline at end of file diff --git a/wwwroot/code/linq54.l b/wwwroot/code/linq54.l new file mode 100644 index 0000000..7c8c40c --- /dev/null +++ b/wwwroot/code/linq54.l @@ -0,0 +1,3 @@ +(let ( (dbls [1.7 2.3 1.9 4.1 2.9]) ) + (println "Every other double from highest to lowest:") + (joinln (/step (reverse (sort dbls)) { :by 2 }))) \ No newline at end of file diff --git a/wwwroot/code/linq54.sc b/wwwroot/code/linq54.sc new file mode 100644 index 0000000..60b5fbc --- /dev/null +++ b/wwwroot/code/linq54.sc @@ -0,0 +1,3 @@ +[ 1.7, 2.3, 1.9, 4.1, 2.9 ] |> to => doubles +`Every other double from highest to lowest:` +doubles |> orderByDescending => it |> step({ by: 2 }) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq54.ss b/wwwroot/code/linq54.ss new file mode 100644 index 0000000..7394c88 --- /dev/null +++ b/wwwroot/code/linq54.ss @@ -0,0 +1,6 @@ +{{ [ 1.7, 2.3, 1.9, 4.1, 2.9 ] |> to => doubles }} +Every other double from highest to lowest: +{{ doubles + |> orderByDescending => it + |> step({ by: 2 }) + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq55.l b/wwwroot/code/linq55.l new file mode 100644 index 0000000..d3eb5df --- /dev/null +++ b/wwwroot/code/linq55.l @@ -0,0 +1,3 @@ +(let ( (words ["cherry" "apple" "blueberry"]) ) + (println "The sorted word list:") + (joinln (to-list (sort words)))) \ No newline at end of file diff --git a/wwwroot/code/linq55.sc b/wwwroot/code/linq55.sc new file mode 100644 index 0000000..6674422 --- /dev/null +++ b/wwwroot/code/linq55.sc @@ -0,0 +1,3 @@ +[ 'cherry', 'apple', 'blueberry' ] |> to => words +`The sorted word list:` +words |> orderBy => it |> toList |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq55.ss b/wwwroot/code/linq55.ss new file mode 100644 index 0000000..640df80 --- /dev/null +++ b/wwwroot/code/linq55.ss @@ -0,0 +1,6 @@ +{{ [ 'cherry', 'apple', 'blueberry' ] |> to => words }} +The sorted word list: +{{ words + |> orderBy => it + |> toList + |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq56.l b/wwwroot/code/linq56.l new file mode 100644 index 0000000..515da29 --- /dev/null +++ b/wwwroot/code/linq56.l @@ -0,0 +1,4 @@ +(let ( (sorted-records [{ :name "Alice", :score 50 } + { :name "Bob", :score 40 } + { :name "Cathy", :score 45 }]) ) + (println "Bob's score: " (:score (:"Bob" (to-dictionary :name sorted-records))))) \ No newline at end of file diff --git a/wwwroot/code/linq56.sc b/wwwroot/code/linq56.sc new file mode 100644 index 0000000..1067867 --- /dev/null +++ b/wwwroot/code/linq56.sc @@ -0,0 +1,3 @@ +[ {name:'Alice',score:50}, {name:'Bob',score:40}, {name:'Cathy',score:45} ] |> to => records +records |> toDictionary => it.name |> to => scoreRecordsDict +`Bob's score: ${scoreRecordsDict.Bob.score}` \ No newline at end of file diff --git a/wwwroot/code/linq56.ss b/wwwroot/code/linq56.ss new file mode 100644 index 0000000..d4abbd6 --- /dev/null +++ b/wwwroot/code/linq56.ss @@ -0,0 +1,3 @@ +{{ [{name:'Alice',score:50},{name:'Bob',score:40},{name:'Cathy',score:45}] |> to => records }} +{{ records |> toDictionary => it.name |> to => scoreRecordsDict }} +Bob's score: {{ scoreRecordsDict.Bob.score }} \ No newline at end of file diff --git a/wwwroot/code/linq57.l b/wwwroot/code/linq57.l new file mode 100644 index 0000000..60ded73 --- /dev/null +++ b/wwwroot/code/linq57.l @@ -0,0 +1,3 @@ +(let ( (numbers [nil 1.0 "two" 3 "four" 5 "six" 7.0]) ) + (println "Numbers stored as doubles:") + (joinln (/of numbers { :type "Double" }))) \ No newline at end of file diff --git a/wwwroot/code/linq57.sc b/wwwroot/code/linq57.sc new file mode 100644 index 0000000..e1580a3 --- /dev/null +++ b/wwwroot/code/linq57.sc @@ -0,0 +1,3 @@ +[null, 1.0, 'two', 3, 'four', 5, 'six', 7.0] |> to => numbers +`Numbers stored as doubles:` +numbers |> of({ type: 'Double' }) |> map => `${it.format('#.0') }` |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq57.ss b/wwwroot/code/linq57.ss new file mode 100644 index 0000000..a6e79ca --- /dev/null +++ b/wwwroot/code/linq57.ss @@ -0,0 +1,5 @@ +{{ [null, 1.0, 'two', 3, 'four', 5, 'six', 7.0] |> to => numbers }} +Numbers stored as doubles: +{{ numbers + |> of({ type: 'Double' }) + |> select: { it |> format('#.0') }\n }} \ No newline at end of file diff --git a/wwwroot/code/linq58.l b/wwwroot/code/linq58.l new file mode 100644 index 0000000..fe26b92 --- /dev/null +++ b/wwwroot/code/linq58.l @@ -0,0 +1 @@ +(dump (first (where #(= (.ProductId %) 12) products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq58.sc b/wwwroot/code/linq58.sc new file mode 100644 index 0000000..269c3e1 --- /dev/null +++ b/wwwroot/code/linq58.sc @@ -0,0 +1 @@ +products |> where => it.ProductId == 12 |> first |> dump \ No newline at end of file diff --git a/wwwroot/code/linq58.ss b/wwwroot/code/linq58.ss new file mode 100644 index 0000000..4b1fe0b --- /dev/null +++ b/wwwroot/code/linq58.ss @@ -0,0 +1,4 @@ +{{ products + |> where => it.ProductId == 12 + |> first + |> dump }} \ No newline at end of file diff --git a/wwwroot/code/linq59.l b/wwwroot/code/linq59.l new file mode 100644 index 0000000..3a999c3 --- /dev/null +++ b/wwwroot/code/linq59.l @@ -0,0 +1,2 @@ +(let ( (strings ["zero" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine"]) ) + (println "A string starting with 'o': " (first (where #(/startsWith % "o") strings)))) \ No newline at end of file diff --git a/wwwroot/code/linq59.sc b/wwwroot/code/linq59.sc new file mode 100644 index 0000000..cc7d280 --- /dev/null +++ b/wwwroot/code/linq59.sc @@ -0,0 +1,4 @@ +['zero','one','two','three','four','five','six','seven','eight','nine'] |> to => strings +#each s in strings where s[0] == 'o' take 1 +`A string starting with 'o': ${s}` +/each \ No newline at end of file diff --git a/wwwroot/code/linq59.ss b/wwwroot/code/linq59.ss new file mode 100644 index 0000000..f1d5b62 --- /dev/null +++ b/wwwroot/code/linq59.ss @@ -0,0 +1,4 @@ +{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] |> to =>strings }} +{{#each s in strings where s[0] == 'o' take 1 }} +A string starting with 'o': {{s}} +{{/each}} diff --git a/wwwroot/code/linq61.l b/wwwroot/code/linq61.l new file mode 100644 index 0000000..cd7d1c5 --- /dev/null +++ b/wwwroot/code/linq61.l @@ -0,0 +1 @@ +(or (first []) "null") \ No newline at end of file diff --git a/wwwroot/code/linq61.sc b/wwwroot/code/linq61.sc new file mode 100644 index 0000000..dc1f6d3 --- /dev/null +++ b/wwwroot/code/linq61.sc @@ -0,0 +1 @@ +([].first()) ?? 'null' \ No newline at end of file diff --git a/wwwroot/code/linq61.ss b/wwwroot/code/linq61.ss new file mode 100644 index 0000000..66a4391 --- /dev/null +++ b/wwwroot/code/linq61.ss @@ -0,0 +1,2 @@ +{{ [] |> to => numbers }} +{{ numbers.first() ?? 'null' }} \ No newline at end of file diff --git a/wwwroot/code/linq62.l b/wwwroot/code/linq62.l new file mode 100644 index 0000000..8936a05 --- /dev/null +++ b/wwwroot/code/linq62.l @@ -0,0 +1,2 @@ +(let ( (product-789 (first (where #(= (.ProductId %) 789) products-list))) ) + (println "Product 789 exists: " (not= product-789 nil))) \ No newline at end of file diff --git a/wwwroot/code/linq62.sc b/wwwroot/code/linq62.sc new file mode 100644 index 0000000..546dc39 --- /dev/null +++ b/wwwroot/code/linq62.sc @@ -0,0 +1,2 @@ +products |> first => it.ProductId = 789 |> to => product789 +`Product 789 exists: ${ product789 != null }` \ No newline at end of file diff --git a/wwwroot/code/linq62.ss b/wwwroot/code/linq62.ss new file mode 100644 index 0000000..c40248d --- /dev/null +++ b/wwwroot/code/linq62.ss @@ -0,0 +1,2 @@ +{{ products |> first => it.ProductId = 789 |> to => product789 }} +Product 789 exists: {{ product789 != null }} \ No newline at end of file diff --git a/wwwroot/code/linq64.l b/wwwroot/code/linq64.l new file mode 100644 index 0000000..61f2271 --- /dev/null +++ b/wwwroot/code/linq64.l @@ -0,0 +1,2 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "Second number > 5: " (:1 (where #(> % 5) numbers)))) \ No newline at end of file diff --git a/wwwroot/code/linq64.sc b/wwwroot/code/linq64.sc new file mode 100644 index 0000000..e752b6b --- /dev/null +++ b/wwwroot/code/linq64.sc @@ -0,0 +1,3 @@ +[ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ] |> to => numbers +numbers |> where => it > 5 | elementAt(1) |> to => fourthLowNum +`Second number > 5: ${fourthLowNum}` \ No newline at end of file diff --git a/wwwroot/code/linq64.ss b/wwwroot/code/linq64.ss new file mode 100644 index 0000000..5dd94ee --- /dev/null +++ b/wwwroot/code/linq64.ss @@ -0,0 +1,3 @@ +{{ [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ] |> to => numbers }} +{{ numbers |> where => it > 5 | elementAt(1) |> to => fourthLowNum }} +Second number > 5: {{ fourthLowNum }} \ No newline at end of file diff --git a/wwwroot/code/linq65.l b/wwwroot/code/linq65.l new file mode 100644 index 0000000..1a242cf --- /dev/null +++ b/wwwroot/code/linq65.l @@ -0,0 +1,2 @@ +(doseq (n (range 100 150)) + (println "The number " n " is " (if (even? n) "even" "odd"))) \ No newline at end of file diff --git a/wwwroot/code/linq65.sc b/wwwroot/code/linq65.sc new file mode 100644 index 0000000..2aa68cd --- /dev/null +++ b/wwwroot/code/linq65.sc @@ -0,0 +1,3 @@ +#each range(100,50) +`The number ${it} is ${ it.isEven() ? 'even' : 'odd' }.` +/each diff --git a/wwwroot/code/linq65.ss b/wwwroot/code/linq65.ss new file mode 100644 index 0000000..d58f6fe --- /dev/null +++ b/wwwroot/code/linq65.ss @@ -0,0 +1,3 @@ +{{#each range(100,50) }} +The number {{it}} is {{ it.isEven() ? 'even' : 'odd' }}. +{{/each}} diff --git a/wwwroot/code/linq66.l b/wwwroot/code/linq66.l new file mode 100644 index 0000000..2e191b6 --- /dev/null +++ b/wwwroot/code/linq66.l @@ -0,0 +1 @@ +(doseq (n (/repeat 7 10)) (println n)) \ No newline at end of file diff --git a/wwwroot/code/linq66.sc b/wwwroot/code/linq66.sc new file mode 100644 index 0000000..964d239 --- /dev/null +++ b/wwwroot/code/linq66.sc @@ -0,0 +1 @@ +10.itemsOf(7) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq66.ss b/wwwroot/code/linq66.ss new file mode 100644 index 0000000..a39d69d --- /dev/null +++ b/wwwroot/code/linq66.ss @@ -0,0 +1 @@ +{{ 10.itemsOf(7) |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq67.l b/wwwroot/code/linq67.l new file mode 100644 index 0000000..99fec4c --- /dev/null +++ b/wwwroot/code/linq67.l @@ -0,0 +1,3 @@ +(let ( (words ["believe" "relief" "receipt" "field"]) ) + (println "There is a word that contains in the list that contains 'ei': " + (any? #(.Contains % "ie") words))) \ No newline at end of file diff --git a/wwwroot/code/linq67.sc b/wwwroot/code/linq67.sc new file mode 100644 index 0000000..6de8f10 --- /dev/null +++ b/wwwroot/code/linq67.sc @@ -0,0 +1,3 @@ +['believe', 'relief', 'receipt', 'field'] |> to => words +words |> any => it.contains('ei') |> to => iAfterE +`There is a word that contains in the list that contains 'ei': ${iAfterE.lower()}` \ No newline at end of file diff --git a/wwwroot/code/linq67.ss b/wwwroot/code/linq67.ss new file mode 100644 index 0000000..15c5ece --- /dev/null +++ b/wwwroot/code/linq67.ss @@ -0,0 +1,3 @@ +{{ ['believe', 'relief', 'receipt', 'field'] |> to => words }} +{{ words |> any => it.contains('ei') |> to => iAfterE }} +There is a word that contains in the list that contains 'ei': {{ iAfterE | lower }} \ No newline at end of file diff --git a/wwwroot/code/linq69.l b/wwwroot/code/linq69.l new file mode 100644 index 0000000..8f55d89 --- /dev/null +++ b/wwwroot/code/linq69.l @@ -0,0 +1,3 @@ +(htmldump (map-where #(any? (fn [p] (= (.UnitsInStock p) 0)) %) + #(it { :category (.Key %), :products % }) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq69.sc b/wwwroot/code/linq69.sc new file mode 100644 index 0000000..fb6abc2 --- /dev/null +++ b/wwwroot/code/linq69.sc @@ -0,0 +1,5 @@ +{{ products + |> groupBy => it.Category + |> where => it.any(it => it.UnitsInStock = 0) + |> let => { Category: it.Key, Products: it } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq69.ss b/wwwroot/code/linq69.ss new file mode 100644 index 0000000..fb6abc2 --- /dev/null +++ b/wwwroot/code/linq69.ss @@ -0,0 +1,5 @@ +{{ products + |> groupBy => it.Category + |> where => it.any(it => it.UnitsInStock = 0) + |> let => { Category: it.Key, Products: it } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq70.l b/wwwroot/code/linq70.l new file mode 100644 index 0000000..8c9651f --- /dev/null +++ b/wwwroot/code/linq70.l @@ -0,0 +1,2 @@ +(let ( (numbers [1 11 3 19 41 65 19]) ) + (println "The list contains only odd numbers: " (all? odd? numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq70.sc b/wwwroot/code/linq70.sc new file mode 100644 index 0000000..3f18267 --- /dev/null +++ b/wwwroot/code/linq70.sc @@ -0,0 +1,3 @@ +[1, 11, 3, 19, 41, 65, 19] |> to => numbers +numbers |> all => it.isOdd() |> to => onlyOdd +`The list contains only odd numbers: ${ onlyOdd }` \ No newline at end of file diff --git a/wwwroot/code/linq70.ss b/wwwroot/code/linq70.ss new file mode 100644 index 0000000..9440272 --- /dev/null +++ b/wwwroot/code/linq70.ss @@ -0,0 +1,3 @@ +{{ [1, 11, 3, 19, 41, 65, 19] |> to => numbers }} +{{ numbers |> all => it.isOdd() |> to => onlyOdd }} +The list contains only odd numbers: {{ onlyOdd }} \ No newline at end of file diff --git a/wwwroot/code/linq72.l b/wwwroot/code/linq72.l new file mode 100644 index 0000000..3e8fced --- /dev/null +++ b/wwwroot/code/linq72.l @@ -0,0 +1,3 @@ +(htmldump (map-where #(all? (fn [p] (> (.UnitsInStock p) 0)) %) + #(it { :category (.Key %), :products % }) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq72.sc b/wwwroot/code/linq72.sc new file mode 100644 index 0000000..cb48e00 --- /dev/null +++ b/wwwroot/code/linq72.sc @@ -0,0 +1,5 @@ +{{ products + |> groupBy => it.Category + |> where => it.all(it => it.UnitsInStock > 0) + |> let => { Category: it.Key, Products: it } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq72.ss b/wwwroot/code/linq72.ss new file mode 100644 index 0000000..cb48e00 --- /dev/null +++ b/wwwroot/code/linq72.ss @@ -0,0 +1,5 @@ +{{ products + |> groupBy => it.Category + |> where => it.all(it => it.UnitsInStock > 0) + |> let => { Category: it.Key, Products: it } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq73.l b/wwwroot/code/linq73.l new file mode 100644 index 0000000..71d0ad0 --- /dev/null +++ b/wwwroot/code/linq73.l @@ -0,0 +1,2 @@ +(let ( (factors-of-300 [2 2 3 5 5]) ) + (println "There are " (count (/distinct factors-of-300)) " unique factors of 300.")) \ No newline at end of file diff --git a/wwwroot/code/linq73.sc b/wwwroot/code/linq73.sc new file mode 100644 index 0000000..3861165 --- /dev/null +++ b/wwwroot/code/linq73.sc @@ -0,0 +1,3 @@ +[2, 2, 3, 5, 5] |> to => factorsOf300 +factorsOf300.distinct().count() |> to => uniqueFactors +`There are ${uniqueFactors} unique factors of 300.` \ No newline at end of file diff --git a/wwwroot/code/linq73.ss b/wwwroot/code/linq73.ss new file mode 100644 index 0000000..086b704 --- /dev/null +++ b/wwwroot/code/linq73.ss @@ -0,0 +1,3 @@ +{{ [2, 2, 3, 5, 5] |> to => factorsOf300 }} +{{ factorsOf300.distinct().count() |> to => uniqueFactors }} +There are {{uniqueFactors}} unique factors of 300. \ No newline at end of file diff --git a/wwwroot/code/linq74.l b/wwwroot/code/linq74.l new file mode 100644 index 0000000..f16a57d --- /dev/null +++ b/wwwroot/code/linq74.l @@ -0,0 +1,2 @@ +(let ( (numbers [4 5 1 3 9 0 6 7 2 0]) ) + (println "There are " (count (where odd? numbers)) " odd numbers in the list.")) \ No newline at end of file diff --git a/wwwroot/code/linq74.sc b/wwwroot/code/linq74.sc new file mode 100644 index 0000000..a79f801 --- /dev/null +++ b/wwwroot/code/linq74.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +numbers |> count => it.isOdd() |> to => oddNumbers +`There are ${oddNumbers} odd numbers in the list.` \ No newline at end of file diff --git a/wwwroot/code/linq74.ss b/wwwroot/code/linq74.ss new file mode 100644 index 0000000..9fefd70 --- /dev/null +++ b/wwwroot/code/linq74.ss @@ -0,0 +1,3 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ numbers |> count => it.isOdd() |> to => oddNumbers }} +There are {{oddNumbers}} odd numbers in the list. \ No newline at end of file diff --git a/wwwroot/code/linq76.l b/wwwroot/code/linq76.l new file mode 100644 index 0000000..3b76000 --- /dev/null +++ b/wwwroot/code/linq76.l @@ -0,0 +1,5 @@ +(doseq (x (map #(it { + :customer-id (.CustomerId %) + :order-count (count (.Orders %)) }) + customers-list)) + (println (:customer-id x) ", " (:order-count x))) \ No newline at end of file diff --git a/wwwroot/code/linq76.sc b/wwwroot/code/linq76.sc new file mode 100644 index 0000000..5cdf23f --- /dev/null +++ b/wwwroot/code/linq76.sc @@ -0,0 +1,3 @@ +{{ customers + |> let => { it.CustomerId, OrderCount: it.Orders.count() } + |> map => `${CustomerId}, ${OrderCount}` |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq76.ss b/wwwroot/code/linq76.ss new file mode 100644 index 0000000..5cdf23f --- /dev/null +++ b/wwwroot/code/linq76.ss @@ -0,0 +1,3 @@ +{{ customers + |> let => { it.CustomerId, OrderCount: it.Orders.count() } + |> map => `${CustomerId}, ${OrderCount}` |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq77.l b/wwwroot/code/linq77.l new file mode 100644 index 0000000..0adc47c --- /dev/null +++ b/wwwroot/code/linq77.l @@ -0,0 +1,4 @@ +(htmldump (map #(it { + :category (.Key %) + :product-count (count %) }) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq77.sc b/wwwroot/code/linq77.sc new file mode 100644 index 0000000..bb54963 --- /dev/null +++ b/wwwroot/code/linq77.sc @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> let => { Category: it.Key, ProductCount: it.count() } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq77.ss b/wwwroot/code/linq77.ss new file mode 100644 index 0000000..bb54963 --- /dev/null +++ b/wwwroot/code/linq77.ss @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> let => { Category: it.Key, ProductCount: it.count() } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq78.l b/wwwroot/code/linq78.l new file mode 100644 index 0000000..f9c2527 --- /dev/null +++ b/wwwroot/code/linq78.l @@ -0,0 +1,2 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "The sum of the numbers is " (sum numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq78.sc b/wwwroot/code/linq78.sc new file mode 100644 index 0000000..10a1e12 --- /dev/null +++ b/wwwroot/code/linq78.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +numbers.sum() |> to => numSum +`The sum of the numbers is ${numSum}.` \ No newline at end of file diff --git a/wwwroot/code/linq78.ss b/wwwroot/code/linq78.ss new file mode 100644 index 0000000..7770fbb --- /dev/null +++ b/wwwroot/code/linq78.ss @@ -0,0 +1,3 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ numbers.sum() |> to => numSum }} +The sum of the numbers is {{numSum}}. \ No newline at end of file diff --git a/wwwroot/code/linq79.l b/wwwroot/code/linq79.l new file mode 100644 index 0000000..5926350 --- /dev/null +++ b/wwwroot/code/linq79.l @@ -0,0 +1,2 @@ +(let ( (words ["cherry", "apple", "blueberry"]) ) + (println "There are a total of " (sum (map count words)) " characters in these words.")) \ No newline at end of file diff --git a/wwwroot/code/linq79.sc b/wwwroot/code/linq79.sc new file mode 100644 index 0000000..e9c8df8 --- /dev/null +++ b/wwwroot/code/linq79.sc @@ -0,0 +1,3 @@ +[ 'cherry', 'apple', 'blueberry'] |> to => words +words |> sum => it.Length |> to => totalChars +`There are a total of ${totalChars} characters in these words.` \ No newline at end of file diff --git a/wwwroot/code/linq79.ss b/wwwroot/code/linq79.ss new file mode 100644 index 0000000..68991da --- /dev/null +++ b/wwwroot/code/linq79.ss @@ -0,0 +1,3 @@ +{{ [ 'cherry', 'apple', 'blueberry'] |> to => words }} +{{ words |> sum => it.Length |> to => totalChars }} +There are a total of {{totalChars}} characters in these words. \ No newline at end of file diff --git a/wwwroot/code/linq80.l b/wwwroot/code/linq80.l new file mode 100644 index 0000000..8d6c937 --- /dev/null +++ b/wwwroot/code/linq80.l @@ -0,0 +1,4 @@ +(htmldump (map #(it { + :category (.Key %) + :total-units-in-stock (sum (map .UnitsInStock %)) }) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq80.sc b/wwwroot/code/linq80.sc new file mode 100644 index 0000000..965a9f8 --- /dev/null +++ b/wwwroot/code/linq80.sc @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> map => { Category: it.Key, TotalUnitsInStock: it.sum(p => p.UnitsInStock) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq80.ss b/wwwroot/code/linq80.ss new file mode 100644 index 0000000..965a9f8 --- /dev/null +++ b/wwwroot/code/linq80.ss @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> map => { Category: it.Key, TotalUnitsInStock: it.sum(p => p.UnitsInStock) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq81.l b/wwwroot/code/linq81.l new file mode 100644 index 0000000..e6fd7df --- /dev/null +++ b/wwwroot/code/linq81.l @@ -0,0 +1,2 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "The minimum number is " (apply min numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq81.sc b/wwwroot/code/linq81.sc new file mode 100644 index 0000000..03bca62 --- /dev/null +++ b/wwwroot/code/linq81.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +numbers.min() |> to => minNum +`The minimum number is ${minNum}.` \ No newline at end of file diff --git a/wwwroot/code/linq81.ss b/wwwroot/code/linq81.ss new file mode 100644 index 0000000..5ae9718 --- /dev/null +++ b/wwwroot/code/linq81.ss @@ -0,0 +1,3 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ numbers.min() |> to => minNum }} +The minimum number is {{minNum}}. \ No newline at end of file diff --git a/wwwroot/code/linq82.l b/wwwroot/code/linq82.l new file mode 100644 index 0000000..91b5b0e --- /dev/null +++ b/wwwroot/code/linq82.l @@ -0,0 +1,2 @@ +(let ( (words ["cherry", "apple", "blueberry"]) ) + (println "The shortest word is " (apply min (map count words)) " characters long.")) \ No newline at end of file diff --git a/wwwroot/code/linq82.sc b/wwwroot/code/linq82.sc new file mode 100644 index 0000000..766ff6c --- /dev/null +++ b/wwwroot/code/linq82.sc @@ -0,0 +1,3 @@ +[ 'cherry', 'apple', 'blueberry' ] |> to => words +words |> min => it.Length |> to => shortestWord +`The shortest word is ${shortestWord} characters long.` \ No newline at end of file diff --git a/wwwroot/code/linq82.ss b/wwwroot/code/linq82.ss new file mode 100644 index 0000000..abbf86e --- /dev/null +++ b/wwwroot/code/linq82.ss @@ -0,0 +1,3 @@ +{{ [ 'cherry', 'apple', 'blueberry' ] |> to => words }} +{{ words |> min => it.Length |> to => shortestWord }} +The shortest word is {{shortestWord}} characters long. \ No newline at end of file diff --git a/wwwroot/code/linq83.l b/wwwroot/code/linq83.l new file mode 100644 index 0000000..edfcfdc --- /dev/null +++ b/wwwroot/code/linq83.l @@ -0,0 +1,4 @@ +(htmldump (map #(it { + :category (.Key %) + :cheapest-price (apply min (map .UnitPrice %)) }) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq83.sc b/wwwroot/code/linq83.sc new file mode 100644 index 0000000..a1c1cbf --- /dev/null +++ b/wwwroot/code/linq83.sc @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> map => { Category: it.Key, CheapestPrice: it.min(p => p.UnitPrice) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq83.ss b/wwwroot/code/linq83.ss new file mode 100644 index 0000000..a1c1cbf --- /dev/null +++ b/wwwroot/code/linq83.ss @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> map => { Category: it.Key, CheapestPrice: it.min(p => p.UnitPrice) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq84.l b/wwwroot/code/linq84.l new file mode 100644 index 0000000..d1b86e0 --- /dev/null +++ b/wwwroot/code/linq84.l @@ -0,0 +1,6 @@ +(htmldump (map (fn [g] + (let ( (min-price (apply min (map .UnitPrice g))) ) { + :category (.Key g) + :cheapest-products (where #(= (.UnitPrice %) min-price) g) + })) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq84.sc b/wwwroot/code/linq84.sc new file mode 100644 index 0000000..4058552 --- /dev/null +++ b/wwwroot/code/linq84.sc @@ -0,0 +1,8 @@ +{{ products + |> groupBy => it.Category + |> let => { + g: it, + MinPrice: it.min(p => p.UnitPrice), + } + |> map => { Category: g.Key, CheapestProducts: g.where(p => p.UnitPrice == MinPrice) } + |> htmlDump }} diff --git a/wwwroot/code/linq84.ss b/wwwroot/code/linq84.ss new file mode 100644 index 0000000..4058552 --- /dev/null +++ b/wwwroot/code/linq84.ss @@ -0,0 +1,8 @@ +{{ products + |> groupBy => it.Category + |> let => { + g: it, + MinPrice: it.min(p => p.UnitPrice), + } + |> map => { Category: g.Key, CheapestProducts: g.where(p => p.UnitPrice == MinPrice) } + |> htmlDump }} diff --git a/wwwroot/code/linq85.l b/wwwroot/code/linq85.l new file mode 100644 index 0000000..693c86e --- /dev/null +++ b/wwwroot/code/linq85.l @@ -0,0 +1,2 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "The maximum number is " (apply max numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq85.sc b/wwwroot/code/linq85.sc new file mode 100644 index 0000000..df19371 --- /dev/null +++ b/wwwroot/code/linq85.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +numbers.max() |> to => maxNum +`The maximum number is ${maxNum}.` \ No newline at end of file diff --git a/wwwroot/code/linq85.ss b/wwwroot/code/linq85.ss new file mode 100644 index 0000000..56cfa1b --- /dev/null +++ b/wwwroot/code/linq85.ss @@ -0,0 +1,3 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ numbers.max() |> to => maxNum }} +The maximum number is {{maxNum}}. \ No newline at end of file diff --git a/wwwroot/code/linq86.l b/wwwroot/code/linq86.l new file mode 100644 index 0000000..ff7a8a2 --- /dev/null +++ b/wwwroot/code/linq86.l @@ -0,0 +1,2 @@ +(let ( (words ["cherry", "apple", "blueberry"]) ) + (println "The longest word is " (apply max (map count words)) " characters long.")) \ No newline at end of file diff --git a/wwwroot/code/linq86.sc b/wwwroot/code/linq86.sc new file mode 100644 index 0000000..7777fbe --- /dev/null +++ b/wwwroot/code/linq86.sc @@ -0,0 +1,3 @@ +[ 'cherry', 'apple', 'blueberry' ] |> to => words +words |> max => it.Length |> to => longestLength +`The longest word is ${longestLength} characters long.` \ No newline at end of file diff --git a/wwwroot/code/linq86.ss b/wwwroot/code/linq86.ss new file mode 100644 index 0000000..5015fd3 --- /dev/null +++ b/wwwroot/code/linq86.ss @@ -0,0 +1,3 @@ +{{ [ 'cherry', 'apple', 'blueberry' ] |> to => words }} +{{ words |> max => it.Length |> to => longestLength }} +The longest word is {{longestLength}} characters long. \ No newline at end of file diff --git a/wwwroot/code/linq87.l b/wwwroot/code/linq87.l new file mode 100644 index 0000000..6005534 --- /dev/null +++ b/wwwroot/code/linq87.l @@ -0,0 +1,5 @@ +(htmldump (map #(it { + :category (.Key %) + :most-expensive-price (apply max (map .UnitPrice %)) + }) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq87.sc b/wwwroot/code/linq87.sc new file mode 100644 index 0000000..aded549 --- /dev/null +++ b/wwwroot/code/linq87.sc @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> map => { Category: it.Key, MostExpensivePrice: it.max(p => p.UnitPrice) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq87.ss b/wwwroot/code/linq87.ss new file mode 100644 index 0000000..aded549 --- /dev/null +++ b/wwwroot/code/linq87.ss @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> map => { Category: it.Key, MostExpensivePrice: it.max(p => p.UnitPrice) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq88.l b/wwwroot/code/linq88.l new file mode 100644 index 0000000..89b0f48 --- /dev/null +++ b/wwwroot/code/linq88.l @@ -0,0 +1,6 @@ +(htmldump (map (fn [g] ( + let ( (max-price (apply max (map .UnitPrice g))) ) { + :category (.Key g) + :most-expensive-products (where #(= (.UnitPrice %) max-price) g) + })) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq88.sc b/wwwroot/code/linq88.sc new file mode 100644 index 0000000..8feea00 --- /dev/null +++ b/wwwroot/code/linq88.sc @@ -0,0 +1,8 @@ +{{ products + |> groupBy => it.Category + |> let => { + g: it, + MaxPrice: it.max(p => p.UnitPrice), + } + |> map => { Category: g.Key, MostExpensiveProducts: g.where(p => p.UnitPrice = MaxPrice) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq88.ss b/wwwroot/code/linq88.ss new file mode 100644 index 0000000..8feea00 --- /dev/null +++ b/wwwroot/code/linq88.ss @@ -0,0 +1,8 @@ +{{ products + |> groupBy => it.Category + |> let => { + g: it, + MaxPrice: it.max(p => p.UnitPrice), + } + |> map => { Category: g.Key, MostExpensiveProducts: g.where(p => p.UnitPrice = MaxPrice) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq89.l b/wwwroot/code/linq89.l new file mode 100644 index 0000000..0a0fb8c --- /dev/null +++ b/wwwroot/code/linq89.l @@ -0,0 +1,2 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) ) + (println "The average number is " (average numbers))) \ No newline at end of file diff --git a/wwwroot/code/linq89.sc b/wwwroot/code/linq89.sc new file mode 100644 index 0000000..fdf9a20 --- /dev/null +++ b/wwwroot/code/linq89.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +numbers.average() |> to => averageNum +`The average number is ${averageNum}.` \ No newline at end of file diff --git a/wwwroot/code/linq89.ss b/wwwroot/code/linq89.ss new file mode 100644 index 0000000..9c0f183 --- /dev/null +++ b/wwwroot/code/linq89.ss @@ -0,0 +1,3 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ numbers.average() |> to => averageNum }} +The average number is {{averageNum}}. \ No newline at end of file diff --git a/wwwroot/code/linq90.l b/wwwroot/code/linq90.l new file mode 100644 index 0000000..26fdd87 --- /dev/null +++ b/wwwroot/code/linq90.l @@ -0,0 +1,2 @@ +(let ( (words ["cherry", "apple", "blueberry"]) ) + (println "The average word length is " (apply average (map count words)) " characters.")) \ No newline at end of file diff --git a/wwwroot/code/linq90.sc b/wwwroot/code/linq90.sc new file mode 100644 index 0000000..f33bb2b --- /dev/null +++ b/wwwroot/code/linq90.sc @@ -0,0 +1,3 @@ +[ 'cherry', 'apple', 'blueberry' ] |> to => words +words |> average => it.Length |> to => averageLength +`The average word length is ${averageLength} characters.` \ No newline at end of file diff --git a/wwwroot/code/linq90.ss b/wwwroot/code/linq90.ss new file mode 100644 index 0000000..b969528 --- /dev/null +++ b/wwwroot/code/linq90.ss @@ -0,0 +1,3 @@ +{{ [ 'cherry', 'apple', 'blueberry' ] |> to => words }} +{{ words |> average => it.Length |> to => averageLength }} +The average word length is {{averageLength}} characters. \ No newline at end of file diff --git a/wwwroot/code/linq91.l b/wwwroot/code/linq91.l new file mode 100644 index 0000000..16a696f --- /dev/null +++ b/wwwroot/code/linq91.l @@ -0,0 +1,5 @@ +(htmldump (map #(it { + :category (.Key %) + :average-price (apply average (map .UnitPrice %)) + }) + (group-by .Category products-list))) \ No newline at end of file diff --git a/wwwroot/code/linq91.sc b/wwwroot/code/linq91.sc new file mode 100644 index 0000000..879bd24 --- /dev/null +++ b/wwwroot/code/linq91.sc @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> map => { Category: it.Key, AveragePrice: it.average(p => p.UnitPrice) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq91.ss b/wwwroot/code/linq91.ss new file mode 100644 index 0000000..879bd24 --- /dev/null +++ b/wwwroot/code/linq91.ss @@ -0,0 +1,4 @@ +{{ products + |> groupBy => it.Category + |> map => { Category: it.Key, AveragePrice: it.average(p => p.UnitPrice) } + |> htmlDump }} \ No newline at end of file diff --git a/wwwroot/code/linq92.l b/wwwroot/code/linq92.l new file mode 100644 index 0000000..1e82212 --- /dev/null +++ b/wwwroot/code/linq92.l @@ -0,0 +1,2 @@ +(let ( (dbls [1.7 2.3 1.9 4.1 2.9]) ) + (println "Total product of all numbers: " (reduce * dbls))) \ No newline at end of file diff --git a/wwwroot/code/linq92.sc b/wwwroot/code/linq92.sc new file mode 100644 index 0000000..f5f070b --- /dev/null +++ b/wwwroot/code/linq92.sc @@ -0,0 +1,3 @@ +[1.7, 2.3, 1.9, 4.1, 2.9] |> to => doubles +doubles.reduce((runningProduct, nextFactor) => runningProduct * nextFactor, 1) |> to => product +`Total product of all numbers: ${ product }.` \ No newline at end of file diff --git a/wwwroot/code/linq92.ss b/wwwroot/code/linq92.ss new file mode 100644 index 0000000..7774cdf --- /dev/null +++ b/wwwroot/code/linq92.ss @@ -0,0 +1,4 @@ +{{ [1.7, 2.3, 1.9, 4.1, 2.9] |> to => doubles }} +{{ doubles.reduce((runningProduct, nextFactor) => runningProduct * nextFactor, 1) + |> to => product }} +Total product of all numbers: {{ product }}. \ No newline at end of file diff --git a/wwwroot/code/linq93.l b/wwwroot/code/linq93.l new file mode 100644 index 0000000..629b2f8 --- /dev/null +++ b/wwwroot/code/linq93.l @@ -0,0 +1,6 @@ +(let ( (start-balance 100) + (attempted-withdrawls [20 10 40 50 10 70 30]) + (end-balance) ) + (setq end-balance (reduce (fn [balance cut] (if (> balance cut) (- balance cut) balance)) + attempted-withdrawls start-balance)) + (println "Ending balance: " end-balance)) \ No newline at end of file diff --git a/wwwroot/code/linq93.sc b/wwwroot/code/linq93.sc new file mode 100644 index 0000000..cd72b14 --- /dev/null +++ b/wwwroot/code/linq93.sc @@ -0,0 +1,5 @@ +[20, 10, 40, 50, 10, 70, 30] |> to => attemptedWithdrawals +{{ attemptedWithdrawals.reduce((balance, nextWithdrawal) => + ((nextWithdrawal <= balance) ? (balance - nextWithdrawal) : balance), 100.0) + |> to => endBalance }} +`Ending balance: ${endBalance}.` \ No newline at end of file diff --git a/wwwroot/code/linq93.ss b/wwwroot/code/linq93.ss new file mode 100644 index 0000000..2ac63b0 --- /dev/null +++ b/wwwroot/code/linq93.ss @@ -0,0 +1,5 @@ +{{ [20, 10, 40, 50, 10, 70, 30] |> to => attemptedWithdrawals }} +{{ attemptedWithdrawals.reduce((balance, nextWithdrawal) => + ((nextWithdrawal <= balance) ? (balance - nextWithdrawal) : balance), 100.0) + |> to => endBalance }} +Ending balance: {{endBalance}}. \ No newline at end of file diff --git a/wwwroot/code/linq94.l b/wwwroot/code/linq94.l new file mode 100644 index 0000000..65c375b --- /dev/null +++ b/wwwroot/code/linq94.l @@ -0,0 +1,4 @@ +(let ( (numbers-a [0 2 4 5 6 8 9]) + (numbers-b [1 3 5 7 8]) ) + (println "All numbers from both arrays:") + (joinln (flatten [numbers-a numbers-b]))) \ No newline at end of file diff --git a/wwwroot/code/linq94.sc b/wwwroot/code/linq94.sc new file mode 100644 index 0000000..8931818 --- /dev/null +++ b/wwwroot/code/linq94.sc @@ -0,0 +1,4 @@ +[0, 2, 4, 5, 6, 8, 9] |> to => numbersA +[1, 3, 5, 7, 8] |> to => numbersB +`All numbers from both arrays:` +numbersA.concat(numbersB) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq94.ss b/wwwroot/code/linq94.ss new file mode 100644 index 0000000..f90f638 --- /dev/null +++ b/wwwroot/code/linq94.ss @@ -0,0 +1,4 @@ +{{ [0, 2, 4, 5, 6, 8, 9] |> to => numbersA }} +{{ [1, 3, 5, 7, 8] |> to => numbersB }} +All numbers from both arrays: +{{ numbersA.concat(numbersB) |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq95.l b/wwwroot/code/linq95.l new file mode 100644 index 0000000..6d6eaff --- /dev/null +++ b/wwwroot/code/linq95.l @@ -0,0 +1,4 @@ +(let ( (customer-names (map .CompanyName customers-list)) + (product-names (map .ProductName products-list)) ) + (println "Customer and product names:") + (joinln (flatten [customer-names product-names]))) \ No newline at end of file diff --git a/wwwroot/code/linq95.sc b/wwwroot/code/linq95.sc new file mode 100644 index 0000000..f524706 --- /dev/null +++ b/wwwroot/code/linq95.sc @@ -0,0 +1,4 @@ +customers |> map => it.CompanyName |> to => customerNames +products |> map => it.ProductName |> to => productNames +`Customer and product names:` +customerNames.concat(productNames) |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq95.ss b/wwwroot/code/linq95.ss new file mode 100644 index 0000000..1c494be --- /dev/null +++ b/wwwroot/code/linq95.ss @@ -0,0 +1,8 @@ +{{ customers + |> map => it.CompanyName + |> to => customerNames }} +{{ products + |> map => it.ProductName + |> to => productNames }} +Customer and product names: +{{ customerNames.concat(productNames) |> joinln }} \ No newline at end of file diff --git a/wwwroot/code/linq96.l b/wwwroot/code/linq96.l new file mode 100644 index 0000000..765b36c --- /dev/null +++ b/wwwroot/code/linq96.l @@ -0,0 +1,3 @@ +(let ( (words-a ["cherry" "apple" "blueberry"]) + (words-b ["cherry" "apple" "blueberry"]) ) + (println "The sequences match: " (/sequenceEquals words-a words-b))) \ No newline at end of file diff --git a/wwwroot/code/linq96.sc b/wwwroot/code/linq96.sc new file mode 100644 index 0000000..4976626 --- /dev/null +++ b/wwwroot/code/linq96.sc @@ -0,0 +1,4 @@ +[ 'cherry', 'apple', 'blueberry' ] |> to => wordsA +[ 'cherry', 'apple', 'blueberry' ] |> to => wordsB +wordsA.equivalentTo(wordsB) |> to => match +`The sequences match: ${match.lower()}` \ No newline at end of file diff --git a/wwwroot/code/linq96.ss b/wwwroot/code/linq96.ss new file mode 100644 index 0000000..e834ed9 --- /dev/null +++ b/wwwroot/code/linq96.ss @@ -0,0 +1,4 @@ +{{ [ 'cherry', 'apple', 'blueberry' ] |> to => wordsA }} +{{ [ 'cherry', 'apple', 'blueberry' ] |> to => wordsB }} +{{ wordsA.equivalentTo(wordsB) |> to => match }} +The sequences match: {{ match |> lower }} \ No newline at end of file diff --git a/wwwroot/code/linq97.l b/wwwroot/code/linq97.l new file mode 100644 index 0000000..c7f92b5 --- /dev/null +++ b/wwwroot/code/linq97.l @@ -0,0 +1,3 @@ +(let ( (words-a ["cherry" "apple" "blueberry"]) + (words-b ["apple" "blueberry" "cherry"]) ) + (println "The sequences match: " (/sequenceEquals words-a words-b))) \ No newline at end of file diff --git a/wwwroot/code/linq97.sc b/wwwroot/code/linq97.sc new file mode 100644 index 0000000..28844dd --- /dev/null +++ b/wwwroot/code/linq97.sc @@ -0,0 +1,4 @@ +[ 'cherry', 'apple', 'blueberry' ] |> to => wordsA +[ 'apple', 'blueberry', 'cherry' ] |> to => wordsB +wordsA.equivalentTo(wordsB) |> to => match +`The sequences match: ${match.lower()}` \ No newline at end of file diff --git a/wwwroot/code/linq97.ss b/wwwroot/code/linq97.ss new file mode 100644 index 0000000..8802bf3 --- /dev/null +++ b/wwwroot/code/linq97.ss @@ -0,0 +1,4 @@ +{{ [ 'cherry', 'apple', 'blueberry' ] |> to => wordsA }} +{{ [ 'apple', 'blueberry', 'cherry' ] |> to => wordsB }} +{{ wordsA.equivalentTo(wordsB) |> to => match }} +The sequences match: {{ match |> lower }} diff --git a/wwwroot/code/linq99.l b/wwwroot/code/linq99.l new file mode 100644 index 0000000..7452874 --- /dev/null +++ b/wwwroot/code/linq99.l @@ -0,0 +1,3 @@ +(let ( (numbers [5 4 1 3 9 8 6 7 2 0]) + (i 0) ) + (doseq (v (map #(it (fn [] (f++ i))) numbers)) (println "v = " (v) ", i = " i))) \ No newline at end of file diff --git a/wwwroot/code/linq99.sc b/wwwroot/code/linq99.sc new file mode 100644 index 0000000..ba2fd9f --- /dev/null +++ b/wwwroot/code/linq99.sc @@ -0,0 +1,3 @@ +[5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers +0 |> to => i +numbers |> let => { i: i + 1 } |> map => `v = ${index + 1}, i = ${i}` |> joinln \ No newline at end of file diff --git a/wwwroot/code/linq99.ss b/wwwroot/code/linq99.ss new file mode 100644 index 0000000..eb6c658 --- /dev/null +++ b/wwwroot/code/linq99.ss @@ -0,0 +1,3 @@ +{{ [5, 4, 1, 3, 9, 8, 6, 7, 2, 0] |> to => numbers }} +{{ 0 |> to => i }} +{{ numbers |> let => { i: i + 1 } |> select: v = {index + 1}, i = {i}\n }} \ No newline at end of file diff --git a/wwwroot/code/mv.sc b/wwwroot/code/mv.sc new file mode 100644 index 0000000..93278dd --- /dev/null +++ b/wwwroot/code/mv.sc @@ -0,0 +1,7 @@ +vfsFileSystem('.') |> to => fs + +#each f in fs.findFiles('linq*.txt') + `copy ${f.Name} ${f.Name.lastLeftPart('.')}.sc` |> sh + `copy ${f.Name} ${f.Name.lastLeftPart('.')}.l` |> sh + `move ${f.Name} ${f.Name.lastLeftPart('.')}.ss` |> sh +/each diff --git a/wwwroot/code/rss.l b/wwwroot/code/rss.l new file mode 100644 index 0000000..6e51e39 --- /dev/null +++ b/wwwroot/code/rss.l @@ -0,0 +1,8 @@ +(load "index:parse-rss") + +(def xml (/urlContentsWithCache "https://news.ycombinator.com/rss")) + +(def top-links (take 5 (:items (parse-rss xml)))) +(def n 0) + +(joinln (map #(str (padLeft (incf n) 2) ". " (:title %) "\n " (:link %) "\n") top-links )) diff --git a/wwwroot/doc-links.html b/wwwroot/doc-links.html new file mode 100644 index 0000000..0cbd7ee --- /dev/null +++ b/wwwroot/doc-links.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/wwwroot/docs/api-reference.html b/wwwroot/docs/api-reference.html new file mode 100644 index 0000000..d32c487 --- /dev/null +++ b/wwwroot/docs/api-reference.html @@ -0,0 +1,252 @@ + + +

    Plugins

    + +{{#markdown}} +Plugins are a nice way to extend templates with customized functionality which can encapsulate any number of blocks, filters, +preferred configuration and dependencies by implementing the `IScriptPlugin` interface. + +The [MarkdownScriptPlugin](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack/MarkdownScriptPlugin.cs) +is a good example of this which registers a markdown +[Page format](/docs/page-formats), [Script Methods](/docs/methods), [Filter Transformer](/docs/transformers) and +[markdown Block](/docs/blocks#markdown): +{{/markdown}} + +{{ 'gfm/api-reference/03.md' |> githubMarkdown }} + +{{#markdown}} +#### Removing Plugins + +When needed any default plugins can be removed with the `RemovePlugins()` API: +{{/markdown}} + +{{ 'gfm/blocks/20.md' |> githubMarkdown }} + +{{#markdown}} +#### Advanced plugin registration + +For greater control over the registration and execution of plugins, they can implement `IScriptPluginBefore` to have custom logic +executed before plugins are registered or implement `IScriptPluginAfter` for executing any logic after. +{{/markdown}} + +

    ScriptContext

    + +

    + The ScriptContext is the sandbox where all templates are executed within that can be customized with the + available APIs below: +

    + +

    Preconfigured defaults

    + +

    + Some default scripts when called without arguments will use the default configuration + shown below that can be overridden by replacing their default value in the ScriptContext's Args collection: +

    + +{{ 'gfm/api-reference/01.md' |> githubMarkdown }} + +

    Args

    + +

    + ScriptContext Arguments can be used to define global variables available to every template, partial, filter, etc: +

    + +

    Virtual Files

    + +

    + Templates only have access to Pages available from its configured VirtualFiles which uses an empty MemoryVirtualFiles. + To make pages available to your ScriptContext instance you can choose to either programatically populate the + VirtualFiles collection from an external source, e.g: +

    + +{{ 'gfm/api-reference/02.md' |> githubMarkdown }} + +

    + Alternatively if you want to enable access to an entire sub directory you can replace the Virtual Files with a + FileSystem VFS at the directory you want to make the root directory: +

    + +
    context.VirtualFiles = new FileSystemVirtualFiles("~/template-files".MapProjectPath());
    + +

    DebugMode

    + +

    + DebugMode is used to control whether full Exception details like StackTrace is displayed. In + SharpPagesFeature it defaults to the AppHost DebugMode, otherwise it's true by default. +

    + +

    ScanTypes

    + +

    + Specify a ScriptMethods or SharpCodePage to auto register. +

    + +

    ScanAssemblies

    + +

    + Specify assemblies that should be scanned to find ScriptMethods's and SharpCodePage's to auto register. + In SharpPagesFeature the AppHost's Service Assemblies are included by default. +

    + +

    ScriptMethods

    + +

    + Register additional instances of filters you want templates to have access to. +

    + +

    CodePages

    + +

    + Register instances of code pages you want templates to have access to. +

    + +

    Container

    + +

    + The IOC Container used by the ScriptContext to register and resolve dependencies, filters and Code Pages. + Uses SimpleContainer by default. +

    + +

    AppSettings

    + +

    + Specify an optional + App Settings provider that templates can access with the + {{#raw}}{{ key | appSetting }}{{/raw}} + default filter. +

    + +

    CheckForModifiedPages

    + +

    + Whether to check for modified pages by default when not in DebugMode, defaults to true. + Note: if DebugMode is true it will always check for changes. +

    + +

    CheckForModifiedPagesAfter

    + +

    + If provided will specify how long to wait before checking if backing files of pages have changed and to reload them if they have. + Note: if DebugMode is true it will always check for changes. +

    + +

    RenderExpressionExceptions

    + +

    + Whether to Render Expression Exceptions in-line (default = false). +

    + +

    PageResult

    + +

    + The PageResult is the rendering context used to render templates whose output can be customized with the APIs below: +

    + +

    Layout

    + +

    + Override the layout used for the page by specifying a layout name: +

    + +
    new PageResult(page) { Layout = "custom-layout" }
    + +

    LayoutPage

    + +

    + Override the layout used for the page by specifying a Layout page: +

    + +
    new PageResult(page) { LayoutPage = Request.GetPage("custom-layout") }
    + +

    Args

    + +

    + Override existing or specify additional arguments in the Template's scope: +

    + +
    new PageResult(page) { 
    +    Args = { 
    +        ["myArg"] = "argValue",
    +    }
    +}
    + +

    ScriptMethods

    + +

    + Make additional filters available to the Template: +

    + +
    new PageResult(page) { 
    +    ScriptMethods = { new MyScriptMethods() }
    +}
    + +

    OutputTransformers

    + +

    + Transform the entire Template's Output before rendering to the OutputStream: +

    + +
    new PageResult(page) {
    +    ContentType = MimeTypes.Html,
    +    OutputTransformers = { MarkdownPageFormat.TransformToHtml },
    +}
    + +

    PageTransformers

    + +

    + Transform just the Page's Output before rendering to the OutputStream: +

    + +
    new PageResult(page) {
    +    ContentType = MimeTypes.Html,
    +    PageTransformers = { MarkdownPageFormat.TransformToHtml },
    +}
    + +

    FilterTransformers

    + +

    + Specify additional Filter Transformers available to the Template: +

    + +
    new PageResult(page) {
    +    FilterTransformers = {
    +        ["markdown"] = MarkdownPageFormat.TransformToHtml
    +    }
    +}
    + +

    ExcludeFiltersNamed

    + +

    + Disable access to the specified registered filters: +

    + +
    new PageResult(page) {
    +    ExcludeFiltersNamed = { "partial", "selectPartial" }
    +}
    + +

    Options

    + +

    + Return additional HTTP Response Headers when rendering to a HTTP Response: +

    + +
    new PageResult(page) {
    +    Options = { 
    +        ["X-Powered-By"] = "#Script"
    +    }
    +}
    + +

    ContentType

    + +

    + Specify the HTTP Content-Type when rendering to a HTTP Response: +

    + +
    new PageResult(page) {
    +    ContentType = "text/plain"
    +}
    + +{{ "doc-links" |> partial({ order }) }} diff --git a/src/wwwroot/docs/arguments.html b/wwwroot/docs/arguments.html similarity index 77% rename from src/wwwroot/docs/arguments.html rename to wwwroot/docs/arguments.html index 67baa6e..676ca28 100644 --- a/src/wwwroot/docs/arguments.html +++ b/wwwroot/docs/arguments.html @@ -4,27 +4,27 @@ -->

    - All data and objects made available to templates are done so using arguments. There are a number of different layers in + All data and objects made available to scripts are done so using arguments. There are a number of different layers in which arguments can be defined, each shadowing the outer scope as it gets closer to the call-site where the argument is accessed, behaving similarly to properties defined in a JavaScript prototype chain.

    -

    TemplateContext Arguments

    +

    ScriptContext Arguments

    - Essentially this can be thought of as where global variables are maintained, anything defined here is available to every template, partial, filter, etc: + Essentially this can be thought of as where global variables are maintained, anything defined here is available to every page, partial, method, block, etc:

    -{{ 'gfm/arguments/01.md' | githubMarkdown }} +{{ 'gfm/arguments/01.md' |> githubMarkdown }}

    Page Arguments

    All pages can define their own arguments within the header of each page, for .html files they are defined within HTML Comments - <!-- --> with each argument on a new line delimited by a colon, e.g: + <!-- --> with each argument on a new line delimited by an (optional) colon, e.g:

    -{{ "live-pages" | partial( +{{ "live-pages" |> partial( { rows: 7, page: 'page', @@ -32,7 +32,7 @@

    Page Arguments

    { '_layout.html': ' layout args: {{ arg }}, {{ arg2 }} @@ -56,7 +56,7 @@

    PageResult Arguments

    Arguments can also be defined in the PageResult context that renders the page, this takes precedence over the above:

    -{{ 'gfm/arguments/02.md' | githubMarkdown }} +{{ 'gfm/arguments/02.md' |> githubMarkdown }}

    Scoped Arguments

    @@ -64,7 +64,7 @@

    Scoped Arguments

    Each template can also create their own arguments using the assignTo or assign filters, e.g:

    -{{ "live-pages" | partial( +{{ "live-pages" |> partial( { rows: 7, page: 'page', @@ -81,7 +81,7 @@

    Scoped Arguments

    arg: 3 --> -{{ 6 | assignTo: arg }} +{{ 6 |> to => arg }} page arg: {{ arg }}' } }) @@ -91,4 +91,4 @@

    Scoped Arguments

    These take precedence over all other arguments, but are only available within the scope that defines them.

    -{{ "doc-links" | partial({ order }) }} +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/blocks.html b/wwwroot/docs/blocks.html new file mode 100644 index 0000000..b7f3846 --- /dev/null +++ b/wwwroot/docs/blocks.html @@ -0,0 +1,573 @@ + + +{{#markdown}} +Script Blocks lets you define reusable statements that can be invoked with a new context allowing the creation custom iterators and helpers - +making it easy to encapsulate reusable functionality and reduce boilerplate for common functionality. + +{{/markdown}} + +{{ 'gfm/blocks/00.md' | githubMarkdown | convertScriptToCodeBlocks }} + +{{#markdown}} + +### Syntax + +The syntax for blocks follows the familiar [handlebars block helpers](https://handlebarsjs.com/block_helpers.html) in both syntax and functionality. +`#Script` also includes most of handlebars.js block helpers which are useful in a HTML template language whilst minimizing any porting efforts if +needing to reuse existing JavaScript handlebars templates. + +We'll walk through creating a few of the built-in Script blocks to demonstrate how to create them from scratch. + +### noop + +We'll start with creating the `noop` block (short for "no operation") which functions like a block comment by removing its inner contents +from the rendered page: +{{/markdown}} + +{{ 'gfm/blocks/01.md' |> githubMarkdown }} + +{{#markdown}} +The `noop` block is also the smallest implementation possible which needs to inherit `ScriptBlock` class, overrides the `Name` getter with +the name of the block and implements the `WriteAsync()` method which for the noop block just returns an empty `Task` there by not writing anything +to the Output Stream, resulting in its inner contents being ignored: +{{/markdown}} + +{{ 'gfm/blocks/02.md' |> githubMarkdown }} + +{{#markdown}} +All Block's are executed with 3 parameters: + + - `ScriptScopeContext` - The current Execution and Rendering context + - `PageBlockFragment` - The parsed Block contents + - `CancellationToken` - Allows the async render operation to be cancelled +{{/markdown}} + +

    Registering Blocks

    + +{{#markdown}} +The same flexible registration options for [Registering Script Methods](/docs/methods#registering-methods) is also available for registering blocks +where if it wasn't already built-in, `NoopScriptBlock` could be registered by adding it to the `ScriptBlocks` collection: +{{/markdown}} + +{{ 'gfm/blocks/03.md' |> githubMarkdown }} + +
    Autowired using ScriptContext IOC
    + +{{#markdown}} +Autowired instances of script blocks and methods can also be created using ScriptContext's configured IOC where they're also injected with any +registered IOC dependencies by registering them in the `ScanTypes` collection: +{{/markdown}} + +{{ 'gfm/blocks/04.md' |> githubMarkdown }} + +{{#markdown}} +When the `ScriptContext` is initialized it will go through each Type and create an autowired instance of each Type and register them in the +`ScriptBlocks` collection. An alternative to registering individual Types is to register an entire Assembly, e.g: +{{/markdown}} + +{{ 'gfm/blocks/05.md' |> githubMarkdown }} + +{{#markdown}} +Where it automatically registers any Script Blocks or Methods contained in the Assembly where the `MyBlock` Type is defined. +{{/markdown}} + +{{#markdown}} +### bold + +A step up from `noop` is the **bold** Script Block which markup its contents within the `` tag: +{{/markdown}} + +
    +{{#raw}}{{#bold}}This text will be bold{{/bold}}{{/raw}}
    +
    + +{{#markdown}} +Which calls the `base.WriteBodyAsync()` method to evaluate and write the Block's contents to the `OutputStream` using the current +`ScriptScopeContext`: +{{/markdown}} + +{{ 'gfm/blocks/06.md' |> githubMarkdown }} + +{{#markdown}} +### with + +The `with` Block shows an example of utilizing arguments. To maximize flexibility arguments passed into your block are captured in a free-form +string (specifically a `ReadOnlyMemory`) which allows creating Blocks varying from simple arguments to complex LINQ-like expressions - a +feature some built-in Blocks take advantage of. + +The `with` block works similarly to [handlebars with helper](https://handlebarsjs.com/block_helpers.html#with-helper) or JavaScript's +[with statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with) where it extracts the properties (or Keys) +of an object and adds them to the current scope which avoids needing a prefix each property reference, +e.g. being able to use `{{Name}}` instead of `{{person.Name}}`: + +
    +{{#with person}}
    +    Hi {{Name}}, your Age is {{Age}}.
    +{{/with}}
    +
    + +Also the `with` Block's contents are only evaluated if the argument expression is `null`. + +The implementation below shows the optimal way to implement `with` by calling `GetJsExpressionAndEvaluate()` to resolve a cached +AST token that's then evaluated to return the result of the Argument expression. + +If the argument evaluates to an object it calls the `ToObjectDictionary()` extension method to convert it into a `Dictionary` +then creates a new scope with each property added as arguments and then evaluates the block's Body contents with the new scope: +{{/markdown}} + +{{ 'gfm/blocks/07.md' |> githubMarkdown }} + +{{#markdown}} +To better highlight how this works, a non-cached version of `GetJsExpressionAndEvaluate()` involves parsing the Argument string into +an AST Token then evaluating it with the current scope: +{{/markdown}} + +{{ 'gfm/blocks/08.md' |> githubMarkdown }} + +{{#markdown}} +The `ParseJsExpression()` extension method is able to parse virtually any [JavaScript Expression](/docs/expression-viewer) into an AST tree +which can then be evaluated by calling its `token.Evaluate(scope)` method. + +##### Final implementation + +The actual [WithScriptBlock.cs](https://github.com/ServiceStack/ServiceStack/tree/master/src/ServiceStack.Common/Script/Blocks/WithScriptBlock.cs) +used in #Script includes extended functionality which uses `GetJsExpressionAndEvaluateAsync()` to be able to evaluate both **sync** and **async** +results. + +##### else if/else statements + +It also evaluates any `block.ElseBlocks` statements which is **functionality available to all blocks** which are able to evaluate any alternative +**else/else if** statements when the main template isn't rendered, e.g. in this case when the `with` block is called with a `null` argument: +{{/markdown}} + +{{ 'gfm/blocks/09.md' |> githubMarkdown }} + +{{#markdown}} +### if + +Since all blocks are able to execute any number of `{{else}}` statements by calling `base.WriteElseAsync()`, the implementation for +the `#if` block ends up being even simpler which just needs to evaluate the argument to `bool`. + +If **true** it writes the body with `WriteBodyAsync()` otherwise it evaluates any `else` statements with `WriteElseAsync()`: +{{/markdown}} + +{{ 'gfm/blocks/10.md' |> githubMarkdown }} + +{{#markdown}} + +### while + +Similar to `#if`, the `#while` block takes a boolean expression, except it keeps evaluating its body until the expression evaluates to `false`. + +The implementation includes a safe-guard to ensure it doesn't exceed the configured `ScriptContext.MaxQuota` to avoid infinite recursion: +{{/markdown}} + +{{ 'gfm/blocks/27.md' |> githubMarkdown }} + +{{#markdown}} +### each + +From what we've seen up till now, the [handlebars.js each block](https://handlebarsjs.com/block_helpers.html#iterators) is also +straightforward to implement which just iterates over a collection argument that evaluates its body with a new scope containing the +elements **properties**, a conventional `it` binding for the element and an `index` argument that can be used to determine the +index of each element: +{{/markdown}} + +{{ 'gfm/blocks/11.md' |> githubMarkdown }} + +{{#markdown}} +Despite its terse implementation, the above Script Block can be used to iterate over any expression that evaluates to a collection, +inc. objects, POCOs, strings as well as Value Type collections like ints. + +##### Built-in each + +However the built-in [EachScriptBlock.cs](https://github.com/ServiceStack/ServiceStack/tree/master/src/ServiceStack.Common/Script/Blocks/EachScriptBlock.cs) +has a larger implementation to support its richer feature-set where it also includes support for async results, custom element bindings and LINQ-like syntax for +maximum expressiveness whilst utilizing expression caching to ensure any complex argument expressions are only parsed once. +{{/markdown}} + +{{ 'gfm/blocks/12.md' |> githubMarkdown }} + +{{#markdown}} +By using `ParseJsExpression()` to parse expressions after each "LINQ modifier", `each` supports evaluating complex JavaScript expressions in each +of its LINQ querying features, e.g: +{{/markdown}} + +
    +
    + +
    +
    +
    +
    +
    + +{{#markdown}} + +##### Custom bindings + +When using a custom binding like `{{#each c in customers}}` above, the element is only accessible with the custom `c` binding which is more efficient +when only needing to reference a subset of the element's properties as it avoids adding each of the elements properties in the items execution scope. + +Check out [LINQ Examples](/linq/restriction-operators) for more live previews showcasing advanced usages of the `{{#each}}` block. + +### raw + +The `{{#raw}}` block is similar to [handlebars.js's raw-helper](https://handlebarsjs.com/block_helpers.html#raw-blocks) which captures +the template's raw text content instead of having its content evaluated, making it ideal for emitting content that could contain +template expressions like client-side JavaScript or template expressions that shouldn't be evaluated on the server such as +Vue, Angular or Ember templates: + +
    +{{#raw}}
    +<div id="app">
    +    {{ message }}
    +</div>
    +{{/raw}}
    +
    + +When called with no arguments it will render its unprocessed raw text contents. When called with a single argument, e.g. `{{#raw varname}}` it will +instead save the raw text contents to the specified global `PageResult` variable and lastly when called with the `appendTo` modifier it will append +its contents to the existing variable, or initialize it if it doesn't exist. + +This is now the preferred approach used in all [.NET Core and .NET Framework Web Templates](https://docs.servicestack.net/templates-websites) for +pages and partials to append any custom JavaScript script blocks they need on the page, e.g: + +
    +{{#raw appendTo scripts}}
    +<script>
    +    //...
    +</script>
    +{{/raw}}
    +
    + +Where any captured custom scripts are rendered at the +[bottom of _layout.html](https://github.com/NetCoreTemplates/templates/blob/8a082a299d59a0b53b9b6e0a07a6fbabf7bf6e2c/MyApp/wwwroot/_layout.html#L49) with: + +
    +    <script src="/assets/js/default.js"></script>
    +
    +    {{ scripts |> raw }}
    +
    +</body>
    +</html>
    +
    + +The implementation to support each of these usages is contained within +[RawScriptBlock.cs](https://github.com/ServiceStack/ServiceStack/tree/master/src/ServiceStack.Common/Script/Blocks/RawScriptBlock.cs) +below which inspects the `block.Argument` to determine whether it should capture the contents into the specified variable or write its raw +string contents directly to the OutputStream: +{{/markdown}} + +{{ 'gfm/blocks/13.md' |> githubMarkdown }} + +{{#markdown}} +### function + +The `{{#function}}` block lets you define reusable functions in your page. Unlike other Script Blocks, the body of a function block +is parsed as a [code block](/docs/syntax#code-blocks) where only the **return** value is used. Any output generated from executing the +function is discarded. Use the [partial](#partial) block if you instead want to define reusable fragments. + +In its simplest form, defining a function requires the function name and a body that returns a value with the +[return](/docs/introduction#evaluate-return-values) method, e.g: +{{/markdown}} + +{{ 'gfm/blocks/23.md' |> githubMarkdown }} + +{{#markdown}} + +This creates a compiled delegate and assigns it to the pages scope where it can be invoked as a normal function, e.g: + + hi() + +Functions can specify arguments using a JavaScript arguments list: +{{/markdown}} + +{{ 'gfm/blocks/24.md' |> githubMarkdown }} + +{{#markdown}} + +Where it can be called as normal function or as an [extension method](/docs/syntax#extension-methods): + + calc(1,2) + 1.calc(2) + +Functions can also be called recursively: +{{/markdown}} + +{{ 'gfm/blocks/25.md' |> githubMarkdown }} + +{{#markdown}} +Although their limited to the configured [MaxStackDepth](/docs/sandbox#max-stack-depth). + +Source code for [FunctionScriptBlock.cs](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Script/Blocks/FunctionScriptBlock.cs). + +### defn + +Similar to `{{#function}}` above, the `{{#defn}}` script block lets you define a function using lisp. The resulting function is +exported as a C# delegate where it can be invoked like any other Script method. + +An equivalent `calc` and `fib` function in lisp looks like: + +{{/markdown}} + +{{ 'gfm/blocks/26.md' |> githubMarkdown }} + +{{#markdown}} + +As in most Lisp expressions, the last expression executed is the implicit return value. + +> The `defn` Script Block is automatically registered when the [Lisp language is registered](/lisp/). + +### capture + +The `{{#capture}}` block is similar to the raw block except instead of using its raw text contents, it instead evaluates its contents and captures +the output. It also supports evaluating the contents with scoped arguments where by each property in the object dictionary is added in the +scoped arguments that the block is executed with: +{{/markdown}} + +{{ 'gfm/blocks/14.md' |> githubMarkdown }} + +{{#markdown}} +With this we can dynamically generate some markdown, capture its contents and convert the resulting markdown to html using the `markdown` Filter transformer: +{{/markdown}} + +{{ 'gfm/blocks/15.md' |> githubMarkdown }} + +{{#markdown}} +### markdown + +The `{{#markdown}}` block makes it even easier to embed markdown content directly in web pages which works as you'd expect where content in a +`markdown` block is converted into HTML, e.g: +{{/markdown}} + +
    {{#raw}}
    +{{#markdown}}
    +## TODO List
    +  - Item 1
    +  - Item 2
    +  - Item 3
    +{{/markdown}}
    +{{/raw}}
    +
    + +{{#markdown}} +Which is now the easiest and preferred way to embed Markdown content in content-rich hybrid web pages like +[Razor Rockstars content pages](https://github.com/sharp-apps/rockwind/blob/master/rockstars/dead/cobain/index.html), +or even this [blocks.html WebPage itself](https://github.com/ServiceStack/sharpscript/blob/master/src/wwwroot/docs/blocks.html) which +makes extensive use of markdown. + +As `markdown` block only supports 2 usages its implementation is much simpler than the `capture` block above: +{{/markdown}} + +{{ 'gfm/blocks/16.md' |> githubMarkdown }} + +{{#markdown}} +### keyvalues + +The `{{#keyvalues}}` block lets you define a key value dictionary in free-text which is useful in [Live Documents](/usecases/live-documents) +for capturing a data structure like expenses in free-text, e.g: + +
    +{{#keyvalues monthlyExpenses}}
    +Rent            1000
    +Internet        50
    +Mobile          50
    +Food            400
    +Misc            200
    +{{/keyvalues}}
    +{{ monthlyExpenses |> values |> sum |> to => totalExpenses }}
    +
    + +By default it's delimited by the first space ' ', but if the first key column can contain spaces you can specify to use a different delimiter, e.g: + +
    +{{#keyvalues monthlyRevenues ':'}}
    +Salary:         4000
    +App Royalties:  200
    +{{/keyvalues}}
    +
    + +The [KeyValuesScriptBlock.cs](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Script/Blocks/KeyValuesScriptBlock.cs) +implementation is fairly straight forward where it passes the string body to `ParseKeyValueText()` method with an optional delimiter and +assigns the results to the specified variable name: +{{/markdown}} + +{{ 'gfm/blocks/21.md' |> githubMarkdown }} + +{{#markdown}} +### csv + +Similar to `keyvalues`, you can specify a multi-column inline data set using the `{{#csv}}` block, e.g: + +{{/markdown}} + +{{ "live-template" |> partial({ rows:7, template: "{{#csv cars}} +Tesla,Model S,79990 +Tesla,Model 3,38990 +Tesla,Model X,84990 +{{/csv}} +{{ cars |> map => { Make: it[0], Model: it[1], Cost: it[2] } |> htmlDump }} +Total Cost: {{ cars |> sum => it[2] |> currency }}" }) }} + +{{#markdown}} + +The [CsvScriptBlock.cs](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Script/Blocks/CsvScriptBlock.cs) +implementation is similar to `keyvalues` except passes the trimmed string body to `FromCsv` into a string List and assigns the result to the specified name: +{{/markdown}} + +{{ 'gfm/blocks/22.md' |> githubMarkdown }} + +{{#markdown}} +### partial + +The `{{#partial}}` block lets you create In Memory partials which is useful when working with partial filters like `selectPartial` as +it lets you declare multiple partials within the same page, instead of requiring multiple individual files. See docs on +[Inline partials](/docs/partials#inline-partials) for a Live comparison of using in memory partials. +{{/markdown}} + +{{#markdown}} +### html + +The purpose of the html blocks is to pack a suite of generically useful functionality commonly used when generating html. All html blocks +inherit the same functionality with blocks registered for the most popular HTML elements, currently: + +`script`, `style`, `link`, `meta`, `ul`, `ol`, `li`, `div`, `p`, `form`, `input`, `select`, `option`, `textarea`, `button`, +`table`, `tr`, `td`, `thead`, `tbody`, `tfoot`, `dl`, `dt`, `dd`, `span`, `a`, `img`, `em`, `b`, `i`, `strong`. + +Ultimately they reduce boilerplate, e.g. you can generate a menu list with a single block: + +
    +{{#ul {each:items, id:'menu', class:'nav'} }} 
    +    <li>{{it}}</li> 
    +{{/ul}}
    +
    + +A more advanced example showcasing many of its different features is contained in the example below: +{{/markdown}} + +{{ 'gfm/blocks/18.md' |> githubMarkdown }} + +{{#markdown}} +This example utilizes many of the features in html blocks, namely: + + - `if` - only render the template if truthy + - `each` - render the template for each item in the collection + - `where` - filter the collection + - `it` - change the name of each element `it` binding + - `class` - special property implementing [Vue's special class bindings](https://vuejs.org/v2/guide/class-and-style.html) where an **object literal** + can be used to emit a list of class names for all **truthy** properties, an **array** can be used to display a list of class names or you can instead use + a **string** of class names. + +All other properties like `id` and `selected` are treated like HTML attributes where if the property is a boolean like `selected` it's only displayed +if its true otherwise all other html attribute's names and values are emitted as normal. + +For a better illustration we can implement the same functionality above without using any html blocks: +{{/markdown}} + +{{ 'gfm/blocks/19.md' |> githubMarkdown }} + +

    ServiceStack Blocks

    + + +{{#markdown}} + +ServiceStack's Blocks are registered by default in [#Script Pages](/docs/sharp-pages) that can be registered in a new `ScriptContext` +by adding the `ServiceStackScriptBlocks` plugin: + + var context = new ScriptContext { + Plugins = { + new ServiceStackScriptBlocks(), + } + }.Init(); + +### Mix in NUglify + +You can configure ServiceStack and `#Script` to use Nuglify's Advanced HTML, CSS, JS Minifiers using [mix](https://docs.servicestack.net/mix-tool) with: + + $ mix nuglify + +Which will add [Configure.Nuglify.cs](https://gist.github.com/gistlyn/4bdb79d21f199c22b8a86f032c186e2d) to your **HOST** project. + +To assist with debugging during development, **no minification** is applied when `DebugMode=true`. + +All minifier Blocks supports an additional `` argument to store the captured output of the minifier block into, e.g: + + {{#minifier capturedMinification}} ... {{/minifier}} + {{capturedMinification}} + +That also supports using the `appendTo` modifier to **concatenate** the minified output instead of replacing it, e.g: + + {{#minifier appendTo capturedMinification}} ... {{/minifier}} + {{#minifier appendTo capturedMinification}} ... {{/minifier}} + {{capturedMinification}} + +### minifyjs + +Use the `minifyjs` block to minify inline JavaScript: + +{{/markdown}} + +{{ "live-template" |> partial({ rows:6, template: "{{#minifyjs}} +function add(left, right) { + return left + right; +} +add(1, 2); +{{/minifyjs}}" }) }} + +{{#markdown}} +### minifycss + +Use the `minifycss` block to minify inline CSS: + +{{/markdown}} + +{{ "live-template" |> partial({ rows:5, template: "{{#minifycss}} +body { + background-color: yellow; +} +{{/minifycss}}" }) }} + +{{#markdown}} +### minifyhtml + +Use the `minifyhtml` block to minify HTML: + +{{/markdown}} + +{{ "live-template" |> partial({ rows:8, template: "{{#minifyhtml capturedHtml}} +

    + Title +

    +

    + Content +


    +{{/minifyhtml}} {{capturedHtml}}" }) }} + +{{#markdown}} +### svg + +Use the `svg` block in your `_init.html` Startup Script to [register SVG Images with ServiceStack](https://docs.servicestack.net/svg#registering-svgs-from-_inithtml). + +{{/markdown}} + + +

    Removing Blocks

    + +{{#markdown}} +Like everything else in `#Script`, all built-in Blocks can be removed. To make it easy to remove groups of related blocks you can just remove the +plugin that registered them using the `RemovePlugins()` API, e.g: +{{/markdown}} + +{{ 'gfm/blocks/20.md' |> githubMarkdown }} + +{{ "doc-links" |> partial({ order }) }} diff --git a/src/wwwroot/docs/code-pages.html b/wwwroot/docs/code-pages.html similarity index 77% rename from src/wwwroot/docs/code-pages.html rename to wwwroot/docs/code-pages.html index b7d9c05..0f2d9e9 100644 --- a/src/wwwroot/docs/code-pages.html +++ b/wwwroot/docs/code-pages.html @@ -1,6 +1,6 @@

    @@ -14,7 +14,7 @@

    Creating Code Pages

    - A Code Page is a class that inherits TemplateCodePage and is annotated with a [Page(virtualPath)] + A Code Page is a class that inherits SharpCodePage and is annotated with a [Page(virtualPath)] attribute which is used to resolve the page in the same way .html pages are resolved by their virtualPath. Code Pages take precedence over .html pages so you can start with an .html page and when you need more power and expressiveness, create a Code Page at the same virtualPath as the page you want to replace and it will get used instead. @@ -39,9 +39,9 @@

    render Method
    A complete CodePage implementation using all these features is below:

    -

    ProductsPage.cs

    +

    ProductsPage.cs

    -{{ 'gfm/code-pages/01.md' | githubMarkdown }} +{{ 'gfm/code-pages/01.md' |> githubMarkdown }}

    Where with just this implementation, it can now be called by navigating to its virtualPath: @@ -50,9 +50,9 @@

    /products

    - You typically wont need to explicitly register Code Pages since the TemplatePagesFeature populates its + You typically wont need to explicitly register Code Pages since the SharpPagesFeature populates its ScanAssemblies with the AppHost's Service assemblies which will automatically find and register any - TemplateCodePage's that are in the same Assembly as ServiceStack Services and just like Services, Code Pages + SharpCodePage's that are in the same Assembly as ServiceStack Services and just like Services, Code Pages are autowired and resolved from the IOC transiently, so they can also be injected with any IOC registered dependencies by declaring them as public properties.

    @@ -65,28 +65,28 @@

    MVC Code Pages

    CodePage by resolving it with its virtualPath:

    -

    ProductServices.cs

    +

    ProductServices.cs

    -{{ 'gfm/code-pages/02.md' | githubMarkdown }} +{{ 'gfm/code-pages/02.md' |> githubMarkdown }}

    Which can be called with /products/view to display exactly the same content as calling ProductsPage directly, but instead uses - the Routing and the argument populated by the ServiceStack Service, instead of the argument populated in the TemplatePagesFeature Arguments. + the Routing and the argument populated by the ServiceStack Service, instead of the argument populated in the SharpPagesFeature Arguments.

    Using the Request.GetCodePage() extension method is the recommended way to resolve Code Pages as it will automatically inject the current IRequest on Code Pages that implement IRequiresRequest like - ServiceStackCodePage's. It's the same as resolving the Code Page from the ITemplatePages dependency + ServiceStackCodePage's. It's the same as resolving the Code Page from the ISharpPages dependency and injecting the IRequest manually:

    -{{ 'gfm/code-pages/03.md' | githubMarkdown }} +{{ 'gfm/code-pages/03.md' |> githubMarkdown }}

    Code Pages as Partials

    - Another area Code Pages are useful are as partials where they're able to escape the normal sandbox of Template Pages and + Another area Code Pages are useful are as partials where they're able to escape the normal sandbox of #Script Pages and and use C# to access the existing functionality available within your Web App.

    @@ -102,18 +102,18 @@
    Partial Arguments
    generated when embed the partial:

    -
    {{ "examples/nav-links.txt" | includeFile }}
    +
    {{ "examples/nav-links.txt" |> includeFile }}

    Which will generate the following links of progressive advanced features, with the link of the current page highlighted:

    -{{ 'navLinks' | partial( +{{ 'navLinks' |> partial( { links: { '/docs/model-view-controller': 'MVC', - '/docs/view-engine': 'View Engine', - '/docs/code-pages': 'Code Pages' + '/docs/sharp-pages': '#Script Pages', + '/docs/code-pages': 'Sharp Code Pages' } }) }} @@ -123,27 +123,27 @@
    Partial Arguments
    the links argument created when the partial was called:

    -

    CodePagePartials.cs

    +

    CodePagePartials.cs

    -{{ 'gfm/code-pages/04.md' | githubMarkdown }} +{{ 'gfm/code-pages/04.md' |> githubMarkdown }}

    The customerCard partial shows another example where Code Pages are useful where they're able to access any IOC dependency and existing Web App functionality, which can be called with:

    -
    {{ "examples/customer-card.txt" | includeFile }}
    +
    {{ "examples/customer-card.txt" |> includeFile }}

    To render a nice Customer Card snapshot:

    -{{ 'customerCard' | partial({ customerId: "ALFKI" }) }} +{{ 'customerCard' |> partial({ customerId: "ALFKI" }) }}

    ServiceStack Code Pages

    - ServiceStackCodePage is a convenience base class that inherits TemplateCodePage and enhances it with + ServiceStackCodePage is a convenience base class that inherits SharpCodePage and enhances it with the same functionality that's available in ServiceStack's Service base class, e.g: access to the current Request context, Gateway, Database, Redis, CacheClient, Users Session, AuthRepository, etc.

    @@ -154,7 +154,7 @@

    ServiceStack Code Pages

    the PathInfo argument and CustomerCardPartial has direct access to the configured database:

    -{{ 'gfm/code-pages/05.md' | githubMarkdown }} +{{ 'gfm/code-pages/05.md' |> githubMarkdown }}

    Since they have access to the same functionality ServiceStack Services do, ServiceStackCodePage's can provide @@ -162,4 +162,4 @@

    ServiceStack Code Pages

    -{{ "doc-links" | partial({ order }) }} +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/db-scripts.html b/wwwroot/docs/db-scripts.html new file mode 100644 index 0000000..a59b1d2 --- /dev/null +++ b/wwwroot/docs/db-scripts.html @@ -0,0 +1,308 @@ + + +

    + OrmLite's database methods gives your scripts database connectivity to the most popular RDBMS's. + To enable install the + OrmLite NuGet package for your RDBMS + and register it in your IOC: +

    + +{{ 'gfm/db-scripts/01.md' |> githubMarkdown }} + +

    + Then to enable register either + DbScriptsAsync + if you're using either SQL Server, PostgreSQL or MySql, otherwise register the sync + DbScripts + to avoid the pseudo async overhead of wrapping synchronous results in Task results. +

    + +{{ 'gfm/db-scripts/02.md' |> githubMarkdown }} + +

    + Now your templates will have access to the available DB Scripts + where you can use the db* methods + to execute sql queries: +

    + +
      +
    • dbSelect - returns multiple rows
    • +
    • dbSingle - returns a single row
    • +
    • dbScalar - returns a single field value
    • +
    + +

    + The sql* filters are used to enable + cross-platform RDBMS support where it encapsulates the differences behind each RDBMS and returns the appropriate SQL + for the RDBMS that's registered. +

    + +

    Opening different connections

    + +

    + By default the DbScripts will use the registered IDbConnectionFactory in the IOC or when + Multi tenancy is configured it will use the connection configured for that request. +

    + +

    + You can also specify to use a different DB connection using the namedConnection and connectionString arguments: +

    + +{{ 'gfm/db-scripts/03.md' |> githubMarkdown }} + +

    DB Filter Examples

    + +

    + For an interactive example that lets you explore DB Filters checkout the Adhoc Querying use-case. +

    + +

    + To explore a complete data-driven Web App built using Templates and DB Filters, checkout the Rockwind Website below + which uses DB Filters to implement both its Web Pages and Sharp APIs. + The URL + The Source code link on the right shows the source code used to generate the Web Page or API Response: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Rockwind Website
    DescriptionURLSource Code
    Customers/northwind/customers/northwind/customers.html
    Employees/northwind/employees/northwind/employees.html
    Products/northwind/products/northwind/products.html
    Categories/northwind/categories/northwind/categories.html
    Suppliers/northwind/suppliers/northwind/suppliers.html
    Shipppers/northwind/shippers/northwind/shippers.html
    Page Queries
    Customers in Germany/northwind/customers?country=Germany/northwind/customers.html
    Customers in London/northwind/customers?city=London
    Alfreds Futterkiste Details/northwind/customer?id=ALFKI/northwind/customer.html
    Order #10643/northwind/order?id=10643/northwind/order.html
    Employee Nancy Davolio Details/northwind/employee?id=1/northwind/employee.html
    Chai Product Details/northwind/products?id=1/northwind/products.html
    Beverage Products/northwind/products?category=Beverages
    Products from Bigfoot Breweries/northwind/products?supplier=Bigfoot+Breweries
    Products containing Tofu/northwind/products?nameContains=Tofu
    API Queries
    All Customers + + + + +
    Accept HTTP Header also supported
    +
    /api/customers.html
    Alfreds Futterkiste Details + + + +
    As List + + + +
    Customers in Germany + + + +
    Customers in London + + + +
    All Products + + + + /api/products.html
    Chai Product Details + + + +
    As List + + + +
    Beverage Products + + + +
    Products from Bigfoot Breweries + + + +
    Products containing Tofu + + + +
    + +

    Run Rockwind against your Local RDBMS

    + +

    + You can run the Rockwind Website + against either an SQLite, SQL Server or MySql database by just changing which app.settings the App is run with, e.g: +

    + +
    x web.sqlserver.settings
    + +
    Populate local RDBMS with Northwind database
    + +

    + If you want to run this Sharp App against your own Database run the + northwind-data + project against your database, e.g: +

    + +
    dotnet run sqlserver "Server=localhost;Database=northwind;User Id=test;Password=test;"
    + +

    + As Rockwind is a Web Template Sharp App it doesn't need any compilation so after + running the Rockwind Sharp App + you can modify the source code and see changes in real-time thanks to its built-in + Hot Reloading support. +

    + +

    PostgreSQL Support

    + +

    + Due to PostgreSQL's automatic conversion of unquoted tables and fields to lower case and MySql not supporting double quotes + for quoting symbols, it's not feasible to develop the same website that runs in both MySql and PostgreSQL unless you + use sqlQuote to quote every column or table or are willing to use lowercase or snake_case for all table and column names. + As a result we've developed an alternate version of Rockwind website called Rockwind VFS + which quotes every Table and Column in double quotes so PostgreSQL preserves casing. The + Rockwind VFS Project + can be run against either PostgreSQL, SQL Server or SQLite by changing the configuration it's run with, e.g: +

    + +
    x web.postgres.settings
    + +

    + See the Scripts API Reference for the + full list of DB filters available. +

    + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/default-scripts.html b/wwwroot/docs/default-scripts.html new file mode 100644 index 0000000..792ff5e --- /dev/null +++ b/wwwroot/docs/default-scripts.html @@ -0,0 +1,568 @@ + + +

    + The default scripts are a comprehensive suite of safe scripts useful within a View Engine or Text Template Generation environment. + The source code for all default scripts are defined in + DefaultScripts. +

    + +

    + For examples of querying methods checkout the Live LINQ Examples, we'll show examples for other useful methods below: +

    + +

    Methods as bindings

    + +

    + Script Methods with no arguments can be used in-place of an argument binding, now and utcNow are some examples of this: +

    + +{{ 'live-template' |> partial({ rows: 4, template: "now: {{ now |> dateFormat }} +UTC now: {{ utcNow |> dateFormat('o') }} +UTC DateTimeOffset {{ utcNowOffset }}, +nguid: {{ nguid }}" }) }} + +

    + Many methods have an implicit default format which can be overridden in the ScriptContext's Args, e.g + if no Date Format is provided it uses the default format yyyy-MM-dd which can be overridden with: +

    + +{{ 'gfm/default-scripts/01.md' |> githubMarkdown }} + +

    HTML Encoding

    + +

    + All values of #Script Expressions in .html pages are HTML-encoded by default, you can bypass the HTML encoding + to emit raw HTML by adding the raw filter as the last filter in an expression: +

    + +{{ 'live-template' |> partial({ template: "With encoding: {{ 'hi' }} +Without: {{ 'hi' |> raw }}" }) }} + +

    Arithmetic

    + +

    + #Script supports the same arithmetic expressions as JavaScript: +

    + +{{ 'live-template' |> partial({ rows: 7, template: "1 + 1 = {{ 1 + 1 }} +2 x 2 = {{ 2 * 2 }} +3 - 3 = {{ 3 - 3 }} +4 / 4 = {{ 4 / 4 }} +3 % 2 = {{ 3 % 2 }} +1 - 2 + 3 * 4 / 5 = {{ 1 - 2 + 3 * 4 / 5 }} +Bitwise = {{ 3 & 1 }}, {{ (3 | 1) }}, {{ 3 ^ 1 }}, {{ 3 << 1 }}, {{ 3 >> 1 }}, {{ ~1 }}" }) }} + +
    + Behavior is as you would expect in JavaScript except for Bitwise OR | needs to be in (parens) to avoid it being treated as a + template expression separator and no assignment operations are supported, i.e. =, ++, --, |=, etc. The = is treated + as an equality operator == so it lets you use SQL-like queries if preferred: a = 1 and b = 2 or c = 3 +
    + +

    + For those who prefer wordier descriptions, you can use the equivalent built-in methods: +

    + +{{ 'live-template' |> partial({ rows: 5, template: "1 + 1 = {{ 1 |> add(1) }} or {{ add(1,1) }} or {{ 1 |> incr }} or {{ 1 |> incrBy(1) }} +2 x 2 = {{ 2 |> mul(2) }} or {{ multiply(2,2) }} +3 - 3 = {{ 3 |> sub(3) }} or {{ subtract(3,3) }} or {{ 3 |> decrBy(3) }} +4 / 4 = {{ 4 |> div(4) }} or {{ divide(4,4) }} +3 % 2 = {{ 3 | mod(2) }} or {{ mod(3,2) }}" }) }} + +

    + It should be noted when porting C# code that as script methods don't follow the implied + Order of Operations used in math calculations + where an expression like 1 - 2 + 3 * 4 / 5 is executed in the implied order of (1 - 2) + ((3 * 4) / 5). + To achieve the same result you'd need to execute them in their implied grouping explicitly: +

    + +{{ 'live-template' |> partial({ rows:4, template: "1 - 2 + 3 * 4 / 5 = {{ 1 - 2 + 3 * 4 / 5 }} +1 - 2 + 3 * 4 / 5 = {{ add(sub(1,2), div(mul(3,4), 5)) }} +1 - 2 + 3 * 4 / 5 = {{ sub(1,2).add(mul(3,4).div(5)) }} +((1 - 2 + 3) * 4) / 5 = {{ 1 |> subtract(2) |> add(3) |> multiply(4) |> divide(5) }}" }) }} + +

    Math

    + +

    + Most Math APIs are available including pi and e constants: +

    + +{{ 'live-template' |> partial({ template: "Circumference = {{ 2 * pi * 10 |> round(5) }} +√ log10 10000 = {{ 10000 | log10 | sqrt }} +Powers of 2 = {{ 10 |> times |> map => `${(it + 1).pow(2)}` |> join }}" }) }} + +

    Date Functions

    + +{{ 'live-template' |> partial({ rows: 10, template: "{{ now |> addTicks(1) }} +{{ now |> addMilliseconds(1) }} +{{ now |> addSeconds(1) }} +{{ now |> addMinutes(1) }} +{{ now |> addHours(1) }} +{{ now |> addDays(1) }} +{{ now |> addMonths(1) }} +{{ now |> addYears(1) }} +{{ '2001-01-01' |> toDateTime }}, {{ date(2001,1,1) }}, {{ date(2001,1,1,1,1,1) }} +{{ time(1,2,3,4) }}, {{ '02:03:04' |> toTimeSpan }}, {{ time(2,3,4) }}" }) }} + +

    Formatting

    + +

    + Use format methods to customize how values are formatted: +

    + +{{ 'live-template' |> partial({ rows: 4, template: ′USD {{12.34 |> currency}} GBP {{12.34 |> currency('en-GB')}} EUR {{12.34 |> currency('FR')}} +Number: {{ 123.456 |> format('0.##') }} Currency: {{ 123.456 |> format('C') }} +Date: {{ now |> dateFormat('dddd, MMMM d, yyyy') }} default: {{ now |> dateFormat }} +Time: {{ now.TimeOfDay |> timeFormat('h\\:mm') }} default: {{ now.TimeOfDay |> timeFormat }}′ }) }} + +

    + When no format provided, the default ScriptConstant's formats are used: +

    + +
      +
    • DefaultDateFormat used by: dateFormat
    • +
    • DefaultDateTimeFormat used by: dateTimeFormat
    • +
    • DefaultTimeFormat used by: timeFormat
    • +
    • DefaultCulture used by: currency
    • +
    + +

    String functions

    + +

    + As can be expected from a templating library there's a comprehensive set of string methods available: +

    + +{{ 'live-template' |> partial({ rows: 13, template: "{{ 'upper' |> upper }} and {{ 'LOWER' | lower }} +{{ 'SubString'|substring(0,3) }} {{'SubString'|substring(3)}} {{'IsSafe'|substring(2,100)}} +{{ 'SubStrEllipsis' |> substringWithEllipsis(9) }} {{'SubStr' |> substringWithEllipsis(3,3)}} +{{ 'left:part' |> leftPart(':') }} + {{ 'part:right' |> rightPart(':') }} +{{ 'last:left:part' |> lastLeftPart(':') }} + {{ 'last:right:part' |> lastRightPart(':') }} +{{ 'split.on.first' |> splitOnFirst('.')|>join}} + {{'split.on.last' |> splitOnLast('.')|>join}} +{{ 'split.these.words' |> split('.') |> get(1)}}, {{'replace this' |> replace('this', 'that')}} +{{ 'index.of' |> indexOf('.') }} + {{ 'last.index.of' |> lastIndexOf('.') }} +{{ 'start' |> appendLine |> append('end') }} +{{ 'in' + ' the ' + 'middle' }} +{{ 'in the {0} of the {1} I go {2}' |> fmt('middle','night','walking') }} +{{ 'in the {0} of the {1} I go {2} in my {3}' |> fmt(['middle','night','walking','sleep']) }} +{{ 'in the ' |> appendFmt('{0} of the {1} I go {2}', 'middle','night','walking') }}" }) }} + +

    Text Style

    + +{{ 'live-template' |> partial({ template: `{{ 'aVarName' | humanize }} and {{ 'AVarName' | splitCase }} +{{ 'wAr aNd pEaCe' | titleCase }} +{{ 'pascalCase' | pascalCase }} and {{ 'CamelCase' | camelCase }}` }) }} + +

    Trimming and Padding

    + +{{ 'live-template' |> partial({ template: "'{{ ' start ' |> trimStart }}', '{{ ' end ' |> trimEnd }}', '{{ ' both ' |> trim }}' +'{{ 'left' |> padLeft(10) }}', '{{ 'right' |> padRight(10) }}' +'{{ 'left' |> padLeft(10,'_') }}', '{{ 'right' |> padRight(10,'_') }}'" }) }} + +

    URL handling

    + +{{ 'live-template' |> partial({ rows: 8, template: "{{ 'http://example.org' |> to => baseUrl }} +{{ baseUrl |> addPath('path') }} +{{ baseUrl |> addPaths(['path1', 'path2', 'path3']) }} +{{ baseUrl |> addQueryString({ a: 1, b: 2 }) }} +{{ baseUrl |> addQueryString({ a: 1, b: 2 }) |> setQueryString({ a: 3 }) }} +{{ baseUrl |> addHashParams({ c: 3, d: 4 }) }} +{{ baseUrl |> addHashParams({ c: 3, d: 4 }) |> setHashParams({ c: 5 }) }} +{{ baseUrl |> addPath('path') |> addQueryString({ a: 1 }) |> addHashParams({ b: 2 }) }}" }) }} + +

    Repeating, Ranges and Generation

    + +{{ 'live-template' |> partial({ rows: 4, template: "{{ 3 |> repeating('ABC ') }} or {{ 'ABC ' |> repeat(3) }} +{{ 3 |> itemsOf(2) |> join }} or {{ 3 |> itemsOf(2) |> sum }} +{{ 5 |> times |> select: {it}, }} +{{ range(5) |> select: {it}, }} {{ range(5,5) |> select: {it}, }}" }) }} + +

    Spacing

    + +

    + The following methods can be used to easily control the precise spacing in templates: +

    + +{{ 'live-template' |> partial({ rows: 6, template: "{{ space }}1 space +{{ 3 | spaces }}3 spaces +{{ indent }}1 indent +{{ 3 |> indents }}3 indents +{{ newLine }}1 new line above +{{ 3 |> newLines }}3 new lines above" }) }} + +
    + The default spacing used for indents \t can be overridden with the Args[DefaultIndent]. +
    + +

    Conditional Tests

    + +

    + These methods allow you to test for different conditions: +

    + +{{ 'live-template' |> partial({ rows: 17, template: "ternary: {{ true ? 1 : 0 }}, {{ 1 + 1 > subtract(2, 1) ? 'YES' : 'NO' }} +or: {{ true || false }}, {{ false || false }}, {{ true | OR(true) }} +and: {{ true && false }}, {{ false and false }}, {{ true | AND(true) }} +exists: {{ null | exists }}, {{ 1 | exists }}, {{ unknownArg | exists }} +equals: {{ 1 == 1 }}, {{1 |> equals(1)}}, {{'A' |> equals('A')}}, {{1 |> eq(1)}} +notEquals: {{ 2 != 1 }}, {{2 |> notEquals(1)}}, {{'A' |> notEquals('A')}} or {{1 |> not(1)}} +greaterThan: {{ 1 > 1 }}, {{ 1 |> greaterThan(1) }} or {{ 1 |> gt(1) }} +greaterEqual: {{ 1 >= 1 }}, {{ 1 |> greaterThanEqual(1) }} or {{ 1 |> gte(1) }} +lessThan: {{ 1 < 1}}, {{ 1 |> lessThan(1) }} or {{ 1 |> lt(1) }} +lessEqual: {{ 1 <= 1}}, {{ 1 |> lessThanEqual(1) }} or {{ 2 |> lte(1) }} +isNull: {{ null == null }}, {{ null |> isNull }}, {{ 1 |> isNull }}, {{ 1 == null }} +isNotNull: {{ null != null }} {{ null |> isNotNull }}, {{ 1 |> isNotNull }}, {{ 1 != null }} +equivalentTo: {{ [1,2] |> equivalentTo([1,2]) }}, {{ {a:1, b:2} |> equivalentTo({a:1, b:2}) }} +contains: {{ [1,2] |> contains(1) }}, {{ [1,2] |> contains(3) }}, {{ 'ABC' |> contains('A') }} +Even/Odd: {{ 1 |> isEven }}, {{ 1 % 2 == 0 }}, {{ 1 |> isOdd }}, {{ 1 % 2 == 1 }} +counts: {{ [1,2] | length }}, {{ [1,2] |> hasMinCount(1) }}, {{ [1,2] |> hasMaxCount(1) }} +starts/ends: {{ 'startsWith' |> startsWith('start') }}, {{ 'endsWith' |> endsWith('end') }}" }) }} + +

    Object Type Tests

    + +

    + Methods to check the type of objects: +

    + +{{ 'live-template' |> partial({ rows: 12, template: "Any Number: {{ 1 |> isInteger }}, {{ 1 |> toLong |> isInteger }}, {{ 1.1 |> isNumber }} +Real Numbers: {{ 1.1 |> isDouble }}, {{1.1 |toFloat|> isFloat}}, {{1.1 |toDecimal|> isDecimal}} +ints: {{ 1 |> isInt }}, {{ 1 |> isLong }}, {{ 1 |> toLong |> isLong }} +strings: {{ 'a' |> isString }}, {{ 'a' |> isChar }}, {{ 'a' |> toChar |> isChar }} +bool: {{ false |> isBool }}, +bytes: {{ 1 |> toByte |> isByte }}, {{ 'a' |> toUtf8Bytes |> isBytes }} +chars: {{ 'abc' |> toChars |> isChars }}, {{ ['a','b','c'] |> toChars |> isChars }} +enumerable: {{ [1] |> isEnumerable }}, {{ {a:1} |> isEnumerable }}, {{ 'abc' |> isEnumerable }} +list: {{ [1] |> isList }}, {{ {a:1} |> isList }}, {{ 'abc' |> isList }} +dictionary: {{ {a:1} |> isDictionary }}, {{ [1] |> isDictionary }}, {{ 'abc' |> isDictionary }} +object dict: {{ {a:1} |> isObjectDictionary }}, {{ {'a':'b'} |> isObjectDictionary }} +value/ref type: {{ 1 |> isValueType }}, {{ [] |> isValueType }}, {{ [] |> isClass }}" }) }} + +

    Object Conversions

    + +

    + Conversions and transformations between different types: +

    + +{{ 'live-template' |> partial({ rows: 5, template: `toInt: {{ '1' |> typeName }}, {{ '1' |> toInt |> typeName }}, {{ 1 |> toDouble |> typeName }} +toChar: {{',' |> typeName}}, {{',' |> toChar |> typeName}}, {{'true' |> toBool |> typeName}} +{{ {a:1,b:2,c:'',d:null} |> to => o }} +toKeys: {{ o |> toKeys |> join }}, toValues: {{ o |> toValues |> join }} +without: {{o |> withoutNullValues |> toKeys|>join}} / {{o |>withoutEmptyValues |>toKeys |>join}}` }) }} + +

    Conditional Display

    + +

    + These methods can be used in combination with Conditional Test methods above to control what text is displayed: +

    + +{{ 'live-template' |> partial({ rows: 11, template: "if: {{ 'Y' |> if(true) }}, {{ 'Y' |> if(false) |> otherwise('N') }}, {{ 'Y' |> if(1) }} +when: {{ 'Y' |> when(true) }}, {{ 'Y' |> when(false) |> otherwise('N') }}, {{ 'Y' |> when(1) }} +if!: {{ 'Y' |> if(!true) |> otherwise('N') }}, {{ 'Y' |> if(!false) }}, {{ 'Y' |> if(!1) }} +ifNot: {{ 'Y' |> ifNot(true) }}, {{ 'Y' |> ifNot(false) }}, {{ 'Y' |> ifNot(1) }} +unless: {{ 'Y' |> unless(true) }}, {{ 'Y' |> unless(false) }}, {{ 'Y' |> unless(1) }} +otherwise: {{ null |> otherwise('Y') }} or {{ 'even' |> if(isEven(1)) |> otherwise('odd') }} +iif: {{ isEven(1) |> iif('even', 'odd') }} or {{ iif(isEven(1), 'even', 'odd') }} +ifFalsy: {{ 'F' |> ifFalsy(false) }}, {{ 'F' |> ifFalsy(0) }}, {{ 'F' |> ifFalsy(null) }} +falsy: {{ false | falsy('F') }}, {{ 0 | falsy('F') }}, {{ null | falsy(F) }} +ifTruthy: {{ 'T' |> ifTruthy(true) }}, {{ 'T' |> ifTruthy(1) }}, {{ 'T' |> ifTruthy(null) }} +truthy: {{ true | truthy('T') }}, {{ 1 | truthy('T') }}, {{ null | truthy('T') }}" }) }} + +

    Content Handling

    + +{{ 'live-template' |> partial({ rows: 7, template: `default: {{ title |> default('A Title') }} +{{ title != null ? title : 'A Title' }} +{{ 'The Title' |> to => title }}{{ title |> default('A Title') }} : {{ 1 |> ifExists(title) }} +{{ noArg }} : {{ noArg |> ifExists }} : {{ 1 |> ifNotExists(noArg) }} : {{ 1 |> ifNo(noArg) }} +{{ 'empty' |> ifEmpty('') }} : {{ 'empty' |> ifEmpty([]) }} : {{ 'empty' |> ifEmpty([1]) }} +{{ [1,2,3] |> to => nums }}{{ nums |> join |> to => list }} +{{ "nums {0}" |> fmt(list) |> ifNotEmpty(nums) }}{{ "nums {0}" |> fmt(list) |> ifNotEmpty([]) }}` }) }} + +

    Control Execution

    + +

    + The end* methods short-circuits the execution of a method and discard any results. + They're useful to use in combination with the + use* methods + which discards the old value and creates a new value + to be used from that point on in the expression. + show is an alias + for use that reads better when used at the end of an expression. +

    + +{{ 'live-template' |> partial({ rows: 12, template: `always {{ 1 |> end |> default('unreachable') }} +null {{ 1 |> endIfNull }}/{{ null |> endIfNull |> default('N/A') }} +empty {{ '' |> endIfEmpty |> default('N/A') }}/{{ [] |> endIfEmpty |> default('N/A') }} +if {{ true |> ifEnd |> use(1) }}/{{ false |> ifEnd |> use(1) }}:{{ endIf(true) |> show: 1 }} +do {{ true |> ifDo |> select: 1 }}/{{ false |> ifDo |> select: 1 }} +any {{ 5 |> times |> endIfAny('it = 4') |> join }}/{{5 |> times |> endIfAny('it = 5') |> join}} +all {{ 5 |> times |> endIfAll('lt(it,4)') |>join}}/{{5 |> times |> endIfAll('lt(it,5)') |>join}} +where {{ 1 |> endWhere: isString(it) }}/{{ 'a' |> endWhere: isString(it) }} +useFmt {{ arg |> endIfExists |> useFmt('{0} + {1}', 1, 2) |> to => arg }}{{ arg }} +useFmt {{ arg |> endIfExists |> useFmt('{0} + {1}', 3, 4) |> to => arg }}{{ arg }} +useFormat {{ arg2 |> endIfExists |> useFormat('value', 'key={0}') |> to => arg2 }}{{ arg2 }} +{{ noArg |> end }} : {{ 1 |> end }} : {{ 1 |> incr |> end }}` }) }} + +
    + You can also use end to discard return values of methods with side-effects, block methods, partials, etc. + It also provides an easy way to comment out any expression by prefixing it at the start, e.g: {{#raw}}{{ end | unreachable }}{{/raw}} +
    + +

    + The ifUse and useIf methods are the inverse of the end* methods where they continue + execution with a new value if the condition is true: +

    + +{{ 'live-template' |> partial({ rows: 2, template: `ifUse: {{ true |> ifUse(1) }} / {{ false |> ifUse(1) |> default('unreachable') }} +useIf: {{ 1 |> useIf(true) }} / {{ 1 |> useIf(false) |> default('unreachable') }}` }) }} + +

    + There's also an + only* method + for each + end* method + above with the inverse behavior, e.g: +

    + +{{ 'live-template' |> partial({ rows: 3, template: `endIfEmpty: {{ a |> endIfEmpty |> show: a is not empty }} +onlyIfEmpty: {{ a |> onlyIfEmpty |> show: a is empty }}` }) }} + +

    Assignment

    + +

    + You can create temporary arguments within a script scope or modify existing arguments with: +

    + +{{ 'live-template' |> partial({ rows: 6, template: "{{ [1,2,3,4,5] |> to => numbers }} +{{ numbers |> join }} +{{ numbers |> do => numbers[index] = it * it }} +{{ numbers |> join }} +{{ 5 |> times |> do => assign(`num${index}`, it) }} +{{ num4 }}" }) }} + +

    Let Bindings and Scope Vars

    + +

    + Let bindings allow you to create scoped argument bindings from individual string expressions: +

    + +{{ 'live-template' |> partial({ rows: 4, template: ′{{ [{name:'Alice',score:50},{name:'Bob',score:40}] |> to =>scoreRecords }} +{{ scoreRecords + |> let => { name:it.name, score:it.score, personNum:index + 1 } + |> select: {personNum}. {name} = {score}\n }}′ }) }} + +

    + scopeVars lets you create bindings from a Dictionary, List of KeyValuePair's or List of Dictionaries using the key + as the name of the argument binding and the value as its value: +

    + +{{ 'live-template' |> partial({ template: "{{ [{name:'Alice',score:50},{name:'Bob',score:40}] |> to =>scoreRecords }} +{{ scoreRecords | scopeVars |> select: {index + 1}. {name} = {score}\n }}" }) }} + +

    Querying Objects

    + +{{ 'live-template' |> partial({ rows: 5, template: `{{ [10,20,30,40,50] |> to => numbers }} +{{ { a:1, b:2, c:3 } |> to => letters }} +Number at [3]: {{ numbers[3] }}, {{ numbers |> get(3) }} +Value of 'c': {{ letters['c'] }}, {{ letters.c }}, {{ letters |> get('c') }} +Property Value: {{ 'A String'.Length }}, {{ 'A String'['Len' + 'gth'] }}` }) }} + +

    Member Expressions

    + +{{ 'live-template' |> partial({ rows: 4, template: `{{ [now] |> to => dates }} +{{ round(dates[0].TimeOfDay.TotalHours, 3) }} +{{ dates |> get(0) |> select: { it.TimeOfDay.TotalHours |> round(3) } }} +{{ [now.TimeOfDay][0].TotalHours |> round(3) }}` }) }} + +

    + JavaScript member expressions are supported except for calling methods on instances as only registered methods can be invoked. +

    + +

    Mapping and Conversions

    + +

    + Use map when you want to transform each item into a different value: +

    + +{{ 'live-template' |> partial({ rows: 4, template: "{{ ['zero','one','two','three','four','five','six','seven','eight','nine'] |> to =>digits }} +{{ range(3) + |> map => it + 5 + |> map => digits[it] |> join(`\\n`) }}" }) }} + +

    + It's also useful for transforming raw data sources into more manageable ones: +

    + +{{ 'live-template' |> partial({ rows: 6, template: "{{ [[1,-2],[3,-4],[5,-6]] |> to =>coords }} +{{ coords + |> map => { x: it[0], y: it[1] } + |> scopeVars + |> map => `${index + 1}. (${x}, ${y})\\n` |> join('') }}" }) }} + +

    + Whilst these methods let you perform some other popular conversions: +

    + +{{ 'live-template' |> partial({ rows: 8, template: "{{ 100 |> toString |> select: {it.Length} }} +{{ { x:1, y:2 } |> toList |> map => `${it.Key} = ${it.Value}` |> join(', ') }} +{{ range(5) |> toArray |> to => numbers }} +{{ numbers.Length |> times |> do => numbers[index] = -numbers[index] }} +{{ numbers |> join }} +Bob's score: {{ [{name:'Alice',score:50},{name:'Bob',score:40}] + |> toDictionary => it.name |> map => it.Bob.score }}" }) }} + +

    + Use parseKeyValueText to convert a key/value string into a dictionary, you can then use + values and keys to extract the Dictionaries keys or values collection: +

    + +{{ 'live-template' |> partial({ rows: 8, template: `{{' +Rent: 1000 +Internet: 50 +Mobile: 50 +Food: 400 +Misc: 200 +'|> trim | parseKeyValueText(':') |> to => expenses }} +Expenses: {{ expenses |> values |> sum |> currency }}` }) }} + +

    Serialization

    + +

    + Use the json method when you want to make your C# objects available to the client JavaScript page: +

    + +{{ 'live-template' |> partial({ template: `{{ [{x:1,y:2},{x:3,y:4}] |> to =>model }} +var coords = {{ model |> json }};` }) }} + +

    Embedding in JavaScript

    + +

    + You can use the methods below to embed data on the client in JavaScript. If it's a valid JS Object or JSON it can + be embedded as a native data JS structure without quotes, otherwise use jsString to capture it in a + JavaScript string variable: +

    + +{{ 'live-template' |> partial({ rows: 9, template: ′{{ '[ + {"name":"Mc Donald\'s"} +]' |> to =>json }} +var obj = {{ json }}; +var str = '{{ json | jsString }}'; +var str = {{ json | jsQuotedString }}; +var escapeSingle = '{{ "single' quote's" |> escapeSingleQuotes }}'; +var escapeDouble = "{{ 'double" quote"s' |> escapeDoubleQuotes }}" +var escapeLines = '{{ "new" |> appendLine |> append("line") |> escapeNewLines }}';′ }) }} + +

    + The JSV Format is great when you want a human-friendly output + of an object graph, you can also use dump if you want the JSV output indented: +

    + +{{ 'live-template' |> partial({ template: `{{ [{x:1,y:2},{x:3,y:4}] |> to => model }} +{{ model |> jsv }} +{{ model |> dump }}` }) }} + +

    + If needed, the csv and xml serialization formats are also available: +

    + +{{ 'live-template' |> partial({ template: `{{ [{Name:'Alice',Score:50},{Name:'Bob',Score:40}] |> to =>scoreRecords }} +{{ scoreRecords |> csv }}` }) }} + +

    Eval

    + +

    + By default the evalTemplate method renders Templates in a new ScriptContext which can be customized + to utilize any additional plugins, methods and blocks available in the configured + SharpPagesFeature, or for full access you can use {use:{context:true}} + to evaluate any Template content under the same context that evalTemplate is run on. +

    + +

    + E.g. you can evaluate dynamic Template Syntax which makes use of the MarkdownScriptPlugin plugin with: +

    + +
    {{#raw}}{{ content |> evalTemplate({use:{plugins:'MarkdownScriptPlugin'}}) |> raw }}{{/raw}}
    + +

    + This method is used by the /preview.html API Page to + create an API that enables live previews. +

    + +

    Iterating

    + +

    + The map, select method and its inverse selectEach are some ways to generate output + for each item in a collection: +

    + +{{ 'live-template' |> partial({ rows: 7, template: "{{ [{name:'Alice',score:50},{name:'Bob',score:40}] |> to => scoreRecords }} +map +
      {{ scoreRecords |> map => `
    1. ${it.name} = ${it.score}
    2. ` |> join('') |> raw }}
    +select +
      {{ scoreRecords |> select:
    1. {it.name} = {it.score}
    2. }}
    +selectEach +
      {{ '
    1. {{ it.name }} = {{ it.score }}
    2. ' |> selectEach(scoreRecords) }}
    " }) }} + +

    + Although a lot of times it's easier to use the each Script Block to iterate over items: +

    + +{{ 'live-template' |> partial({ rows: 4, template: "{{ [{name:'Alice',score:50},{name:'Bob',score:40}] |> to => scoreRecords }} +{{#each scoreRecords}} + {{index+1}}. {{it.name}} = {{it.score}} +{{/each}}" }) }} + +

    If you instead need to iterate over a collection to perform side-effects without generating output you can use forEach:

    + +{{ 'live-template' |> partial({ rows: 4, template: "{{ [{name:'Alice',score:50},{name:'Bob',score:40}] |> to => scoreRecords }} +{{ var scores = [] }} +{{ scoreRecords.forEach((record, index) => scores.push(record.score)) }} +{{ scores |> join}}" }) }} + + +

    + Use step if you want to iterate using a custom step function: +

    + +{{ 'live-template' |> partial({ rows: 1, template: `{{ range(20) | step({ from: 10, by: 3 }) |> join }}` }) }} + +

    Miscellaneous

    + +

    Use #raw block method or pass to emit a Template Expression without evaluating it: +

    + +{{ 'live-template' |> partial({ rows: 4, template: `{{ pass: 'shout' |> upper }} +{{#raw}} +{{ 'shout' |> upper }} +{{/raw}}` }) }} + +
    + Useful when you want to emit a client-side Vue method or show examples of #Script Expressions. +
    + +

    + of will let you find values of a specified type: +

    + +{{ 'live-template' |> partial({ rows: 1, template: `{{ ['A',1.1,2,3.3,true,null] |> of({ type: 'Double' }) |> join }}` }) }} + +

    + You can use appSetting method to access the value of the ScriptContext's configured AppSettings Provider: +

    + +
    {{ pass: 'websitePublicUrl' |> appSetting }} 
    + +

    Collections and Querying

    + +

    + Checkout the Live LINQ examples to explore the various collection and querying features. +

    + +

    Filter API Reference

    + +

    + See the Filter API Reference for the + full list of default scripts available. +

    + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/deploying-sharp-apps.html b/wwwroot/docs/deploying-sharp-apps.html new file mode 100644 index 0000000..7b11616 --- /dev/null +++ b/wwwroot/docs/deploying-sharp-apps.html @@ -0,0 +1 @@ +{{ httpResult({ status: 301, Location:'/sharp-apps/deploying-sharp-apps' }) |> return }} \ No newline at end of file diff --git a/wwwroot/docs/error-handling.html b/wwwroot/docs/error-handling.html new file mode 100644 index 0000000..9a5b15f --- /dev/null +++ b/wwwroot/docs/error-handling.html @@ -0,0 +1,328 @@ + + +

    + #Script uses Exceptions for error flow control flow that by default will fail fast by emitting the Exception details at + the point where the error occured before terminating the Stream and rethrowing the Exception which lets you for + instance catch the Exception in Unit Tests. +

    + +

    Handled Exceptions

    + +

    + You can instead choose to handle exceptions and prevent them from short-circuiting page rendering by assigning them to an + argument using either the assignError method which will capture any subsequent Exceptions thrown on the page: +

    + +{{ 'live-template' |> partial({ rows:3, template: `{{ 'ex' |> assignError }} +{{ 'An error!' |> throw }} +{{ ex |> select: { it.Message } }}` }) }} + +

    catchError

    + +

    + Alternatively you can specify `catchError` at each call-site where it will capture the Exception thrown by the method: +

    + +{{ 'live-template' |> partial({ rows:3, template: `{{ 'An error!' |> throw({ catchError: 'ex' }) }} +{{ ex |> select: { it.Message } }}` }) }} + +
    + An easy way to prettify errors in Web Pages is to use HTML Error Scripts. +
    + +{{#markdown}} +#### ifErrorReturn + +You can use `ifErrorReturn` in all Script Methods that [throw Managed Exceptions](#implementing-method-exceptions) +to specify a return value to use instead of throwing the Exception. + +E.g. this can be used to catch the `Unauthorized` Exception thrown when the `Authenticate` Service is called from an unauthenticated Request, e.g: + + {{#script}} + AUTH = {{ 'Authenticate' |> execService({ ifErrorReturn: "null" }) |> json }}; + {{/script}} + +{{/markdown}} + +

    Unhandled Exceptions Behavior

    + +

    + If Exceptions are unassigned they're considered to be unhandled. The default behavior for ScriptContext is to + rethrow Exceptions, but as ServiceStack's SharpPagesFeature is executed within the context of a HTTP Server + it's default is configured to: +

    + +{{ 'gfm/error-handling/01.md' |> githubMarkdown }} + +

    + Which instead captures any unhandled Exceptions in PageResult.LastFilterError and continues rendering the page except + it skips evaluating any subsequent methods and instead only evaluates the methods which handle errors, + currently: +

    + +{{#raw}} + + + + + + + + + + + + + + + + + + + + + +
    ifError + Only execute the expression if there's an error:
    + {{ ifError |> select: FAIL! { it.Message } }} +
    lastError + Returns the last error on the page or null if there was no error, that's passed to the next method:
    + {{ lastError |> ifExists |> select: FAIL! { it.Message } }} +
    htmlError + Display detailed htmlErrorDebug when in DebugMode + otherwise display a more appropriate end-user htmlErrorMessage. +
    htmlErrorMessageDisplay the Exception Message
    htmlErrorDebugDisplay a detailed Exception
    +{{/raw}} + +

    + This behavior provides the optimal UX for developers and end-users as HTML Pages with Exceptions are still rendered well-formed + whilst still being easily able to display a curated error messages for end-users without developers needing to guard against + executing methods when Exceptions occur. +

    + +

    Controlling Unhandled Exception Behavior

    + +

    + We can also enable this behavior on a per-page basis using the skipExecutingFiltersOnError method: +

    + +{{ 'live-template' |> partial({ rows:5, template: `{{ skipExecutingFiltersOnError }} +

    {{ 'Before Exception' }}

    +{{ 'An error!' |> throw }} +

    {{ 'After Exception' }}

    +{{ htmlErrorMessage }}` }) }} + +

    + Here we can see that any normal methods after exceptions are never evaluated unless they're specifically for handling Errors. +

    + +

    Continue Executing Methods on Unhandled Exceptions

    + +

    + We can also specify that we want to continue executing methods in which case you'll need to manually guard methods you want to + control the execution of using the ifNoError or ifError methods: +

    + +{{ 'live-template' |> partial({ rows:8, template: `{{ continueExecutingFiltersOnError }} +{{ ifNoError |> select: No Exception thrown yet! }} +

    {{ 'Before Exception' }}

    +{{ 'An error!' |> throw }} +{{ ifError |> select: There was an error! }} +

    {{ 'After Exception' }}

    +{{ htmlErrorDebug }}` }) }} + +

    Accessing Page Exceptions

    + +

    + The last Exception thrown are accessible using the lastError* methods: +

    + +{{ 'live-template' |> partial({ rows:6, template: `{{ continueExecutingFiltersOnError }} +{{ 'arg' |> throwArgumentNullExceptionIf(true) }} + + + +
    {{ lastError |> typeName }}
    Last Error Message{{ lastErrorMessage }}
    Last Error StackTrace{{ lastErrorStackTrace }}
    ` }) }} + +

    Throwing Exceptions

    + +

    + We've included a few of the popular Exception Types, methods prefixed with throw always throws the Exceptions below: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MethodException
    throwException(message)
    throwArgumentExceptionArgumentException(message)
    throwArgumentNullExceptionArgumentNullException(paramName)
    throwNotSupportedExceptionNotSupportedException(message)
    throwNotImplementedExceptionNotImplementedException(message)
    throwUnauthorizedAccessExceptionUnauthorizedAccessException(message)
    throwFileNotFoundExceptionFileNotFoundException(message)
    throwOptimisticConcurrencyExceptionOptimisticConcurrencyException(message)
    + +
    + You can extend this list with your own custom methods, see the + Error Handling Methods + for examples. +
    + +

    + These methods will only throw if a condition is met: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MethodException
    throwIfmessage |> Exception |> if(test)
    throwArgumentNullExceptionIfparamName |> ArgumentNullException |> if(test)
    ifthrowif(test) |> Exception(message)
    ifThrowArgumentNullExceptionif(test) |> ArgumentNullException(paramName)
    ifThrowArgumentException + if(test) |> ArgumentException(message)
    + if(test) |> ArgumentException(message, paramName)
    +
    + +

    Ensure Argument Helpers

    + +

    + The ensureAll* methods assert the state of multiple arguments where it will either throw an Exception unless + all the arguments meet the condition or return the Object Dictionary if all conditions are met: +

    + +{{ 'live-template' |> partial({ rows:5, template: `{{ 1 |> to => one }}{{ 'bar' |> to => foo }}{{ '' |> to => empty }} +{{ {one,foo,empty} |> ensureAllArgsNotNull |> htmlDump({ caption: 'ensureAllArgsNotNull' }) }} +ensureAllArgsNotEmpty: +{{ {one,foo,empty} |> ensureAllArgsNotEmpty({ assignError:'ex' }) |> htmlDump }} +{{ ex |> htmlErrorMessage }}` }) }} + +

    + The ensureAny* methods only requires one of the arguments meet the condition to return the Object Dictionary: +

    + +{{ 'live-template' |> partial({ rows:5, template: `{{ '' |> to => empty }} +{{ { foo, empty } |> ensureAnyArgsNotNull |> htmlDump({ caption: 'ensureAnyArgsNotNull' }) }} +ensureAnyArgsNotEmpty: +{{ { foo, empty } |> ensureAnyArgsNotEmpty({ assignError:'ex' }) |> htmlDump }} +{{ ex |> htmlErrorMessage }}` }) }} + +

    Fatal Exceptions

    + +

    + The Exception Types below are reserved for Exceptions that should never happen, such as incorrect usage of an API where it would've + resulted in a compile error in C#. When these Exceptions are thrown in a method or a page they'll immediately short-circuit execution of + the page and write the Exception details to the output. + The Fatal Exceptions include: +

    + +
      +
    • NotSupportedException
    • +
    • NotImplementedException
    • +
    • TargetInvocationException
    • +
    + +

    Rendering Exceptions

    + +

    + The OnExpressionException delegate in + Page Formats + are able to control how Exceptions in #Script expressions are rendered where if preferred exceptions can be rendered in-line + in the template expression where they occurred with: +

    + +{{ 'gfm/error-handling/02.md' |> githubMarkdown }} + +

    + The OnExpressionException can also suppress Exceptions from being displayed by capturing any naked Exception Types registered in + TemplateConfig.CaptureAndEvaluateExceptionsToNull + and evaluate the template expression to null which by default will suppress the following naked Exceptions thrown in methods: +

    + +
      +
    • NullReferenceException
    • +
    • ArgumentNullException
    • +
    + +

    Implementing Method Exceptions

    + +

    + In order for your own Method Exceptions to participate in the above Script Error Handling they'll need to be wrapped in an + StopFilterExecutionException including both the Script's scope and an optional options object + which is used to check if the assignError binding was provided so it can automatically populate it with the Exception. +

    + +

    + The easiest way to Implement Exception handling in methods is to call a managed function which catches all Exceptions and + throws them in a StopFilterExecutionException as seen in OrmLite's + DbScripts: +

    + +{{ 'gfm/error-handling/03.md' |> githubMarkdown }} + +

    + The overloads are so the methods can be called without specifying any method options. +

    + +

    + For more examples of different error handling features and strategies checkout: + ErrorHandlingTests.cs +

    + +{{ "doc-links" |> partial({ order }) }} diff --git a/src/wwwroot/docs/expression-viewer.html b/wwwroot/docs/expression-viewer.html similarity index 95% rename from src/wwwroot/docs/expression-viewer.html rename to wwwroot/docs/expression-viewer.html index e2c5638..8cb8fd5 100644 --- a/src/wwwroot/docs/expression-viewer.html +++ b/wwwroot/docs/expression-viewer.html @@ -1,6 +1,6 @@ {{#raw}} @@ -49,10 +49,12 @@
    Arguments
    + + @@ -188,4 +190,4 @@
    Tree
    {{/raw}} -{{ "doc-links" | partial({ order }) }} +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/gist-desktop-apps.html b/wwwroot/docs/gist-desktop-apps.html new file mode 100644 index 0000000..6d727ba --- /dev/null +++ b/wwwroot/docs/gist-desktop-apps.html @@ -0,0 +1 @@ +{{ httpResult({ status: 301, Location:'/sharp-apps/gist-desktop-apps' }) |> return }} \ No newline at end of file diff --git a/wwwroot/docs/hot-reloading.html b/wwwroot/docs/hot-reloading.html new file mode 100644 index 0000000..3fea3dd --- /dev/null +++ b/wwwroot/docs/hot-reloading.html @@ -0,0 +1,65 @@ + + +

    + #Script has a simple, but effective configuration-free hot-reloading feature built-in that's enabled + when registering the SharpPagesFeature plugin: +

    + +{{ 'gfm/hot-reloading/01.md' |> githubMarkdown }} + +

    + It can then be enabled in your website by adding the expression below at the top of your _layout.html: +

    + +{{ 'gfm/hot-reloading/02.md' |> githubMarkdown }} + +

    + This will embed the dependency-free hot-loader.js + script during development to poll the service endpoint below: +

    + +
    /templates/hotreload/page?path=path/to/page&eTag=lastETagReceived
    + +

    + Which responds with whether or not the client should reload their current page, preserving their current scroll offset. +

    + +
    + An easy way to temporarily disable hot reloading is to surround the expression with the noop block + {{#raw}}{{#noop}}{{#if debug}} ...{{/noop}}{{/raw}} or if preferred you can just use comments + {{#raw}}{{* #if debug ... *}}{{/raw}} +
    + +

    When to reload

    + +

    + Hot Reloading only monitors #Script Pages. You'll need to do a hard refresh with Ctrl+Shift+F5 if making changes to + .css or .js to force the browser to not use its cache. For App Code or Configuration changes you'll + need to restart your App to pick up the changes. +

    + +

    Implementation

    + +

    + The Service is just a wrapper around the ISharpPages.GetLastModified(page) API which returns the + last modified date of the page and any of its dependencies by scanning each expression in the page's AST to + find each referenced partial or included file to find the most recent modified date which it compares against + the eTag provided by the client to determine whether or not any of the pages resources have changed. +

    + +

    + Since it's just scanning the AST instead of evaluating it, it will only be able to find files and partials that were + statically referenced, i.e. the typical case of using a string literal for the file name as opposed to a dynamically creating it. +

    + +

    Preview

    + +

    If enabled and working correctly hot reloading should allow you to view instant UI changes on save:

    + + + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/html-scripts.html b/wwwroot/docs/html-scripts.html new file mode 100644 index 0000000..97c7791 --- /dev/null +++ b/wwwroot/docs/html-scripts.html @@ -0,0 +1,95 @@ + + +

    + HTML-specific methods for use in .html page formats. +

    + +

    htmlDump

    + +

    + Generates a HTML <table/> by recursively traversing the public members of a .NET instance object graph.
    + headerStyle can be any Text Style +

    + +{{ 'live-template' |> partial({ template: `{{ [{FirstName: 'Kurt', Age: 27},{FirstName: 'Jimi', Age: 27}] |> to => rockstars }} +{{ rockstars |> htmlDump }} +Uses defaults: {{ rockstars |> htmlDump({ headerTag: 'th', headerStyle: 'splitCase' }) }}` }) }} + +
    htmlDump customizations
    + +{{ 'live-template' |> partial({ rows:3, template: `{{ [{FirstName: 'Kurt', Age: 27},{FirstName: 'Jimi', Age: 27}] |> to => rockstars }} +{{ rockstars |> htmlDump({ className: "table table-striped", caption: "Rockstars" }) }} +{{ [] |> htmlDump({ captionIfEmpty: "No Rockstars"}) }}` }) }} + +

    htmlClass

    + +

    + Helper method to simplify rendering a class="..." list on HTML elements, it accepts Dictionary of boolean flags, List of strings or string, e.g: +

    +{{ 'live-template' |> partial({ rows:7, template: `{{ 1 |> to => index }} +Dictionary All: {{ {alt:isOdd(index), active:true} |> htmlClass }} +Dictionary One: {{ {alt:isEven(index), active:true} |> htmlClass }} +Dictionary None: {{ {alt:isEven(index), active:false} |> htmlClass }} +List: All {{ ['nav', !disclaimerAccepted? 'blur':'', isEven(index)? 'alt':''] |> htmlClass }} +String: {{ 'alt' |> if(isOdd(index)) |> htmlClass }} +htmlClassList: {{ {alt:isOdd(index), active:true} |> htmlClassList }}` }) }} + +

    + Or use htmlClassList if you just want the list of classes. +

    + +

    htmlAttrs

    + +

    + Helper method to simplify rendering HTML attributes on HTML elements, it accepts a Dictionary of Key/Value Pairs + which will be rendered as HTML attributes. Keys with a boolean value will only render the attribute name if it's true + and htmlAttrs also supports common JS keyword overrides for htmlFor and className, e.g: +

    +{{ 'live-template' |> partial({ rows:4, template: `Normal Key/Value Pairs: {{ {'data-index':1,id:'nav',title:'menu'} |> htmlAttrs }} +Boolean Values: {{ {selected:true, active:false} |> htmlAttrs }} +Keyword Names: {{ {for:'txtName', class:'lnk'} |> htmlAttrs }} +Special Cases: {{ {htmlFor:'txtName', className:'lnk'} |> htmlAttrs }}` }) }} + +

    htmlError

    + +

    + Renders an Exception into HTML, in DebugMode this renders a + detailed view using the htmlErrorDebug method otherwise it's rendered using htmlErrorMessage. +

    + +{{ 'live-template' |> partial({ rows:3, template: `{{ 'the error' |> throw({ assignError: 'ex' }) }} +{{ ex |> htmlError }} +{{ ex |> htmlError({ className: "alert alert-warning" }) }}` }) }} + +

    htmlErrorMessage

    + +

    + Renders the Exception Message into into HTML using the default class in Args[DefaultErrorClassName], + overridable with className: +

    + +{{ 'live-template' |> partial({ rows:3, template: `{{ 'the error' |> throw({ assignError: 'ex' }) }} +{{ ex |> htmlErrorMessage }} +{{ ex |> htmlErrorMessage({ className: "alert alert-warning" }) }}` }) }} + +

    htmlErrorDebug

    + +

    + Renders a debug view of the Exception into into HTML using the default class in Args[DefaultErrorClassName], + overridable with className: +

    + +{{ 'live-template' |> partial({ rows:3, template: `{{ 'the error' |> throw({ assignError: 'ex' }) }} +{{ ex |> htmlErrorDebug }} +{{ ex |> htmlErrorDebug({ className: "alert alert-warning" }) }}` }) }} + +

    + See the Scripts API Reference for the + full list of HTML methods available. +

    + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/installation.html b/wwwroot/docs/installation.html new file mode 100644 index 0000000..18c0a26 --- /dev/null +++ b/wwwroot/docs/installation.html @@ -0,0 +1,181 @@ + + +

    Running Sharp Scripts or creating Sharp Apps

    + +

    + If you're only looking to run + Sharp Scripts or creating Sharp Apps + or Gist Desktop Apps then you'll only need to install the + x or app dotnet tools. +

    + +

    Cross-platform command-line tool for Windows, macOS and Linux

    + +
    $ dotnet tool install -g x
    + +

    Windows 64bit in Chromium Desktop App

    + +
    $ dotnet tool install -g app
    + +

    Update to latest versions

    + +

    + If you already have the tools installed, upgrade them to the latest version with: +

    + +
    $ dotnet tool update -g x
    +$ dotnet tool update -g app
    + +

    Using #Script within .NET or .NET Core Apps

    + +

    + #Script is available for .NET Core or .NET v4.5+ Framework projects from the ServiceStack.Common NuGet Package: +

    + +
    PM> Install-Package ServiceStack.Common
    + +

    + It's also available for ASP.NET Core Apps on the .NET Framework by installing: +

    + +
    PM> Install-Package ServiceStack.Common.Core
    + +

    + You're now all set to have fun with #Script! Start by evaluating a script: +

    + +{{ 'gfm/installation/01.md' |> githubMarkdown }} + +{{ "live-template" |> partial({ template: "The time is now: {{ now |> dateFormat('HH:mm:ss') }}" }) }} + +

    Configure with ServiceStack

    + +

    + To utilize #Script Pages in ServiceStack, + register the SharpPagesFeature plugin: +

    + +{{ 'gfm/installation/02.md' |> githubMarkdown }} + +

    Starter Project Templates

    + +

    + The Starter Projects below provide a quick way to get started with a pre-configured ServiceStack App with #Script Pages: +

    + +

    .NET Core #Script Pages Project

    + +

    + Create a new #Script Pages Website .NET 5.0 App with + x new: +

    + +
    +
    $ dotnet tool install --global x 
    +
    +$ x new script ProjectName
    + + +.NET Core Starter Template + +

    ASP.NET Core #Script Pages Project on .NET Framework

    + +

    + To create ASP.NET Core Project on the .NET Framework: +

    + +
    +
    $ x new script-corefx ProjectName
    + +

    ASP.NET v4.5 #Script Pages Project

    + +

    + For ASP.NET v4.5+ projects create a new ServiceStack ASP.NET #Script Pages with Bootstrap from the VS.NET Templates in + ServiceStackVS VS.NET Extension + to create an ASP.NET v4.5 Project using + ServiceStack's recommended project structure: +

    + +

    + + ASP.NET v4.5 Starter Template + +

    + +

    SharpApp Project Templates

    + +

    + Sharp Apps is our revolutionary new approach to dramatically simplify .NET Wep App development by using ServiceStack + Templates to build entire Websites in a live real-time development workflow without any C# and requiring no development environment, + IDE’s or build tools - dramatically reducing the cognitive overhead and conceptual knowledge required for developing .NET Core Websites in + a powerful dynamic templating language that's simple, safe and intuitive enough that Web Designers and Content Authors can use. +

    + +

    Init Sharp App

    + +

    + The easiest way to create an empty Sharp App is to run app init from an Empty App Directory: +

    + +
    $ md Acme && cd Acme && web init
    + +

    + Which will create an empty Sharp App as seen in the Spirals video: +

    + + + + +

    Bare SharpApp

    + +

    + To start with a simple and minimal website, create a new bare-app project template: +

    + + + +
    $ x new bare-app ProjectName
    + +

    + This creates a multi-page Bootstrap Website with Menu navigation that's ideal for content-heavy Websites. +

    + +

    Parcel SharpApp

    + +

    + For more sophisticated JavaScript Sharp Apps we recommended starting with the + parcel-app project template: +

    + + + +
    $ x new parcel-app ProjectName
    + +

    + This provides a simple and powerful starting template for developing modern JavaScript .NET Core Sharp Apps utilizing the + zero-configuration Parcel bundler to enable a rapid development workflow with access to + best-of-class web technologies like TypeScript that's managed by pre-configured + npm scripts to handle its entire dev workflow. +

    + +

    SharpApp Examples

    + +

    + View our Example Sharp Apps to explore different features and see examples of how easy it is to create with Sharp Apps: +

    + +
      +
    • About Bare SharpApp - Simple Content Website with Menu navigation
    • +
    • About Parcel SharpApp - Simple Integrated Parcel SPA Website utilizing TypeScript
    • +
    • Redis HTML - A Redis Admin Viewer developed as server-generated HTML Website
    • +
    • Redis Vue - A Redis Admin Viewer developed as Vue Client Single Page App
    • +
    • Rockwind - Combination of mult-layout Rockstars website + data-driven Nortwhind Browser
    • +
    • Plugins - Extend Sharp Apps with Plugins, Filters, ServiceStack Services and other C# extensions
    • +
    • Chat - Highly extensible App with custom AppHost that leverages OAuth + Server Events for real-time Chat
    • +
    • Blog - A minimal, multi-user Twitter OAuth blogging platform that can create living, powerful pages
    • +
    + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/introduction.html b/wwwroot/docs/introduction.html new file mode 100644 index 0000000..2cbbcf9 --- /dev/null +++ b/wwwroot/docs/introduction.html @@ -0,0 +1,383 @@ + + +

    + #Script is a simple and elegant, highly-extensible, sandboxed, + high-performance general-purpose, embeddable scripting language for .NET Core and .NET Framework. + It's designed from the ground-up to be incrementally adoptable where its basic usage is simple enough + for non-technical users to use whilst it progressively enables access to more power and functionality allowing it to scale up to + support full server-rendering Web Server workloads and beyond. Its high-fidelity with JavaScript syntax allows it to use a common + language for seamlessly integrating with client-side JavaScript Single Page App frameworks where its syntax is designed to be + compatible with Vue filters. +

    + +

    Executing #Script in .NET

    + +

    + To render the pages we first create and initialize a ScriptContext +

    + +{{ 'gfm/introduction/01.md' |> githubMarkdown }} + +

    + The ScriptContext is the sandbox where all scripts are executed within, everything your script has access to and generates + is maintained within the ScriptContext. Once initialize you can start using it to evaluate scripts which you can do with: +

    + +{{ 'gfm/introduction/02.md' |> githubMarkdown }} + +

    + Scripts only have access to script methods, blocks and arguments defined within its Context, which for an empty Context are + the comprehensive suite of safe Default Scripts and HTML Scripts. +

    + + +
    Rendering Script Pages
    + +

    + Behind the scenes this creates a dynamic page with your Script and uses the PageResult to render it to a string: +

    + +{{ 'gfm/introduction/09.md' |> githubMarkdown }} + +

    Evaluating Scripts with return values

    + +

    + Sharp Scripts can be used to either render text as above, they can also have return values with the return method + which can be accessed using the Evaluate() API: +

    + +{{ 'gfm/introduction/10.md' |> githubMarkdown }} + + +

    Usage in .NET

    + +

    + To evaluate #Script in .NET you'll first create the ScriptContext containing all functionality and features + your Scripts have access to: +

    + +{{ "net-usage" |> partial }} + +

    Scripting .NET Types

    + +

    + To find out how to enable unrestricted access to existing .NET Types see Scripting .NET Types. +

    + +

    Autowired using ScriptContext IOC

    + +

    + ScanTypes is useful for Autowiring instances of scripts created using ScriptContext's configured IOC + where they're also injected with any registered IOC dependencies and can be used to autowire + ScriptMethods, + ScriptBlocks and + Code Pages: +

    + +{{ 'gfm/methods/07.md' |> githubMarkdown }} + +

    + When the ScriptContext is initialized it will go through each Type and create an autowired instance of each Type + and register them in the ScriptMethods, ScriptBlocks and CodePages collections. +

    + +

    + An alternative to registering a single Type is to register an entire Assembly, e.g: +

    + +{{ 'gfm/methods/08.md' |> githubMarkdown }} + +

    + Where it will search each Type in the Assembly for Script Methods and automatically register them. +

    + +

    Multi page Scripts

    + +{{ "live-pages" |> partial( + { + page: 'page', + files: + { + '_layout.html': 'I am the Layout: {{ page }}', + 'page.html' : 'I am the Page' + } + }) +}} + + +

    + Typically you'll want to use #Script to render entire pages which are sourced from its configured + Virtual File System which uses an In Memory Virtual + File System by default that we can programmatically populate: +

    + +{{ 'gfm/introduction/03.md' |> githubMarkdown }} + +

    + Pages are rendered using a PageResult essentially a rendering context that needs to be provided the Page to render: +

    + +{{ 'gfm/introduction/04.md' |> githubMarkdown }} + +

    + The script page output can then be asynchronously rendered to any Stream: +

    + +{{ 'gfm/introduction/05.md' |> githubMarkdown }} + +

    + Or to access the output as a string you can use the convenience extension method: +

    + +{{ 'gfm/introduction/06.md' |> githubMarkdown }} + +

    + All I/O within #Script is non-blocking, but if you're evaluating an adhoc script or using the default In Memory Virtual FileSystem + there's no I/O so you can safely block to get the generated output with: +

    + +{{ 'gfm/introduction/07.md' |> githubMarkdown }} + +

    + Both APIs returns the result you see in the Live Example above. +

    + +

    Cascading Resolution

    + +

    + There's no forced special centralized folders like /Views or /Views/Shared required to store layouts or share partials or + artificial "Areas" concept to isolate website sections. Different websites or sections are intuitively grouped into different + folders and #Script automatically resolves the closest layout it finds for each page. Cascading resolution also applies to + including files or partial pages where you can use just its name to resolve the closest one, or an absolute path from the + WebRootPath to include a specific partial or file from a different folder. +

    + +

    Instant Startup

    + +

    + There's no pre-compilation, pre-loading or Startup penalty, all Pages are lazily loaded on first use and cached for fast subsequent + evaluation. Its instant Startup, fast runtime performance and sandboxed isolation opens it up to a myriad of new use-cases which + can enhance .NET Apps with a rich Live programming experience. +

    + +

    Fast Runtime Performance

    + +

    + #Script is fast, parsing is done using StringSegment for minimal GC pressure, all I/O is non-blocking inc. async + writes to OutputStream's. There's no buffering: Layouts, Pages and Partials are asynchronously written to a forward only stream. + There's no runtime reflection, each filter or binding within #Script expressions executes compiled and cached C# Expressions. +

    + +

    Pure, Functional and Reactive

    + +

    + #Script is pure at both the library-level where they're a clean library with no external dependencies outside + ServiceStack.Common, no coupling to web frameworks, external configuration files, + designer tooling, build tools, pre-compilation steps or require any special deployment requirements. + It binds to simple, clean, small interfaces for its + Virtual File System, + IOC and + AppSettings providers which are easily overridden to + integrate cleanly into external web frameworks. +

    + +

    + It's also pure at the code-level where it doesn't have any coupling to concrete dependencies, components or static classes. Default script methods + don't mutate any external state and return an expression. This forces a more readable and declarative + programming-style, one where it is easier to quickly determine the subject of expressions and the states that need to be met for methods to be executed. + Conceptually scripts are "evaluated" in that they take in arguments, methods and script pages as inputs and evaluates them to an output stream. + They're highly testable by design where the same environment used to create the context can easily be re-created in Unit tests, + including simulating pages in a File System using its In Memory Virtual File System. +

    + +

    Optimal Development Experience

    + +

    + The above attributes enables an instant iterative development workflow with a Live Development experience that supports + configuration-free Hot Reloading out of the box that lets you build entire + Web Apps and Web APIs without ever having to compile or manually Refresh pages. +

    + +

    Simplicity end-to-end

    + +

    + There are 3 main concepts in #Script: Arguments - variables which can be made available + through a number of cascading sources, Script Blocks which define the statements available and + Script Methods - public C# methods that exist in the list of + ScriptMethods registered in the PageResult or ScriptContext that scripts are executed within. +

    + +

    + Layouts, Pages and Partials are all just "pages", evaluated in the same way with access to arguments and filters. Parameters passed + to partials are just scoped arguments, accessed like any other arguments. Typically pages are sourced from the configured + File System but when access to more advanced functionality is required they can also be Code Pages implemented + in pure C# that are otherwise interchangeable and can be used anywhere a normal page is requested. +

    + +

    + There's no language constructs or reserved words in #Script itself, all functionality is implemented inside script methods. + There's also nothing special about the Default Scripts included with #Script + other than they're pre-registered by default. External script methods can do anything built-in methods can do which can just + as easily be shadowed or removed giving you a clean slate where you're free to define your own + language and preferred language naming scheme. +

    + +

    + They're non-invasive, by default #Script Pages is configured to handle + .html files but can be configured to handle any html extension or text file format. + When a page doesn't contain any #Script Expressions it's contents are returned as-is, making it easy to adopt in existing + static websites. +

    + +

    + #Script scripts are sandboxed, they can't call static or instance methods, invoke setters or access anything + outside of the arguments, methods and blocks made available to them. Without methods or blocks, expressions wouldn't have any methods + they can call, leaving them with the only thing they can do which is access arguments and replace their variable placeholders, + including the {{ pass: page }} placeholder to tell the Layout where to render the page. +

    + +

    High-level, Declarative and Intent-based

    + +

    + High-level APIs are usually synonymous with being slow by virtue of paying a penalty for their high-level abstraction, + but in the domain of I/O and Streams such-as rendering text to Streams they make it trivial to compose high-level functionality + that's implemented more efficiently than would be typically written in C# / ASP.NET MVC Apps. +

    + +

    + As an example let's analyze the script below: +

    + +{{ 'gfm/introduction/08.md' |> githubMarkdown }} + +

    + The intent of #Script code should be clear even if it's the first time reading it. From left-to-right we can deduce that it + retrieves a url from the quote table, downloads its contents of and converts it to markdown before replacing the text "Razor" and + "2010" and displaying the raw non-HTML encoded html output. +

    + +
    Implementation using ASP.NET MVC
    +

    + In MVC the typical and easiest approach would be to create a an MVC Controller Action, use EF to make a sync call to access the database, + a sync call with a new HTTP Client to download the content which is buffered inside the Controller action before returned inside a View Model + that is handed off to MVC to execute the View inside the default Layout. +

    + +
    Implementation using #Script
    +

    + What does #Script do? lets step through the first filter: +

    + +{{ "live-template" |> partial({ output:'no-scroll', rows:1, template: "{{ 'select url from quote where id= @id' |> dbScalar({ id:1 }) |> htmlLink }}" }) }} + +

    + What filter implementation gets called depends on which DB Scripts is registered, if your RDBMS supports ADO.NET's + async API you can register + DbScriptsAsync + to execute all queries asynchronously, otherwise if using an RDBMS whose ADO.NET Provider doesn't support async you can register the + DbScripts + to execute each DB request synchronously without paying for any pseudo-async overhead, in each case the exact same code executes + the most optimal ADO.NET APIs. #Script also benefits from using the much faster + OrmLite and also saves on the abstraction cost from generating a + parameterized SQL Statement from a Typed Expression-based API. +

    + +
    Arguments chain
    +

    + The next question becomes what is id bound to? Similar to JavaScript's prototype chain it resolves the closest + argument in its Arguments hierarchy, e.g. when evaluated as a view page it could be set + by arguments in [paged based routes](/docs/sharp-pages#page-based-routing) or when the same page is evaluated as a + partial it could be set from the scoped arguments it was called with. +

    + +
    Async I/O
    +

    + The url returned from the db is then passed to the urlContents filter which if it was the last filter in the expression + avoids any buffering by asynchronously streaming the url content stream directly to the forward-only HTTP Response Stream: +

    + +
    {{ pass: url |> urlContents }}
    + +

    + urlContents is a Block Method which instead of returning a value writes its + response to the OutputStream it's given. But then how could we convert it to Markdown if it's already written to the Response Stream? + #Script analyzes the Expression's AST to determine if there's any filters remaining, if there is it gives the urlContents + Block filter a MemoryStream to write to, then forwards its buffered output to the next filter. Since they don't return values, + the only thing that can come after a Block Filter are other Block filters or Stream Transformers. + markdown is one such Filter Transformer which takes in a stream of Markdown + and returns a Stream of HTML. +

    + +
    {{ pass: url |> urlContents |> markdown }}
    + +
    Same intent, different implementations
    +

    + The assignTo filter is used to set a value in the current scope. After Block Filters, a different assignTo Block Filter + is used with the same name and purpose but a different implementation that reads all the contents of the stream into a UTF-8 string + and sets a value in the current scope before returning an empty Stream so nothing is written to the Response. +

    + +
    {{ pass: url |> urlContents |> markdown |> to => quote }}
    + +

    + Once the streamed output is captured and assigned it goes back into becoming a normal argument that opens it up to be + able to use all filters again, which is how we're able to use the string replace filters before rendering the final result :) +

    + +

    /examples/qotd?id=1

    + +
    Using the most efficient implementation allowable
    +

    + So whilst it conceptually looks like each filter is transforming large buffered strings inside every filter, the expression is + inspected to utilize the most efficient implementation allowable. At the same time filters are not statically bound to + any implementation so you could for instance insert a Custom Filter before the Default + Filters containing the same name and arguments count to have #Script execute your custom script methods instead, all whilst the + script source code and intent remains untouched. +

    + +
    Intent-based code is easier to augment
    +

    + If it was later discovered that some URLs were slow or rate-limited and you needed to introduce caching, your original C# code + would require a fair amount of rework, in #Script you can simply add WithCache to call the + urlContentsWithCache filter to return locally cached contents + on subsequent requests. +

    + +
    {{ pass: url |> urlContentsWithCache |> markdown }}
    + +

    Simplified Language

    + +

    + As there's very little ceremony in #Script, a chain of filters looks like it's using its own DSL to accomplish each task and + given implementing and registering custom filters is trivial you're + encouraged to write the intent of your code first then implement any filters that are missing to realize its intent. Once + you've captured the intent of what you want to do, it's less likely to ever need to change, focus is instead on + fixing any bugs and making the filter implementations as efficient as possible, which benefits all existing code using the same filter. +

    + +

    + To improve readability and make it more approachable, #Script aims to normalize the mechanics of the underlying implementation from + the code's intent so you can use the same syntax to access an argument, e.g. {{ pass: arg }} as you would a filter without + arguments {{ pass: now }} and just like JavaScript you can use obj.Property syntax to access both a public property + on a Type or an entry in a Dictionary. +

    + +

    Late-bound flexibility

    + +

    + There's no static coupling to concrete classes, static methods or other filters, ambiguous method exceptions or namespace collisions. + Each filter is self-contained and can easily be shared and dropped into any Web App by + registering them in a list. Inspired by the power and + flexibility in Smalltalk and LISP, filters are late-bound at run-time to the first matching filter + in the user-defined list of ScriptMethods. This ability to shadow filters enables high-level intent-based APIs decoupled from implementations which + sendToAutoQuery leverages to automatically route filter invocations to + the appropriate implementation depending of if it's an AutoQuery RDBMS or an + AutoQuery Data request, masking their implementations as a transparent detail. + This flexibility also makes it easy create proxies, intercept and inject behavior like logging or profiling without modifying existing + script method implementations or script source code. +

    + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/methods.html b/wwwroot/docs/methods.html new file mode 100644 index 0000000..6cd4518 --- /dev/null +++ b/wwwroot/docs/methods.html @@ -0,0 +1,214 @@ + + +

    + #Script scripts are sandboxed, they can't call methods on objects nor do they have any access to any static functions built into the .NET Framework, + so just as Arguments define all data and objects available, script methods define all functionality available to scripts. +

    + +

    + The only methods registered by default are the Default Scripts + containing a comprehensive suite of scripts useful within #Script Pages or custom Scripting environments and + HTML Scripts. + There's nothing special about these script methods other than they're pre-registered by default, your scripts have access to the same + APIs and functionality and can do anything that built-in scripts can do. +

    + +

    What are Script Methods?

    + +

    + Script methods are just C# public instance methods from a class that inherits from ScriptMethods, e.g: +

    + +{{ 'gfm/methods/03.md' |> githubMarkdown }} + +

    Registering Methods

    + +

    + The examples below show the number of different ways scripts can be registered: +

    + +
    Add them to the ScriptContext.ScriptMethods
    + +

    + Methods can be registered by adding them to the context.ScriptMethods collection directly: +

    + +{{ 'gfm/methods/04.md' |> githubMarkdown }} + +

    That can now be called with:

    + +{{ 'gfm/methods/05.md' |> githubMarkdown }} + +

    + This also shows that Scripts are initialized and have access to the ScriptContext through the Context property. +

    + +
    Add them to PageResult.ScriptMethods
    + +

    + If you only want to use a custom script in a single Page, it can be registered on the PageResult that renders it instead: +

    + +{{ 'gfm/methods/06.md' |> githubMarkdown }} + +
    Autowired using ScriptContext IOC
    + +

    + Autowired instances of scripts can also be created using ScriptContext's configured IOC where they're + also injected with any registered IOC dependencies. To utilize this you need to specify the Type of the ScriptMethods that + should be Autowired by either adding it to the ScanTypes collection: +

    + +{{ 'gfm/methods/07.md' |> githubMarkdown }} + +

    + When the ScriptContext is initialized it will go through each Type and create an autowired instance of each Type + and register them in the ScriptMethods collection. An alternative to registering a single Type is to register + an entire Assembly, e.g: +

    + +{{ 'gfm/methods/08.md' |> githubMarkdown }} + +

    + Where it will search each Type in the Assembly for Script Methods and automatically register them. +

    + +

    Method Resolution

    + +

    + #Script will use the first matching method with the same name and argument count it can find by searching through all + registered methods in the ScriptMethods collection, so you could override default methods with the same name by + inserting your ScriptMethods as the first item in the collection, e.g: +

    + +
    Shadowing Methods
    + +{{ 'gfm/methods/09.md' |> githubMarkdown }} + +

    Delegate Arguments

    + +

    + In addition to Script Methods, #Script also lets you call delegates registered as arguments: +

    + +{{ 'gfm/script-net/01.md' |> githubMarkdown }} + +

    + Which just like user-defined functions and other Script Methods can be called positionally or as an + extension method: +

    + +
    fn(1,2)
    +1.fn(2)
    + +

    Removing Default Scripts

    + +

    + Or if you want to start from a clean slate, the default scripts can be removed by clearing the collection: +

    + +{{ 'gfm/methods/02.md' |> githubMarkdown }} + + +

    Auto coercion into Method argument Types

    + +

    + A unique feature of script methods is that each of their arguments are automatically coerced into the script method argument Type using the + powerful conversion facilities built into ServiceStack's + Auto Mapping Utils and + Text Serializers which can deserialize most of .NET's primitive Types like + DateTime, TimeSpan, Enums, etc in/out of strings as well being able to convert a Collection into other Collection + Types and any Numeric Type into any other Numeric Type which is how, despite only accepting doubles: +

    + +{{ 'gfm/methods/10.md' |> githubMarkdown }} + +

    + squared can also be used with any other .NET Numeric Type, e.g: byte, int, long, decimal, etc. + The consequence to this is that there's no method overloading in script methods which are matched based on their name and their number of arguments + and each argument is automatically converted into its script method Param Type before it's called. +

    + +

    Context Script Methods

    + +

    + Script Methods can also get access to the current scope by defining a ScriptScopeContext as it's first parameter which + can be used to access arguments in the current scope or add new ones as done by the assignTo method: +

    + +{{ 'gfm/methods/11.md' |> githubMarkdown }} + +

    Block Methods

    + +

    + Script Methods can also write directly into the OutputStream instead of being forced to return buffered output. A Block Method + is declared by its Task return Type where instead of returning a value it instead writes directly to the + ScriptScopeContext OutputStream as seem with the implementation of the includeFile protected scripts: +

    + +{{ 'gfm/methods/12.md' |> githubMarkdown }} + +
    + For maximum performance all default script methods which perform any I/O use Block Methods to write directly to the OutputStream + and avoid any blocking I/O or buffering. +
    + +

    Block Methods ends the template expression

    + +

    + Block methods effectively end the filter chain expression since they don't return any value that can be injected into + a normal method. The only thing that can come after a Block Method are other Block Methods or Filter Transformers. + If any are defined, the output of the Block Method is buffered into a MemoryStream and passed into + the next Block Method or Filter Transformer in the chain, its output is then passed into the next one in the chain if any, + otherwise the last output is written to the OutputStream. +

    + +

    + An example of using a Block method with a Filter Transformer is when you want include a markdown document and then + convert it to HTML using the markdown Filter Transformer before writing its HTML output to the OutputStream: +

    + +
    {{ pass: 'doc.md' |> includeFile |> markdown }}
    + +

    Capture Block Method Output

    + +

    + You can also capture the output of a Block Method and assign it to a normal argument by using the assignTo Block Method: +

    + +
    {{ pass: 'doc.md' |> includeFile |> to => contents }}
    + +

    Async support

    + +{{ 'gfm/methods/13.md' |> githubMarkdown }} + +

    Implementing Method Exceptions

    + +

    + In order for your own Method Exceptions to participate in the above Script Error Handling they'll need to be wrapped in an + StopFilterExecutionException including both the Script's scope and an optional options object + which is used to check if the assignError binding was provided so it can automatically populate it with the Exception. +

    + +

    + The easiest way to Implement Exception handling in methods is to call a managed function which catches all Exceptions and + throws them in a StopFilterExecutionException as seen in OrmLite's + DbScripts: +

    + +{{ 'gfm/error-handling/03.md' |> githubMarkdown }} + +

    + The overloads are so the methods can be called without specifying any method options. +

    + +

    + For more examples of different error handling features and strategies checkout: + ErrorHandlingTests.cs +

    + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/model-view-controller.html b/wwwroot/docs/model-view-controller.html new file mode 100644 index 0000000..621a3f2 --- /dev/null +++ b/wwwroot/docs/model-view-controller.html @@ -0,0 +1,62 @@ + + +

    + Simplicity is a driving goal behind the design of #Script where in its simplest form it's usable by non-programmers who just + know HTML as they're able to embed dynamic content in their HTML pages using intuitive Mustache syntax and with the + intuitive way in how #Script Pages works they're able to develop entire content-heavy websites + without needing to write any code. +

    + +

    + As requirements become more demanding you can use progressively advanced features to enable greater flexibility and + functionality whilst still retaining using scripts to generate the HTML view with access to existing layouts and partials. +

    + +

    + The first option to enable functionality is to reuse the rich functionality available in Services to populate the data + required by your view. To do this in ServiceStack add a reference to the ISharpPages dependency which was registered + by ScriptContext, then return a PageResult containing the .html page you want to + render as well as any additional arguments you want available in the page: +

    + +

    CustomerServices.cs

    + +{{ 'gfm/model-view-controller/01.md' |> githubMarkdown }} + +

    + The above can Service can also be made slightly shorter by using the Request.GetPage() extension method, e.g: +

    + +{{ 'gfm/model-view-controller/02.md' |> githubMarkdown }} + +

    Model PageResult Property

    + +

    + The Model property is a special argument that automatically registers all its public properties as arguments + as well as registering itself in the model argument, this lets views reference model properties directly like + {{ pass: CustomerId }} instead of the more verbose {{ pass: model.CustomerId }} as used in: +

    + +

    examples/customer.html

    + +{{ 'gfm/model-view-controller/03.md' |> githubMarkdown }} + +

    + Now that we have the Service handling the Request and populating the Model for our page we can use it to view each Customer + in a nice detail page: +

    + +
    +
    + +
    +
    +
    +
    +
    + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/mvc-netcore.html b/wwwroot/docs/mvc-netcore.html new file mode 100644 index 0000000..f8ab29a --- /dev/null +++ b/wwwroot/docs/mvc-netcore.html @@ -0,0 +1,104 @@ + + +

    + The easiest way to enable #Script support in a .NET Core App is to register an empty ServiceStack AppHost + with the SharpPagesFeature plugin enabled: +

    + +{{ 'gfm/mvc-netcore/01.md' |> githubMarkdown }} + +

    + This let you use the #Script Pages and Sharp Code Pages features + to build your entire .NET Core Web App with Templates without needing to use anything else in ServiceStack. +

    + +

    MVC PageResult

    + +

    + Just as you can use a ServiceStack Service as the Controller for a Sharp Page View, you can also use an MVC Controller + which works the same way as a Service, but instead of returning the PageResult directly, you need to call + ToMvcResult() extension method to wrap it in an MVC ActionResult, e.g: +

    + +{{ 'gfm/mvc-netcore/02.md' |> githubMarkdown }} + +

    + This has the same effect as it has in a ServiceStack Service where the PageResult sets the Content-Type + HTTP Response Header and asynchronously writes the template to the Response OutputStream for maximum performance. +

    + +

    Sharing Dependencies

    + +

    + To resolve ISharpPages from ASP.NET Core you will need to register it in ASP.NET's IOC, the easiest way to do + this is to use a Modular Startup + where any dependencies registered in Configure(IServiceCollection) are available to both ASP.NET Core and ServiceStack, e.g: +

    + +{{ 'gfm/mvc-netcore/03.md' |> githubMarkdown }} + +

    + This will then let you access ISharpPages as a normal dependency in your MVC Controller: +

    + +{{ 'gfm/mvc-netcore/04.md' |> githubMarkdown }} + +

    Using #Script without a ServiceStack AppHost

    + +

    + If you don't need the #Script Pages support and would like to use Templates without a + ServiceStack AppHost you can, just register a ScriptContext instance in .NET Core's IOC, replace + its Virtual File System to point to the WebRootPath and you can start returning PageResult's as above: +

    + +{{ 'gfm/mvc-netcore/05.md' |> githubMarkdown }} + +

    + Checkout the NetCoreApps/MvcTemplates repository for a + stand-alone example of this, complete with a + sidebar.html partial + and a CustomScriptMethods.cs. +

    + + + + + +

     

    + +

    ASP.NET v4.5 Framework MVC

    + +

    + You can return a Sharp Page in a classic ASP.NET MVC similar to ASP.NET Core MVC Controller except that in order to + work around the lack of being able to async in classic ASP.NET MVC Action Results you need to return a task, but because we can + replace the IOC Controller Factory in ASP.NET MVC + you can use Constructor Injection to access the ISharpPages dependency: +

    + +{{ 'gfm/mvc-aspnet/01.md' |> githubMarkdown }} + +

    ASP.NET MVC + Templates Demo

    + +

    + Checkout the ServiceStackApps/MvcTemplates repository for a + stand-alone example, complete with a + sidebar.html partial + and a CustomScriptMethods.cs. +

    + + + + + +
    + This demo was created from the + ServiceStack ASP.NET MVC5 Empty VS.NET Template. +
    + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/page-formats.html b/wwwroot/docs/page-formats.html new file mode 100644 index 0000000..6a1583c --- /dev/null +++ b/wwwroot/docs/page-formats.html @@ -0,0 +1,74 @@ + + +

    + #Script is a general purpose text templating language which doesn't have any notion of HTML or any other format embedded in the language itself. + It simply emits text outside of mustaches verbatim and inside mustaches evaluates the expression and emits the result. + All custom behavior pertinent to specific text formats are instead extrapolated in its Page Format. +

    + +

    + #Script supports rendering multiple different text formats simultaneously within the same ScriptContext, + but the only Page Format pre-registered by default is the HtmlPageFormat which is defined below: +

    + +

    HTML Page Format

    + +{{ 'gfm/page-formats/01.md' |> githubMarkdown }} + +

    + The HtmlPageFormat is used to specify: +

    + +
      +
    • That the Page Arguments should be defined within HTML comments at the top of the page
    • +
    • That files with the .html extension should use this format
    • +
    • That it should return the text/html Content-Type in HTTP Responses
    • +
    • That the result of all Template Expressions should be HTML-encoded by default
    • +
    • That it should not use a default layout for complete HTML files that start with a HTML or HTML5 tag
    • +
    • How Expression Exceptions should be rendered in HTML if RenderExpressionExceptions is enabled
    • +
    + +

    Results of all #Script Expressions are HTML-encoded by default

    + +

    + The EncodeValue in the HtmlPageFormat automatically encodes the results of all #Script expressions so they're + safe to embed in HTML, e.g: +

    + +{{ 'live-template' |> partial({ rows: 1, template: "{{ 'hi' }}" }) }} + +

    + You can use the raw default filter to skip HTML-encoding which will let you emit raw HTML as-is: +

    + +{{ 'live-template' |> partial({ rows: 1, template: "{{ 'hi' |> raw }}" }) }} + +
    + Inside filters you can return strings wrapped in a new RawString(string) or use the ToRawString() extension + method to skip HTML-encoding. +
    + +

    Markdown Page Format

    + +

    + By contrast here is what the MarkdownPageFormat looks like which is able to use most of the default implementations: +

    + +{{ 'gfm/page-formats/02.md' |> githubMarkdown }} + +

    Registering a new PageFormat

    + +

    + To register a new Page Format you just need to add it to the ScriptContext's PageFormat collection: +

    + +{{ 'gfm/page-formats/03.md' |> githubMarkdown }} + +

    + Which now lets you resolve pages with a .md file extension who will use the behavior defined in MarkdownPageFormat. +

    + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/partials.html b/wwwroot/docs/partials.html new file mode 100644 index 0000000..f76c88e --- /dev/null +++ b/wwwroot/docs/partials.html @@ -0,0 +1,120 @@ + + +

    + Partials are just normal pages which contain reusable scripts you'd like to embed in different pages. + There is no difference between pages and partials other than how they're embedded where pages are embedded + in a _layout using the {{ pass: page }} expression and partials are embedded using the + partial block method which can also define scoped arguments on the call-site using an Object + literal: +

    + +{{ "live-pages" |> partial( + { + page: 'page', + files: + { + '_layout.html': `{{ 'from layout' |> to => layoutArg }} +I am a Layout with page +{{ page }}`, + 'page.html' : `I am a Page with a partial +{{ 'my-partial' |> partial({ arg: "from page" }) }}`, + 'my-partial.html' : `I am a partial called with the scoped argument {{ arg }} +Who can also access other arguments in scope {{ layoutArg }}` + } + }) +}} + +

    Select Partial

    + +

    + Another way that partials can be embedded is using the selectPartial block method which will re-evaluate + the same partial for each item in the collection which is made available in the it binding, e.g: +

    + +
    +
    +
    +
    customer.html
    + +
    +
    +
    order.html
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +

    Inline Partials

    + +

    + Partials can also be defined in-line with the {{pass: #partial}} block statement: +

    + +{{ 'live-template' |> partial({ rows:13, template: ` +{{#partial order}} + Order {{ it.OrderId }}: {{ it.OrderDate |> dateFormat }} +{{/partial}} +{{#partial customer}} +Customer {{ it.CustomerId }} {{ it.CompanyName |> raw }} +{{ it.Orders |> selectPartial: order }}{{/partial}} + +{{ customers |> where => it.Region == 'WA' |> to => waCustomers }} + Customers from Washington and their orders: + {{ waCustomers |> selectPartial: customer }}` }) }} + +
    + The linq 04 + example below shows how to implement this without partials using {{pass: #each}} statement blocks: +
    + +{{ 'live-template' |> partial({ rows:7, template: `Customers from Washington and their orders: +{{#each c in customers where c.Region == 'WA'}} +Customer {{ c.CustomerId }} {{ c.CompanyName |> raw }} +{{#each c.Orders}} + Order {{ OrderId }}: {{ OrderDate |> dateFormat }} +{{/each}} +{{/each}}` }) }} + + +

    Resolving Partials and Pages

    + +

    + Just like pages, partials are resolved from the ScriptContext's configured VirtualFiles sources + where partials in the root directory can be resolved without any specifying any folders: +

    + +
    {{ pass: 'my-partial' |> partial }}
    + +
    Cascading resolution
    + +

    + If an exact match isn't found it will look for the closest page with that name it can find, starting from the directory + where the page that contains the partial is located and traversing up until it reaches the root folder. +

    + +

    + Otherwise you can specify the virtual path to the partial from the Virtual Files root, e.g: +

    + +
    {{ pass: 'dir/subdir/my-partial' |> partial }}
    + +

    ownProps

    + +

    + Use ownProps to iterate all user-defined arguments specified for a partial, e.g: +

    + +{{ 'live-template' |> partial({ rows:2, template: `{{#partial test}} {{ it.ownProps() |> map => it.Key |> dump }} {{/partial}} +{{ 'test' |> partial({ A:1, B:2, C:3 }) }}` }) }} + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/protected-scripts.html b/wwwroot/docs/protected-scripts.html new file mode 100644 index 0000000..a2d3f1b --- /dev/null +++ b/wwwroot/docs/protected-scripts.html @@ -0,0 +1,163 @@ + + +

    + One of the goals of #Script is that its defaults should be safe enough to be able to execute arbitrary scripts by untrusted 3rd parties. + Given this constraint, only default scripts are pre-registered which contain a comprehensive set of filters we deem safe for use by anyone. + Other methods available in #Script which are useful to have in a server-generated website environment but we don't want 3rd Parties to + access are filters in + ProtectedScripts.cs. +

    + +

    + ProtectedScripts are not pre-registered when creating a new ScriptContext but they are pre-registered when + registering the SharpPagesFeature ServiceStack Plugin as that's designed to use #Script within a View Engine where access + to its context is limited to the server's Web Application. +

    + +

    + To access ProtectedScripts features within your own ScriptContext it can be + registered like any other filter: +

    + +{{ 'gfm/protected-scripts/01.md' |> githubMarkdown }} + +

    includeFile

    +
    fileContents
    + +

    + Use includeFile to embed content directly within pages: +

    + +{{ "live-pages" |> partial( + { + page: 'page', + files: + { + 'page.html' : `I am a Page with a file +{{ 'my-file.txt' |> includeFile }}`, + 'my-file.txt' : `I am a text file` + } + }) +}} + +
    Cascading resolution
    + +

    + If an exact match isn't found it will look for the closest file with that name it can find, starting from the directory + where the containing page that uses the filter is located and traversing up until it reaches the root folder. +

    + +

    includeFileWithCache

    +
    fileContentsWithCache
    + +

    + If your VirtualFiles is configured to use a combination + of various sources that includes a remote file service like + S3VirtualFiles, you'll + likely want to cache the contents in memory to ensure fast subsequent access the next time the file is requested, + which you can cache without expiration using includeFileWithCache without arguments: +

    + +
    {{ pass: 'my-file.txt' |> includeFileWithCache }}
    + +

    + In which case it will use the 1 minute default overridable in Args[DefaultFileCacheExpiry] + or if you want the content to be refreshed after 1hr you can use: +

    + +
    {{ pass(`'my-file.txt' |> includeFileWithCache({ expiresInSecs: 3600 })`) }}
    + +

    includeUrl

    +
    urlContents
    + +

    + You can also embed the contents of remote URLs in your page using includeUrl: +

    + +{{ "live-pages" |> partial( + { + page: 'page', + files: + { + 'page.html' : '{{ "https://raw.githubusercontent.com/ServiceStack/sharpscript/master/src" |> to => src }} +{{ `${src}/wwwroot/examples/email-template.txt` |> includeUrl }}' + } + }) +}} + +

    + includeUrl is actually a very flexible HTTP Client which can leverage the + URL Handling filters to easily construct urls and the additional + filter arguments to customize the HTTP Request that's sent, here are some examples: +

    + +

    + Accept JSON responses: +

    + +{{#raw}} +
    {{ url |> includeUrl({ accept: 'application/json' }) }}
    +
    {{ url |> includeUrl({ dataType: 'json' }) }}
    + +

    + Send data as form-urlencoded in a HTTP PUT Request with a #Script User-Agent: +

    + +
    {{ url |> includeUrl({ method:'PUT', data: { id: 1, name: 'foo' }, userAgent:"#Script" }) }}
    + +

    + Send data as JSON in a HTTP POST request and Accept JSON response: +

    + +
    {{ url |> includeUrl({ method: 'POST', data: { id: 1, name: 'foo' }, 
    +                       accept: 'application/json', contentType: 'application/json' }) }}
    + +

    + Shorter version of above request: +

    + +
    {{ url |> includeUrl({ method:'POST', data: { id: 1, name: 'foo' }, dataType: 'json' }) }}
    + +

    + Send data as CSV in a HTTP POST Request and Accept a CSV response: +

    + +
    includeUrl({ method:'POST', data: [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }], dataType:'csv' })
    + + +

    includeUrlWithCache

    +
    urlContentsWithCache
    + +

    + In the same way includeFileWithCache can cache file contents, includeUrlWithCache can cache URL content, + either without an expiration: +

    + + +
    {{ url |> includeUrlWithCache }}
    + +

    + In which case it will use the 1 minute default overridable in Args[DefaultUrlCacheExpiry] + or if you want to ensure that no more than 1 url request is made per hour for this url you can specify a custom expiry with: +

    + +
    {{ url |> includeUrlWithCache({ expiresInSecs: 3600 }) }}
    + +

    Virtual File System APIs

    + +

    + The Virtual File System APIs are mapped to the following methods: +

    +{{/raw}} + +{{ 'gfm/protected-scripts/02.md' |> githubMarkdown }} + +

    + See the Scripts API Reference for the + full list of ServiceStack scripts available. +

    + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/redis-scripts.html b/wwwroot/docs/redis-scripts.html new file mode 100644 index 0000000..7b0f595 --- /dev/null +++ b/wwwroot/docs/redis-scripts.html @@ -0,0 +1,160 @@ + + +

    + The Redis Scripts provide connectivity with a Redis Server instance using the + ServiceStack.Redis library. To enable, install + the ServiceStack.Redis NuGet Package, then + register a Redis Client Manager + in the IOC: +

    + +{{ 'gfm/redis-scripts/01.md' |> githubMarkdown }} + +

    + Then to enable in your scripts register + RedisScripts: +

    + +{{ 'gfm/redis-scripts/02.md' |> githubMarkdown }} + +

    + Your scripts are now able to use the available Redis Scripts. +

    + +

    redisCall

    + +

    + This is your main interface into Redis which utilizes the + Custom Commands API + to be able to execute any arbitrary Redis command. It can either be called with a string similar to commands sent + with redis-cli, e.g: +

    + +
    {{ pass: 'GET foo' |> redisCall }}
    + +

    + This works for most Redis commands but uses internal heuristics to determine where to split the command into the multiple arguments that + redis-server natively understands. A more deterministic usage which doesn't rely on any heuristics is to pass an array of arguments: +

    + +
    {{ pass: ['GET', 'foo'] |> redisCall }}
    + +

    + redisCall returns either a single object for redis commands returning single values or a List or nested List of + objects for Redis commands returning complex Responses. +

    + +

    redisInfo

    + +

    + Returns the Redis Server Info from Redis Info command in a String Dictionary. +

    + +

    redisSearchKeys

    + +

    + Provides an efficient API for searching Redis Keys by utilizing + Redis's non-blocking SCAN command, to fetch results and a server-side LUA script + to populate the results with metadata. Results are returned in a typed List<RedisSearchResult>: +

    + +{{ 'gfm/redis-scripts/03.md' |> githubMarkdown }} + +

    redisSearchKeysAsJson

    + +

    + If preferred, you can access the search results as JSON and parse it on the client instead. +

    + +

    redisConnection

    + +

    + Returns the current Connection Info in an Object Dictionary containing: {{ pass: host, port, db }} +

    + +

    redisToConnectionString

    + +

    + Converts an redisConnection Object Dictionary into a + Redis Connection String. +

    + +

    Redis App Examples

    + +

    + The Sharp Apps below utilize Redis Scripts for all their Redis functionality: +

    + +

    Redis Vue

    +

    + redis.web-app.io is a generic Redis Database Viewer that provides a human-friendly view + of any Redis data Type, including a dedicated UI to create and populate any Redis data type which just utilizes the above Redis Scripts + and a single Vue index.html App to power the Redis UI: +

    + +

    + .NET Core Redis Viewer +

    + +

    Redis HTML

    + +

    + redis-html.web-app.io is a version of Redis UI built using just #Script and Redis + methods where all functionality is maintained in a single index.html + weighing under <200 LOC including HTML and JavaScript. It's a good example of how the declarative style of programming + that #Script encourages a highly-readable code-base that packs a lot of functionality in a tiny foot print. +

    + +
    Server Generated HTML
    + +

    + It's not immediately obvious when running this locally since both #Script and Redis are super fast, but Redis HTML was developed as + a traditional Website where all HTML is server-generated and every search box key stroke and click on search results performs + a full-page reload. There's a slight sub-second delay that causes a noticeable flicker when hosted on the Internet due to network lag, but + otherwise server-generated #Script Websites can enable a highly responsive UI (especially in Intranets) with great SEO and deep-linking + and back-button support working as expected without the complexity of adopting a client-side JavaScript SPA framework and build-system. +

    + +

    Run against your local Redis instance

    + +

    + Since redis is published Gist Desktop App, + you can run the above redis SharpApp's against your local redis instance with: +

    + +
    app open redis
    + +

    + Using open always downloads and runs the latest version, if you don't have an Internet connection you can later run + any previous run Sharp Apps using run: +

    + +
    app run redis
    + +

    + As both are Sharp Apps it doesn't need any compilation so after + running the Redis Web App locally + you can modify the source code and see changes in real-time thanks to its built-in Hot Reloading support. +

    + +
    Populate redis with the Northwind database
    + +

    + You can use the + northwind-data + project to populate a Redis instance with Northwind test data by running: +

    + +
    dotnet run redis localhost:6379
    + +

    + See the Scripts API Reference for the + full list of Redis scripts available. +

    + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/sandbox.html b/wwwroot/docs/sandbox.html new file mode 100644 index 0000000..42b3578 --- /dev/null +++ b/wwwroot/docs/sandbox.html @@ -0,0 +1,144 @@ + + +

    + Another useful feature of #Script is that it operates within a controlled sandbox where each ScriptContext instance is + isolated and defines the entire execution environment on which scripts are executed within as such it should be safe to run + scripts from untrusted 3rd Party sources as they're confined to what's available within their allowed ScriptContext instance. +

    + +

    ScriptContext

    + +

    + By default the only functionality and objects #Script has access to is what's pre-configured within a new + ScriptContext instance which has access to + Default Scripts, HTML Scripts and + default Script Blocks. + #Script can't call methods on instances or have any other way to invoke a method unless it's explicitly registered. +

    + +

    + To give additional functionality to your Scripts extend the ScriptContext that executes your script with additional: +

    + + + + + + +

    Protected Scripts

    + +

    + When running in a trusted contexts like Server Scripts in #Script Pages, scripts have elevated + privileges with access to Protected Scripts, + ServiceStack and Info Scripts, + Bootstrap Scripts and + ServiceStack Blocks. +

    + +

    + Protected Scripts allows your Scripts to escape the default sandbox and create new instances + on existing .NET Types, populate them and call their methods as documented in Scripting .NET. + There are 2 levels of access available: +

    + +

    Script Assemblies and Types

    + +

    + You can limit which Types are scriptable by specifying just the individual Types: +

    + +{{ 'gfm/sandbox/04.md' |> githubMarkdown }} + +

    AllowScriptingOfAllTypes

    + +

    + To give your Scripts maximum accessibility, you can give them access to all .NET Types in loaded assemblies: +

    + +{{ 'gfm/sandbox/05.md' |> githubMarkdown }} + +

    + Use ScriptNamespaces to include additional Lookup namespaces for resolving Types similar to C# using statements. +

    + +

    + See Scripting .NET for how to access .NET Types in #Script. +

    + +

    Running untrusted Scripts

    + +

    + If running a script from an untrusted source we recommend running them within a new ScriptContext instance so they're + kept isolated from any other ScriptContext instance. Context's are cheap to create, so there won't be a noticeable delay when + executing in a new instance but they're used to cache compiled lambda expressions which will need to be recreated if executing script + in new ScriptContext instances. For improved performance you can instead have all untrusted templates use the same + ScriptContext instance that way they're able to reuse existing caches and compiled expressions. +

    + +

    Remove Default Scripts

    + +

    + If you want to start from a clean slate, the default scripts can be removed by clearing the ScriptMethods collection: +

    + +{{ 'gfm/sandbox/01.md' |> githubMarkdown }} + +

    Disabling adhoc Filters

    + +

    + Or if you only want to disable access to some filters without removing them all, you can disable access to adhoc filters + by adding to the ExcludeFiltersNamed collection: +

    + +{{ 'gfm/sandbox/02.md' |> githubMarkdown }} + +
    + Script Methods can also be disabled on an individual PageResult by populating its ExcludeFiltersNamed collection. +
    + +

    Instance creation and MaxQuota

    + +

    + The only instances that can be created within scripts are what's allowed in + JavaScript Literals and the + Generation and Repeating Filters. To limit any potential CPU and GC abuse any default scripts + that can generate instances are limited to a MaxQuota of 10000 iterations. This quota can be modified with: +

    + +{{ 'gfm/sandbox/03.md' |> githubMarkdown }} + +

    Max Stack Depth

    + +

    + To prevent user-defined functions from causing out of memory Exceptions its execution is limited to + a Maximum Stack Depth which can be configured with: +

    + +
    ScriptConfig.MaxStackDepth = 25; //default
    + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/script-net.html b/wwwroot/docs/script-net.html new file mode 100644 index 0000000..c341621 --- /dev/null +++ b/wwwroot/docs/script-net.html @@ -0,0 +1,84 @@ + + +{{#markdown}} + +The recommended way to add functionality to your scripts is by [extending your ScriptContext](/docs/introduction#net-usage) +with arguments, scripts, blocks and transformers - encapsulating it in a well-defined scope akin to +[Vue Components](https://vuejs.org/v2/guide/components.html) where all functionality is clearly defined in a componentized +design that promotes decoupling and reuse of testable components where the same environment can be easily simulated and +re-created to allow reuse of scripts in multiple contexts. + +> When scripting .NET Types or C# delegates directly it adds coupling to those Types making it harder to substitute and potentially +limits reuse where if the Types were defined in a Host project, it can prevent reuse in different Apps or Test projects, +and Types defined in .NET Framework-only or .NET Core-only dll's can prohibit platform portability + +Although being able to Script .NET Types directly gives your Scripts greater capabilities and opens it up to a lot more use-cases that's +especially useful in predominantly #Script-heavy contexts like [Sharp Apps](/docs/sharp-apps) and [Shell Scripts](/docs/sharp-scripts) +giving them maximum power that would otherwise require the usage of [Plugins](/docs/sharp-apps#plugins). + +We can visualize the different scriptability options from the diagram below where +by default scripts are limited to functionality [defined within their ScriptContext](/docs/introduction#net-usage), +whether to [limit access to specific Types and Assemblies](#script-assemblies-and-types) or whether to lift the escape hatch +and [allow scripting of any .NET Types](#allowscriptingofalltypes). + +![](/assets/img/sandbox.svg) + +Scripting of .NET Types is only available to Script's executed within **trusted contexts** +like [#Script Pages](/docs/sharp-pages), [Sharp Apps](/docs/sharp-apps) and [Shell Scripts](/docs/sharp-scripts) +that are registered with [Protected Scripts](/docs/protected-scripts). The different ways to allow scripting of .NET Types include: + +#### Script Assemblies and Types + +Using `ScriptTypes` to limit scriptability to **only specific Types**: + +{{/markdown}} + +{{ 'gfm/sandbox/04.md' |> githubMarkdown }} + +{{#markdown}} + +#### AllowScriptingOfAllTypes + +To give your Scripts maximum accessibility where they're able to pierce the well-defined [ScriptContext sandbox](/docs/sandbox), +you can set `AllowScriptingOfAllTypes` to allow scripting of **all .NET Types** available in loaded assemblies: + +{{/markdown}} + +{{ 'gfm/sandbox/05.md' |> githubMarkdown }} + +{{#markdown}} + +`ScriptNamespaces` is used to include additional Lookup namespaces for resolving Types akin to C# `using` statements. + +Using `AllowScriptingOfAllTypes` also allows access to both public and **non-public** Types. + +## Scripting .NET APIs + +The following [Protected Scripts](/docs/protected-scripts) are all that's needed to create new instances, call methods and +populate instances of .NET Types, including generic types and generic methods. + +{{/markdown}} + +{{ 'gfm/script-net/02.md' |> githubMarkdown }} + +

    Type Resolution

    + +{{ 'gfm/script-net/type-resolution.md' |> githubMarkdown }} + +

    Creating Instances

    + +{{ 'gfm/script-net/create-instances.md' |> githubMarkdown }} + +

    Calling Methods

    + +{{ 'gfm/script-net/call-methods.md' |> githubMarkdown }} + +

    Function

    + +{{ 'gfm/script-net/Function.md' |> githubMarkdown }} + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/script-pages.html b/wwwroot/docs/script-pages.html new file mode 100644 index 0000000..0a4a4e2 --- /dev/null +++ b/wwwroot/docs/script-pages.html @@ -0,0 +1,566 @@ + + +

    + One of the most popular use-cases for a high-performance and versatile scripting language like #Script is as a server-side + HTML #Script Pages for .NET Web Applications where it can provide a simpler, cleaner and portable alternative than Razor and + Razor Pages in ASP.NET and ASP.NET Core Web Apps. +

    + +

    #Script Pages in ServiceStack

    + +

    + The SharpPagesFeature plugin provides a first-class experience for generating dynamic websites where it's able to + generate complete server-generated websites (like this one) without requiring any additional Controllers or Services. +

    + +

    + To enable #Script Pages in ServiceStack you just need to register the SharpPagesFeature plugin: +

    + +{{ 'gfm/sharp-pages/01.md' |> githubMarkdown }} + +

    + SharpPagesFeature is a subclass of ScriptContext which defines the context on which all ServiceStack + #Script Pages are executed within. It provides deep integration within ServiceStack by replacing the ScriptContext's stand-alone + dependencies with ServiceStack AppHost providers, where it: +

    + +
      +
    • Configures it to use ServiceStack's Virtual File Sources + allowing Pages to be loaded from any configured VFS Source
    • +
    • Configures it to use ServiceStack's Funq IOC Container + so all ServiceStack dependencies are available to Code Pages
    • +
    • Configures it to use ServiceStack's AppSettings + so all AppHost AppSettings are available to #Script Pages as well
    • +
    • Configures ScanAssemblies to use AppHost Service Assemblies so it auto-registers all Filters in Service .dlls
    • +
    • Registers the ProtectedScripts allowing pages to access richer server-side functionality
    • +
    • Registers the markdown Filter Transformer using ServiceStack's built-in MarkdownDeep implementation
    • +
    • Makes the ServiceStackCodePage subclass available so Code Pages has access to same functionality as Services
    • +
    • Registers a Request Handler which enables all requests .html pages to be handled by #Script Pages
    • +
    + +

    If preferred, you can change which .html extension gets handled by #Script Pages with:

    + +{{ 'gfm/sharp-pages/02.md' |> githubMarkdown }} + +

    Register additional functionality

    + +

    + The same APIs for extending a ScriptContext is also how to extend + SharpPagesFeature to enable additional functionality in your #Script Pages: +

    + +{{ 'gfm/sharp-pages/14.md' |> githubMarkdown }} + +

    Runs Everywhere

    + +

    + The beauty of #Script working natively with ServiceStack is that it runs everywhere ServiceStack does + which is in all major .NET Server Platforms. That is, your same #Script-based Web Application is able to use + the same #Script implementation, "flavour" and feature-set and is portable across whichever platform you choose to host it on: +

    + + + +

    + Once registered, SharpPagesFeature gives all your .html pages scripting super powers where sections can be + compartmentalized and any duplicated content can now be extracted into reusable partials, metadata can be added to the top of + each page and its page navigation dynamically generated, contents of files and urls can be embedded directly and otherwise + static pages can come alive with access to Default Scripts. +

    + +

    + The Starter Projects below provide a quick way to get started with a pre-configured ServiceStack App with #Script Pages: +

    + +

    .NET Core #Script Pages Project

    + +

    + Create a new #Script Pages Website .NET 5.0 App with + x new: +

    + +
    +
    $ dotnet tool install --global x 
    +
    +$ x new script ProjectName
    + + +.NET Core Starter Template + +

    ASP.NET Core #Script Pages Project on .NET Framework

    + +

    + To create ASP.NET Core Project on the .NET Framework: +

    + +
    +
    $ x new script-corefx ProjectName
    + +

    ASP.NET v4.5 #Script Pages Project

    + +

    + For ASP.NET v4.5+ projects create a new ServiceStack ASP.NET #Script Pages with Bootstrap from the VS.NET Templates in + ServiceStackVS VS.NET Extension + to create an ASP.NET v4.5 Project using + ServiceStack's recommended project structure: +

    + +

    + + ASP.NET v4.5 Starter Template + +

    + + +

    Content Pages

    + +

    + There are a number of different ways you can use #Script to render dynamic pages, requests that calls and renders + #Script .html pages directly are called "Content Pages" which don't use any Services or Controllers + and can be called using a number of different styles and calling conventions: +

    + +

    Page Based Routing

    + +

    + Any .html page available from your AppHost's configured Virtual File Sources + can be called directly, typically this would mean the File System which in a .NET Core Web App starts from the WebRootPath + (e.g /wwwroot) so a request to /docs/sharp-pages goes through all configured VirtualFileSources to find the first + match, which for this website is the file + /src/wwwroot/docs/sharp-pages.html. +

    + +

    Pretty URLs by default

    + +

    + Essentially #Script Pages embraces conventional page-based routing which enables pretty urls inferred from the pages file and directory names + where each page can be requested with or without its .html extension: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    pathpage
    /db 
    /db.html/db.html
    /posts/new 
    /posts/new.html/posts/new.html
    + +

    + The default route / maps to the index.html in the directory if it exists, e.g: +

    + + + + + + + + + + + + + + + + + + +
    pathpage
    //index.html
    /index.html/index.html
    + +

    Dynamic Page Routes

    + +

    + In addition to these static conventions, #Script Pages now supports Nuxt-like + Dynamic Routes + where any file or directory names prefixed with an _underscore enables a wildcard path which assigns the matching path component + to the arguments name: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    pathpagearguments
    /ServiceStack/_user/index.htmluser=ServiceStack
    /posts/markdown-example/posts/_slug/index.htmlslug=markdown-example
    /posts/markdown-example/edit/posts/_slug/edit.htmlslug=markdown-example
    + +

    Layout and partial recommended naming conventions

    + +

    + The _underscore prefix for declaring wildcard pages is also what is used to declare "hidden" pages, to distinguish them from hidden + partials and layouts, the recommendation is for them to include layout and partial their name, e,g: +

    + +
      +
    • _layout.html
    • +
    • _alt-layout.html
    • +
    • _menu-partial.html
    • +
    + +
    + Pages with layout or partial in their name remain hidden and are ignored in wildcard path resolution. +
    + +

    + If following the recommended _{name}-partial.html naming convention, you'll be able to reference them using just their name: +

    + +{{#raw}} +
    +{{ 'menu' |> partial }}          // Equivalent to:
    +{{ '_menu-partial' |> partial }}
    +
    +{{/raw}} + +

    View Pages

    + +

    + "View Pages" are pages that are rendered after a Service is executed, where it's typically used to provide the "HTML UI" + for the Service's Response DTO where it's populated in the Model page argument + as done in Razor ViewPages. +

    + +

    + View Pages can be in any nested folder within the /Views folder but all page names within the /Views folder need to be unique. + The name should use the format using the format {PageName}.html where PageName can be either: +

    + +
      +
    • Request DTO Name (e.g. GetContact)
    • +
    • Response DTO Name (e.g. GetContactResponse)
    • +
    + +

    + There are a number of other ways to specify which View you want to render: +

    + +

    Specify the Services DefaultView

    + +

    + You can specify which view all Services should use to render their responses by using the [DefaultView] Request Filter Attribute: +

    + +{{ 'gfm/sharp-pages/12.md' |> githubMarkdown }} + +

    Specify View with custom HttpResult

    + +

    + Just like ServiceStack.Razor, you can specify to use different Views or Layouts by returning a + decorated response in custom HttpResult with the View or Template you want the Service rendered in , e.g: +

    + +{{ 'gfm/sharp-pages/09.md' |> githubMarkdown }} + +

    Return Custom PageResult

    + +

    + For maximum flexibility to control how views are rendered you can return a custom PageResult using + Request.GetPage() or Request.GetCodePage() extension methods as seen in + Model View Controller: +

    + +{{ 'gfm/model-view-controller/02.md' |> githubMarkdown }} + +

    Allow Views to be specified on the QueryString

    + +

    + You can use the [ClientCanSwapTemplates] Request Filter attribute to let the View and Template by specified on the QueryString, + e.g: ?View=CustomPage&Template=_custom-layout +

    + +{{ 'gfm/sharp-pages/13.md' |> githubMarkdown }} + +

    + Additional examples of dynamically specifying the View and Template are available in + SharpViewsTests.cs. +

    + +

    Cascading Layouts

    + +

    + One difference from Razor is that it uses a cascading _layout.html instead of /Views/Shared/_Layout.cshtml. +

    + +

    So if your view page was in:

    + +
    +
    +/Views/dir/MyRequest.html
    +
    +
    + +

    It will use the closest `_layout.html` it can find starting from:

    + +
    +
    +/Views/dir/_layout.html
    +/Views/_layout.html
    +/_layout.html
    +
    +
    + +

    Layout Selection

    + +

    + Unless it's a complete HTML Page (e.g. starts with html or HTML5 tag) the page gets rendered using the closest _layout.html + page it can find starting from the directory where the page is located, traversing all the way up until it reaches the root directory. + Which for this page uses the + /src/wwwroot/_layout.html template + in the WebRoot directory, which as it's in the root directory, is the fallback Layout for all .html pages. +

    + +

    + Pages can change the layout they use by either adding their own _layout.html page in their sub directory or specifying + a different layout in their pages metadata header, e.g: +

    + +{{ 'gfm/sharp-pages/03.md' |> githubMarkdown }} + +

    + Where it instead embed the page using the closest mobile-layout.html it can find, starting from the Page's directory. + If your pages are instead embedded in the different folder you can request it directly from the root dir: +

    + +{{ 'gfm/sharp-pages/04.md' |> githubMarkdown }} + +

    Request Variables

    + +

    + The QueryString and FORM variables sent to the page are available as arguments within the page using the + form and query (or its shorter qs alias) collections, so a request like + /docs/sharp-pages?id=1 + can access the id param with {{ pass: qs.id }}. The combined {{pass: 'id' | formQuery }} + method enables the popular use-case of checking for the param in POST FormData before falling back to + the QueryString. Use {{pass: 'ids' | formQueryValues }} for accessing multiple values sent by multiple checkboxes + or multiple select inputs. The {{pass: 'id' | httpParam }} method searches all Request params including HTTP Headers, QueryString, + FormData, Cookies and Request.Items. +

    + +

    + To help with generating navigation, the following Request Variables are also available: +

    + +
      +
    • {{ pass: Verb }} evaluates to {{ Verb }}
    • +
    • {{ pass: AbsoluteUri }} evaluates to {{ AbsoluteUri }}
    • +
    • {{ pass: RawUrl }} evaluates to {{ RawUrl }}
    • +
    • {{ pass: PathInfo }} evaluates to {{ PathInfo }}
    • +
    + +

    + You can use {{ pass: PathInfo }} to easily highlight the active link in a links menu as done in + sidebar.html: +

    + +{{ 'gfm/sharp-pages/05.md' |> githubMarkdown }} + +

    Init Pages

    + +

    + Just as how Global.asax.cs can be used to run Startup initialization logic in ASP.NET Web Applications and + Startup.cs in .NET Core Apps, you can now add a /_init.html page for any script logic that's only executed once on Startup. +

    + +

    + This is used in the Blog Web App's _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: +

    + +{{ 'gfm/sharp-pages/10.md' |> githubMarkdown }} + +

    Ignoring Pages

    + +

    + You can ignore #Script from evaluating static .html files with the following page arguments: +

    + +{{ 'gfm/sharp-pages/06.md' |> githubMarkdown }} + +{{ 'gfm/sharp-pages/07.md' |> githubMarkdown }} + +{{ 'gfm/sharp-pages/08.md' |> githubMarkdown }} + +
    + Complete .html pages starting with <!DOCTYPE HTML> or <html have their layouts ignored by default. +
    + +

    Server UI Controls

    + +{{ 'gfm/sharp-pages/15.md' |> githubMarkdown }} + +

    Admin Service

    + +

    + The new Admin Service lets you run admin actions against a running instance which by default is only accessible to Admin + users and can be called with: +

    + +
    /script/admin
    + +

    + Which will display the available actions which are currently only: +

    + +
      +
    • invalidateAllCaches - Invalidate all caches and force pages to check if they've been modified on next request
    • +
    • RunInitPage - Runs the Init page again
    • +
    + +

    Zero downtime deployments

    + +

    + These actions are useful after an xcopy/rsync deployment to enable zero downtime deployments by getting a running instance to invalidate all + internal caches and force existing pages to check if it has been modified, the next time their called. +

    + +

    + Actions can be invoked in the format with: +

    + +
    /script/admin/{Actions}
    + +

    + Which can be used to call 1 or more actions: +

    + +
    /script/admin/invalidateAllCaches
    +/script/admin/invalidateAllCaches,RunInitPage
    + +

    + By default it's only available to be called by **Admin** Users (or AuthSecret) + but can be changed with: +

    + +{{ 'gfm/sharp-pages/11.md' |> githubMarkdown }} + +

    ServiceStack Scripts

    + +

    + Methods for integrating with ServiceStack are available in + ServiceStack Scripts and Info Scripts both + of which are pre-registered when registering the SharpPagesFeature Plugin. +

    + +

    Markdown Support

    + +

    + Markdown is a great way to maintain and embed content of which #Script has great support for, which can be rendered using the + markdown filter to render markdown text to HTML: +

    + +
    {{#raw}}{{ `## Heading` |> markdown }}{{/raw}}
    + +

    + This can be combined with other composable filters like includeFile to easily and + efficiently render markdown content maintained in a separate file: +

    + +
    {{#raw}}{{ 'doc.md' |> includeFile |> markdown }}{{/raw}}
    + +

    + Or you could use includeUrlWithCache to efficiently render markdown from an + external URL: +

    + +
    {{#raw}}{{ url |> includeUrlWithCache |> markdown }}{{/raw}}
    + +

    + A popular way for embedding static markdown content inside a page is to use the markdown script block + which this website makes extensive use of: +

    + +
    {{#raw}}
    +{{#markdown}}
    +### Heading
    +> Static Markdown Example
    +{{/markdown }}
    +{{/raw}}
    + +

    + Providing an easy way for mixing in markdown content inside a dynamic web page. To embed dynamically rendered markdown content you can use + the capture script block to capture dynamic markdown that you can render with the markdown filter: +

    + +
    {{#raw}}{{#capture md}}
    +### Dynamic Markdown Example
    +{{#each i in range(1,5)}}
    +  - {{i}}
    +{{/each}}
    +{{/capture}}
    +{{ md |> markdown }}
    +{{/raw}}
    + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/script-plugins.html b/wwwroot/docs/script-plugins.html new file mode 100644 index 0000000..a4cb936 --- /dev/null +++ b/wwwroot/docs/script-plugins.html @@ -0,0 +1,8 @@ + + +{{ 'gfm/script-plugins/01.md' |> githubMarkdown }} + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/scripts-reference.html b/wwwroot/docs/scripts-reference.html new file mode 100644 index 0000000..38e4361 --- /dev/null +++ b/wwwroot/docs/scripts-reference.html @@ -0,0 +1,165 @@ + + +{{ 'nameContains,tab' |> importRequestParams }} + +
    +
    +
    +
    + + + + +
    +
    +
    +
    + + + +{{#raw appendTo scripts}} + +{{/raw}} + + + +{{ var rows = 6 }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'DefaultScripts' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'HtmlScripts' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'ProtectedScripts' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'InfoScripts' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'RedisScripts' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'DbScriptsAsync' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'ValidateScripts' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'AutoQueryScripts' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    +{{ "live-template" |> partial({ rows, className, template:`{{ 'ServiceStackScripts' |> to => method }} +{{ method |> methodsAvailable |> where => it.Name.lower().contains((nameContains ?? '').lower()) + |> to => methods }} + +{{#each methods}}{{/each}} +
    {{ method |> methodLinkToSrc }}
    {{FirstParam}}{{Body}}{{Return}}
    ` }) }} +
    +
    + + +{{ "doc-links" |> partial({ order }) }} \ No newline at end of file diff --git a/wwwroot/docs/servicestack-scripts.html b/wwwroot/docs/servicestack-scripts.html new file mode 100644 index 0000000..afeb6a2 --- /dev/null +++ b/wwwroot/docs/servicestack-scripts.html @@ -0,0 +1,391 @@ + + +

    + The ServiceStack Scripts provide integration with ServiceStack features that are already pre-registered in ServiceStack's + SharpPagesFeature which are implemented in + ServiceStackScripts. +

    + +
    + See Info Scripts for accessing the currently authenticated user. +
    + + + +

    sendToGateway

    + +

    + sendToGateway lets you easily call any ServiceStack Service available through its + Service Gateway which allows the same API to transparently + call an In-Process or Remote Service. +

    + +

    + The example below calls this QueryCustomers + AutoQuery RDBMS Service, its entire implementation is below: +

    + +{{ 'gfm/servicestack-scripts/01.md' |> githubMarkdown }} + +{{ 'examples/sendtogateway-customers.html' |> includeFile |> to => template }} +{{ "live-template" |> partial({ rows:11, template }) }} + +

    execService

    + +

    + execService is a readable alternative to calling a service via sendToGateway without arguments, e.g: +

    + +
    {{#raw}}AUTH = {{ 'Authenticate' |> execService({ ifErrorReturn: "null" }) |> json }};
    +
    +AUTH = {{ {} |> sendToGateway('Authenticate', { ifErrorReturn: "null" }) |> json }};{{/raw}}
    + +

    publishToGateway

    + +

    + publishToGateway is for sending OneWay requests with side-effects to IReturnVoid Services, e.g: +

    + +{{ 'gfm/servicestack-scripts/02.md' |> githubMarkdown }} + +

    publishMessage

    + +

    + publishMessage is used for sending Request DTOs to be processed by the configured MQ Server: +

    + +{{ 'gfm/servicestack-scripts/09.md' |> githubMarkdown }} + +

    sendToAutoQuery

    + +

    + The sendToAutoQuery method makes requests directly against the AutoQuery API. + The ServiceStackScripts only supports calling + AutoQuery Data Services as it's implementation is contained + within the ServiceStack NuGet package. +

    + +

    + AutoQuery Data + is an open querying abstraction that supports multiple pluggable back-ends that enables rich querying of + In Memory collections, + results from executed ServiceStack Services as well as + AWS Dynamo DB data stores. It also maintains the equivalent + external API and wire-format as AutoQuery RDBMS Services + which is how AutoQuery Viewer is able to transparently support + building custom queries for any AutoQuery Service. +

    + +

    GitHub AutoQuery Data Example

    + +

    + For this example we'll register a ServiceSource which will call the + GetGithubRepos Service implementation + for any AutoQuery Data DTOs that query GithubRepo data sources: +

    + +{{ 'gfm/servicestack-scripts/03.md' |> githubMarkdown }} + +

    + This registration also specifies to cache the response of the GetGithubRepos Service in the registered + Caching Provider and operate on the cached data set for up to + 10 minutes to mitigate GitHub API's rate-limiting. All that's remaining is to create the QueryGitHubRepos + Service by defining the Request DTO below and implement the backing + GetGithubRepos Service + it calls which combines a number of GitHub API calls to fetch all Repo's for a GitHub User or Organization: +

    + +{{ 'gfm/servicestack-scripts/04.md' |> githubMarkdown }} + +

    + That's all that's required to be able to query GitHub's User and Organization APIs, since they're just normal ServiceStack Services + we could've used sendToAutoQuery to call QueryGitHubRepos but it would be limited to only being able to call properties + explicitly defined on the Request DTO, whereas sendToAutoQuery executes against the IAutoQueryData API which + enables access to all Implicit Conventions + and other Querying related functionality: +

    + +{{ 'examples/sendToAutoQuery-data.html' |> includeFile |> to => template }} +{{ "live-template" |> partial({ rows:6, template }) }} + +

    AutoQuery RDBMS

    + +

    + AutoQuery RDBMS is implemented in the + ServiceStack.Server NuGet package + which you'll need to install along with the + OrmLite NuGet package for your RDBMS + which can then be registered in the IOC with: +

    + +{{ 'gfm/servicestack-scripts/05.md' |> githubMarkdown }} + +

    + AutoQuery can then be enabled by registering the AutoQueryFeature plugin: +

    + +{{ 'gfm/servicestack-scripts/06.md' |> githubMarkdown }} + +

    + Which will let you start developing AutoQuery Services. + To then let your Scripts call AutoQuery Services directly, register the AutoQueryScripts: +

    + +{{ 'gfm/servicestack-scripts/07.md' |> githubMarkdown }} + +

    sendToAutoQuery

    + +

    + As they share the same semantics and wire-format, you can use the same sendToAutoQuery method name to call + either AutoQuery Data or AutoQuery RDBMS Services which automatically gets routed to the appropriate implementation. + This also means that you can replace your implementation to from AutoQuery Data to RDBMS and vice-versa behind the + scenes and your scripts will continue to work untouched. +

    + +

    + For this example we'll re-use the same QueryCustomers AutoQuery Implementation that the + sendToGateway uses: +

    + +{{ 'gfm/servicestack-scripts/08.md' |> githubMarkdown }} + +

    + But instead of being limited by explicit properties on the Request DTO + sendToAutoQuery extends the queryability of AutoQuery Services to enable querying all + implicit conventions as well: +

    + +{{ 'examples/sendToAutoQuery-rdms.html' |> includeFile |> to => template }} +{{ "live-template" |> partial({ rows:6, template }) }} + +

    + See the Scripts API Reference for the + full list of ServiceStack Scripts available. +

    + +

    Info Scripts

    + + +

    + The debug info methods provide an easy to inspect the state of a remote ServiceStack Instance by making a number of metadata + objects and APIs available to query. The + InfoScripts + are pre-registered in ServiceStack's SharpPagesFeature. You can make them available in a ScriptContext with: +

    + +{{ 'gfm/info-scripts/01.md' |> githubMarkdown }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Method NameAPI Mapping
    Environment APIs
    envVariableEnvironment.GetEnvironmentVariable(variable)
    envExpandVariablesEnvironment.ExpandEnvironmentVariables(name)
    envProcessorCountEnvironment.ProcessorCount
    envTickCountEnvironment.TickCount
    envServerUserAgentEnv.ServerUserAgent
    envServiceStackVersionEnv.ServiceStackVersion
    envIsWindows (isWin)Env.IsWindows (net45) / IsOSPlatform(Windows) (netcore)
    envIsLinuxEnv.IsLinux (net45) / IsOSPlatform(Linux) (netcore)
    envIsOSXEnv.IsOSX (net45) / IsOSPlatform(OSX) (netcore)
    networkIpv4AddressesGetAllNetworkInterfaceIpv4Addresses()
    networkIpv6AddressesGetAllNetworkInterfaceIpv6Addresses()
    ServiceStack User APIs
    userSessionIRequest.GetSession()
    userSessionIdIRequest.GetSessionId()
    userAuthIdIRequest.GetSession().UserAuthId
    userAuthNameIRequest.GetSession().UserAuthName
    userSessionIRequest.GetSession()
    userPermanentSessionIdIRequest.GetPermanentSessionId()
    userSessionOptionsIRequest.GetSessionOptions()
    userHasRoleIAuthSession.HasRole(role)
    userHasPermissionIAuthSession.HasPermission(permission)
    ServiceStack Metadata APIs
    metaAllDtosMetadata.GetAllDtos()
    metaAllDtoNamesMetadata.GetOperationDtos(x => x.Name)
    metaAllOperationsMetadata.Operations
    metaAllOperationNamesMetadata.GetAllOperationNames()
    metaAllOperationTypesMetadata.GetAllOperationTypes()
    metaOperationMetadata.GetOperation(name)
    + +
    + +

    /metadata/debug

    +

    Debug Inspector

    + + + Metadata Debug Inspectors Screenshot + + +

    + The Debug Inspector is a Service in SharpPagesFeature that's pre-registered in DebugMode. + The Service can also be available when not in DebugMode by enabling it with: +

    + +{{ 'gfm/info-scripts/02.md' |> githubMarkdown }} + +

    + This registers the Service but limits it to Users with the Admin role, alternatively you configure an + Admin Secret Key: +

    + +{{ 'gfm/info-scripts/03.md' |> githubMarkdown }} + +

    + Which will let you access it by appending the authsecret to the querystring: + /metadata/debug?authsecret=secret +

    + +

    + Alternatively if preferred you can make the Debug Inspector Service available to all users with: +

    + +{{ 'gfm/info-scripts/04.md' |> githubMarkdown }} + +

    + Which is the configuration used in this .NET Core App which makes the Debug Inspector Service accessible to everyone: +

    + +

    /metadata/debug

    + +

    + Debug Inspectors are executed in a new ScriptContext instance pre-configured with the above InfoScripts + and a copy of any context arguments in SharpPagesFeature which is the only thing it can access from the plugin. +

    + +

    + In addition to SharpPagesFeature arguments, they're also populated with the following additional arguments: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    requestThe current IHttpRequest
    appHostThe ServiceStack AppHost Instance
    appConfigServiceStack's AppHost Configuration
    appVirtualFilesPathThe Read/Write IVirtualFiles (aka ContentRootPath)
    appVirtualFileSourcesPathMultiple read-only Virtual File Sources (aka WebRootPath)
    metaServiceStack Metadata Services Object
    + +

    + See the Scripts API Reference for the + full list of Info scripts available. +

    + + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/sharp-apis.html b/wwwroot/docs/sharp-apis.html new file mode 100644 index 0000000..419abbd --- /dev/null +++ b/wwwroot/docs/sharp-apis.html @@ -0,0 +1,213 @@ + + +

    + In addition to being productive high-level .NET scripting language for generating dynamic HTML pages, #Script can also + be used to rapidly develop Web APIs which can take advantage of the new support for + Dynamic Page Based Routes + to rapidly develop data-driven JSON APIs and make them available under the ideal "pretty" URLs whilst utilizing the same Live Development workflow + that doesn't need to define any C# Types or execute any builds - as all development can happen in real-time whilst the App is running, + enabling the fastest way to develop Web APIs in .NET! +

    + +

    Dynamic Sharp APIs

    + +

    + The only difference between a Sharp Page that generates HTML or a Sharp Page that returns an API Response is that Sharp APIs + return a value using the return method. +

    + +

    + For comparison, to create a Hello World C# ServiceStack Service you would typically create a Request DTO, Response DTO and a Service implementation: +

    + +{{ 'gfm/sharp-apis/04.md' |> githubMarkdown }} + +

    Dedicated Sharp APIs

    + +

    + Dedicated Sharp APIs lets you specify a path where your "Sharp API" are located when registering SharpPagesFeature: +

    + +{{ 'gfm/sharp-apis/01.md' |> githubMarkdown }} + +

    + All pages within the /api folder are also treated like "Sharp API" for creating Web APIs where instead of writing their response to the + Output Stream, their return value is serialized in the requested Content-Type using the return method: +

    + +
    {{#raw}}{{ response |> return }}
    +{{ response |> return({ ... }) }}
    +{{ httpResult({ ... }) |> return }}
    +{{/raw}}
    + +

    + The route for the dedicated API page starts the same as the filename and one advantage over Dynamic Sharp APIs above is that a single Page + can handle multiple requests with different routes, e.g: +

    + +
    +/api/customers                // PathArgs = []
    +/api/customers/1              // PathArgs = ['1']
    +/api/customers/by-name/Name   // PathArgs = ['by-name','Name']
    +
    + +

    API Page Examples

    + +

    + To demonstrate Sharp APIs in action we've added Web APIs equivalents for Rockwind's + customers and + products HTML pages with the implementation below: +

    + + + +

    /api/customers

    + +

    + The entire implementation of the customers API is below: +

    + +{{ 'gfm/sharp-apis/02.md' |> githubMarkdown }} + +

    + These are some of the API's that are made available with the above implementation: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    /customers API
    All Customers + + + + +
    Accept HTTP Header also supported
    +
    Alfreds Futterkiste Details + + + +
    As List + + + +
    Customers in Germany + + + +
    Customers in London + + + +
    Combination Query + /api/customers?city=London&country=UK&limit=3 +
    + +

    /api/products

    + +

    + The Products API is an example of a more complex API where data is sourced from multiple tables: +

    + +{{ 'gfm/sharp-apis/03.md' |> githubMarkdown }} + +

    + Some API examples using the above implementation: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    /products API
    All Products + + + +
    Chai Product Details + + + +
    As List + + + +
    Beverage Products + + + +
    Bigfoot Breweries Products + + + +
    Products containing Tofu + + + +
    + +

    Untyped APIs

    + +

    + As these APIs don't have a Typed Schema they don't benefit from any of ServiceStack's metadata Services, i.e. + they're not listed in Metadata pages, included in + Open API or have Typed APIs generated using + Add ServiceStack Reference. +

    + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/sharp-apps.html b/wwwroot/docs/sharp-apps.html new file mode 100644 index 0000000..024b307 --- /dev/null +++ b/wwwroot/docs/sharp-apps.html @@ -0,0 +1 @@ +{{ httpResult({ status: 301, Location:'/sharp-apps/' }) |> return }} \ No newline at end of file diff --git a/wwwroot/docs/sharp-pages.html b/wwwroot/docs/sharp-pages.html new file mode 100644 index 0000000..3cd1834 --- /dev/null +++ b/wwwroot/docs/sharp-pages.html @@ -0,0 +1 @@ +{{ httpResult({ status: 301, Location:'/docs/script-pages' }) |> return }} \ No newline at end of file diff --git a/wwwroot/docs/sharp-scripts.html b/wwwroot/docs/sharp-scripts.html new file mode 100644 index 0000000..1d58e8b --- /dev/null +++ b/wwwroot/docs/sharp-scripts.html @@ -0,0 +1,304 @@ + + +{{#markdown}} +Unlike most languages, `#Script` has 2 outputs, the side-effect of the script itself and its textual output which makes it ideal for +literate programming where executable snippets can be embedded inside an executable document. + +[code blocks](/docs/syntax#code-blocks) are a convenient markup for embedding executable scripts within a document without the distracting boilerplate +of wrapping each statement in expression blocks. Together with `x watch`, `#Script` allows for an iterative, exploratory style of programming +in a live programming environment that benefits many domains whose instant progressive feedback unleashes an unprecedented amount of +productivity, one of those areas it benefits is shell scripting where the iterative feedback is invaluable in working towards a solution +for automating the desired task. + +Some [protected script methods](/docs/protected-scripts) used to improve Usage in shell scripts, include: + + - `string proc(string exeFileName, {arguments:string, dir:string})` + - Execute a local system process + - **arguments**: command line args + - **dir**: working directory + - `sh(cmdArgs, {dir:string})` + - Executes shell commands. Uses `cmd.exe /C {cmdArgs}` in Windows, otherwise `/bin/bash -c {cmdArgs}` on macOS/Linux + - `string exePath(string exeName)` + - Returns the full path to an executable located in the users **$PATH** + - `string osPaths(string path)` + - Rewrite paths to use `\` for Windows, otherwise uses `/` + +From this list, you're likely to use `sh` the most to execute arbitrary commands with: + + cmd |> sh + +> As with any `#Script` method, it can also be executed using the less natural forms of `cmd.sh()` or `sh(cmd)` + +Which will emit the **StandardOutput** of the command if successful, otherwise it throws an Exception if anything was written to +the **StandardError**. Checkout the [Error Handling docs](https://sharpscript.net/docs/error-handling) for how to handle exceptions. + +With the features in this release, `#Script` has now became our preferred way to create x-plat shell scripts which can be run +with the Windows [app](https://docs.servicestack.net/netcore-windows-desktop) or x-plat [x](https://docs.servicestack.net/dotnet-tool) dotnet tool which runs everywhere .NET Core does. + +As such, all scripts in [ServiceStack/mix](https://github.com/ServiceStack/mix) provides a good collection of `.ss` scripts to check out +which are used to maintain the Gist content that powers the new `mix` feature. + +#### Live Shell Scripting Preview + +To provide a glimpse of the productivity of this real-time exploratory programming-style, here's an example +of a task we needed to automate to import and rename **48px** SVG images from [Material Design Icons Project](https://github.com/google/material-design-icons) folders into the form needed in the `mix` Gists: + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/rename-icons.gif)](https://youtu.be/joXSHtfb_7g) + +> YouTube: [youtu.be/joXSHtfb_7g](https://youtu.be/joXSHtfb_7g) + +Here we can see how we're able to progressively arrive at our solution without leaving the source code, with the **watched** script +automatically updated at each `Ctrl+S` save point where we're able to easily infer its behavior from its descriptive textual output. + +#### Explore HTTP APIs in real-time + +The real-time scriptability of `#Script` makes it ideal for a whole host of Exploratory programming tasks that would otherwise be painful +in the slow code/build/debug cycles of a compiled language like C#, like being able to cut, sort & slice HTTP APIs with ease: + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/github-api.gif)](https://youtu.be/Yjx_9Tp91bQ) + +> YouTube: [youtu.be/Yjx_9Tp91bQ](https://youtu.be/Yjx_9Tp91bQ) + +#### Live Querying of Databases + +It also serves a great tool for data exploration, akin to a programmable SQL Studio environment with instant feedback: + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/query-northwind.gif)](https://youtu.be/HCjxVJ8RyPc) + +> YouTube: [youtu.be/HCjxVJ8RyPc](https://youtu.be/HCjxVJ8RyPc) + +That benefits from being able to maintain reusable queries in simple, executable text files that can be organized along with other +shell scripts and treated like source code where it can be checked into a versionable repo, that anyone will be able to checkout and run +from the command-line in Windows, macOS or Linux OS's. + +> Refer to [Sharp App Docs](/docs/sharp-apps#multi-platform-configurations) for different `db.connection` examples of supported RDBMS's. + +#### Utilize high-level ServiceStack Features + +Here's another example showing the expressive power of `#Script` and its [comprehensive high-level library](https://sharpscript.net/docs/scripts-reference) +which is used to update all library dependencies of the [Vue and React "lite" Project Templates](/templates-lite): + +{{/markdown}} + +{{ 'gfm/sharp-scripts/12.md' | githubMarkdown | convertScriptToCodeBlocks }} + +{{#markdown}} + +Running without any arguments: + + $ x run libraries.ss + +will update both React and Vue dependencies: + + Writing to 26 files to react-lite-lib 'ad42adc11337c243ee203f9e9f84622c' ... + Writing to 41 files to vue-lite-lib '717258cd4c26ba612e5eed0615d8d61c' ... + +Alternatively it can be limited to updating a single Framework dependencies with: + + $ x run libraries.ss vue-lite-lib + +The [web dotnet tool](https://docs.servicestack.net/web-new) also includes the capability of both **executing** `#Script` scripts as well +as **watching** scripts to enable a [live visual REPL](#live-script-with-web-watch) with instant real-time feedback that makes it perfect +for Exploratory tasks. + +### Bundling and Minification + +The Vue and React "lite" project templates take advantage of this in their +[Pre-compiled minified production _bundle.ss script](https://docs.servicestack.net/templates-lite#pre-compiled-minified-production-bundles) +which is run with `x run {script}`: +{{/markdown}} + +{{ 'gfm/sharp-scripts/10.md' | githubMarkdown | convertScriptToCodeBlocks }} + +{{#markdown}} +Which can be run with the `x` tool: + + $ dotnet tool install --global x + + $ x run wwwroot/_bundle.ss + +Which will create the production bundles, minify all already non-minified bundles and write them to disk with the paths written visible in the +`#Script` *_bundle.ss* output: +{{/markdown}} + +{{ 'gfm/sharp-scripts/11.md' |> githubMarkdown }} + +{{#markdown}} +#### Sharp Scripts run in context of Sharp Apps + +Sharp Scripts are **run in the same context** and have access to the same functionality and features as a [Sharp App](/docs/sharp-apps) +including extensibility va [custom plugins](/docs/sharp-apps#plugins). +They can run **stand-alone** independent of an +[app.settings](/docs/sharp-apps#ideal-for-web-designers-and-content-authors) config file, instead the app settings configuration +can be added in its page arguments to enable or configure any features. + +Lets go through a couple of different possibilities we can do with scripts: + +### AWS Dashboards + +The [comprehensive built-in scripts](/docs/default-scripts) coupled with ServiceStack's agnostic +providers like the [Virtual File System](/virtual-file-system) makes it easy to quickly query infrastructure resources +like all Tables and Row counts in managed AWS RDS Instances or Search for static Asset resources in S3 Buckets. +{{/markdown}} + +{{ 'gfm/sharp-scripts/03.md' | githubMarkdown | convertScriptToCodeBlocks }} + +{{#markdown}} +You can use `$NAME` to move confidential information out of public scripts where it will be replaced with Environment +Variables. Then run the script as normal and optionally provide a pattern for files you want to search for: + + $ x run script-aws.ss *.jpg + +Where it displays a dashboard of activity from your AWS resources: containing all Tables with their Row Counts, +adhoc queries like your last 5 Orders, The Root files and Folders available in your S3 Bucket and any matching resources +from your specified search pattern: + + Querying AWS... + + | Tables || + |--------------------|------| + | Order Detail | 2155 | + | Order | 830 | + | Customer | 91 | + | Product | 77 | + | Territory | 53 | + | Region | 0 | + | Shipper | 0 | + | Supplier | 0 | + | Category | 0 | + | Employee | 0 | + | Employee Territory | 0 | + + Last 5 Orders: + + | # | Id | CustomerId | EmployeeId | Freight | + |---|-------|------------|------------|---------| + | 1 | 11077 | RATTC | 1 | $8.53 | + | 2 | 11076 | BONAP | 4 | $38.28 | + | 3 | 11075 | RICSU | 8 | $6.19 | + | 4 | 11074 | SIMOB | 7 | $18.44 | + | 5 | 11073 | PERIC | 2 | $24.95 | + + + | Root Files + Folders | + |------------------------| + | api/ | + | northwind/ | + | rockstars/ | + | index.html | + | web.aws.settings | + | web.postgres.settings | + | web.sqlite.settings | + | web.sqlserver.settings | + + + First 5 *.jpg files in S3: + assets/img/home-icon.jpg + rockstars/alive/grohl/splash.jpg + rockstars/alive/love/splash.jpg + rockstars/alive/springsteen/splash.jpg + rockstars/alive/vedder/splash.jpg + +### Azure Dashboards + +The nice thing about `#Script` late-binding and cloud agnostic providers is that with just different configuration we +can **use the exact same script** to query an Azure managed SQL Server Database and Azure Blob File Storage: +{{/markdown}} + +{{ 'gfm/sharp-scripts/04.md' | githubMarkdown | convertScriptToCodeBlocks }} + +{{#markdown}} +## Live #Script with 'web watch' + +What's even nicer than the fast feedback of running adhoc scripts? Is the instant feedback you get from being able to **"watch"** the same script! + +To watch a script just replace `run` with `watch`: + + $ x watch script-aws.ss *.jpg + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/sharpscript/watch-aws-azure.gif)](https://youtu.be/GQvxyPHQjhM) + +> YouTube: [youtu.be/GQvxyPHQjhM](https://youtu.be/GQvxyPHQjhM) + +The ability to run stand-alone adhoc scripts in an extensible dynamic scripting language feels like you're +using a "developer enhanced" SQL Studio, where you can combine queries from multiple data sources, [manipulate them with LINQ](https://sharpscript.net/linq/restriction-operators) +and quickly pipe results to dump utils to combine them in the same output for instant visualization. + +`#Script` scripts can also be easily shared, maintained in gists and run on all different Win/OSX/Linux OS's that .NET Core runs on. + +### Live Transformations + +Another area where "watched" scripts can shine is as a "companion scratch pad" assistant during development that you can quickly switch to +and instantly test out live code fragments, calculations and transformations, e.g. This ends up being a great way to test out markdown syntax +and Nuglify's advanced compression using our new `minifyjs` and `minifycss` [Script Blocks](/docs/blocks): +{{/markdown}} + +{{ 'gfm/sharp-scripts/05.md' |> githubMarkdown }} + +{{#markdown}} +Then run with: + + $ x watch livepad.ss + +Which starts a live watched session that re-renders itself on save, initially with: +{{/markdown}} + +{{ 'gfm/sharp-scripts/06.md' |> githubMarkdown }} + +{{#markdown}} +### Live Session + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/sharpscript/livepad.gif)](/docs/sharp-scripts) + +### Adhoc reports + +Scripts can use the built-in [Database Scripts](/docs/db-scripts) to be able to [run queries against any](/docs/sharp-apps#multi-platform-configurations) `sqlite`, `sqlserver`, `mysql` and `postgres` database and quickly view data snapshots using the built-in +[HTML Scripts](/docs/html-scripts#htmldump), e.g: +{{/markdown}} + +{{ 'gfm/sharp-scripts/01.md' |> githubMarkdown }} + +{{#markdown}} +#### Specifying Script Arguments + +The above script generates a static HTML page can be invoked with **any number of named arguments** after the script name, in this case it +generates a report for Northwind Order **#10643**, saves it to `10643.html` and opens it in the OS's default browser: + + $ x run script.html -id 10643 > 10643.html && start 10643.html + +Which looks like: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/sharpscript/10643.html.png) + +### textDump + +Generating static `.html` pages can quickly produce reports that looks good enough to share with others, +but if you just want to see a snapshot info at a glance or be able to share in text-based mediums like email or chat +channels you can replace `htmlDump` with `textDump` where it will instead output GitHub flavored Markdown tables, e.g: +{{/markdown}} + +{{ 'gfm/sharp-scripts/02.md' |> githubMarkdown }} + +{{#markdown}} +As the output is human-readable we can view directly it without a browser: + + $ x run script.ss -id 10643 + +Which will output: +{{/markdown}} + +{{ 'gfm/sharp-scripts/07.md' |> githubMarkdown }} + +{{#markdown}} +And because they're GitHub Flavored Markdown Tables they can be embedded directly in Markdown docs (like this) where it's renders as: +{{/markdown}} + +
    +{{ 'gfm/sharp-scripts/08.md' |> githubMarkdown }} +
    + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/syntax.html b/wwwroot/docs/syntax.html new file mode 100644 index 0000000..950629b --- /dev/null +++ b/wwwroot/docs/syntax.html @@ -0,0 +1,585 @@ + + +

    + #Script aims to be a familiar and expressive dynamic language for scripting .NET Apps, that is optimal + at generating text, especially HTML where it's pre-configured with the + HTML Page Format and HTML Scripts but + unlike C#'s Razor can support multiple pluggable page formats which can be used for generating any kind of text. +

    + +

    + For maximum familiarity #Script uses JavaScript Expressions + that for increased readability and expressiveness supports being used within Template Expressions + which like Vue.js filters, + and Angular's Template Expressions + lets you use the | pipe operator to chain the return values of methods into subsequent methods from left-to-right. + For statements #Script adopts the familiar Handlebars-like + Script Blocks that is also popular among HTML templating engines. +

    + +

    + Effectively #Script lets you use the same familiar JS language for server HTML rendering as you would do + in client-side rendering of Single Page Apps despite it binding natively to C# objects and calling C# methods behind-the-scenes. +

    + +

    Mustache expressions

    + +

    + Like Vue/Angular Templates, only expressions inside mustaches are evaluated, whilst everything outside are emitted as-is: +

    + +{{ 'live-template' |> partial({ template: "outside {{ 'shout' |> upper }} text" }) }} + +

    + Which calls the upper default script method where the argument on the left-side of the "pipe" symbol is + passed as the first argument to the upper method which is implemented as: +

    + +{{ 'gfm/syntax/01.md' |> githubMarkdown }} + +

    + This can be rewritten without the "pipe forward" operator by calling it as a method or an extension method instead: +

    + +{{ 'live-template' |> partial({ template: "outside {{ upper('shout') }} or {{ 'shout'.upper() }} text" }) }} + +

    Methods can be chained

    + +

    + Methods are chained from left-to-right where the value on the left side of the "pipe" symbol is passed as the first + argument in the method on the right and the output of that is passed as the input of the next method in the chain and so on: +

    + +{{ 'live-template' |> partial({ template: "{{ 'shout' |> upper |> substring(2) |> padRight(6, '_') |> repeat(3) }}" }) }} + +

    + Methods can also accept additional arguments which are passed starting from the 2nd argument since the first + argument is the value the method is called with. E.g. here are the implementations for the substring and + padRight default scripts: +

    + +{{ 'gfm/syntax/02.md' |> githubMarkdown }} + +

    JavaScript literal notation

    + +

    + You can use the same literal syntax used to define numbers, strings, booleans, null, Objects and Arrays in JavaScript + within templates and it will get converted into the most appropriate .NET Type, e.g: +

    + +{{ 'live-template' |> partial({ rows: 8, template: "{{ null |> typeName }} +{{ true |> typeName }} +{{ 1 |> typeName }} +{{ 1.1 |> typeName }} +{{ 'string' |> typeName }} +{{ ['array', 'items'] |> typeName }} +{{ { key: 'value' } |> typeName }} +" }) }} + +

    + ES6 Shorthand notation is also supported where you can use the argument name as its property name in a Dictionary: +

    + +{{ 'live-template' |> partial({ template: "{{ var bar = 'foo' }} +{{ var obj = { bar } }} +{{ obj['bar'] }}" }) }} + +

    Local Variables

    + +

    + Like JavaScript you can declare a locally scoped variable with var: +

    + +
    var sliders = dirFiles('img/sliders')
    + +

    + Like JS you can use either var, let or const but they all behave like let and assign a locally scoped variable + at the time the expression is executed. Also like JS the semicolon is optional and you can assign multiple variables in a single expression: +

    + +
    let a = 1 + 2, b = 3 * 4, c, d = 'D';
    + +

    Global Variables

    + +

    + Global Variables in #Script are maintained in the PageResult.Args dictionary which you could previously assign to using the + toGlobal script method where it’s accessible to all scripts rendered within that PageResult. +

    + +

    + #Script now mimics JS's behavior to allow assignment expressions to assign global variables where **Assignment Expressions** on + undeclared variables (i.e. where no locally scoped variable exists) will assign a global variable: +

    + +
    a = 1
    + +

    + A more descriptive syntax available to declare a global variable is to assign it to the global object (inspired by node’s global) + which is an alias to the PageResult.Args dictionary: +

    + +
    global.a = 1
    + +

    Assignment Expressions

    + +

    + In addition to declaring and assigning variables, there’s also support for using assignment expressions to assign and mutate Collections and + Type Properties using either Member Expression or Index expression syntax, e.g: +

    + +{{ 'gfm/syntax/11.md' |> githubMarkdown }} + +

    Quotes

    + +

    + Strings can be defined using single quotes, double quotes, prime quotes or backticks: +

    + +{{ 'live-template' |> partial({ rows:4, template: "{{ \"double quotes\" }} +{{ 'single quotes' }} +{{ ′prime quotes′ }} +{{ `backticks` }}" }) }} + +
    + Strings can also span multiple lines. +
    + +

    Template Literals

    + +Backticks strings implement JavaScript's Template literals +which can be be used to embed expressions: + +{{ 'live-template' |> partial({ rows:4, template: "{{ `Time is ${now}, expr= ${true ? pow(1+2,3) : ''}` }} +Prime Quotes {{ now.TimeOfDay |> timeFormat(′h\\:mm\\:ss′) }} +Template Literal {{ now.TimeOfDay |> timeFormat(`h\\\\:mm\\\\:ss`) }}" }) }} + +The example above also shows the difference in escaping where Template literals evaluate escaped characters whilst normal strings +leave \ backspaces unescaped. + +

    Shorthand arrow expression syntax

    + +

    + #Script Expressions have full support for JavaScript expressions but doesn't support statements or function declarations although it + does support JavaScript's arrow function expressions which can be used in functional methods to enable LINQ-like queries. + You can use fat arrows => immediately after methods to define lambda's with an implicit (it => ...) binding, e.g: +

    + +{{ 'live-template' |> partial({ template: ′{{ [0,1,2,3,4,5] + |> where => it >= 3 + |> map => it + 10 |> join(`\n`) }}′ }) }} + +

    + This is a shorthand for declaring lambda expressions with normal arrow expression syntax: +

    + +{{ 'live-template' |> partial({ template: ′{{ [0,1,2,3,4,5] + |> where(it => it >= 3) + |> map(x => x + 10) |> join(`\n`) }}′ }) }} + +

    + Using normal lambda expression syntax lets you rename lambda parameters as seen in the map(x => ...) example. +

    + +

    Special string argument syntax

    + +

    + As string expressions are a prevalent in #Script, we've also given them special wrist-friendly syntax where you + can add a colon at the end of the method name which says to treat the following characters up until the end of the + line or mustache expression as a string, trim it and convert '{' and '}' chars into mustaches. With this syntax + you can write: +

    + +{{ 'live-template' |> partial({ template: "{{ [3,4,5] |> select: { it |> incrBy(10) }\\n }}" }) }} + +

    + and it will be rewritten into its equivalent and more verbose form of: +

    + +{{ 'live-template' |> partial({ template: "{{ [3,4,5] |> select(′{{ it |> incrBy(10) }}\\n′) }}" +}) }} + +

    SQL-like Boolean Expressions

    + +

    + To maximize readability and intuitiveness for non-programmers, boolean expressions can also adopt an SQL-like syntax where + instead of using && or || operator syntax to define boolean expressions you can also + use the more human-friendly and and or alternatives: +

    + +{{ 'live-template' |> partial({ template: "{{ [0,1,2,3,4,5] + |> where => it.isOdd() and it == 3 + |> join }}" }) }} + +

    Include Raw Content Verbatim

    + +Use #raw blocks to ignore evaluating expressions and emit content verbatim. This is useful when using a +client Handlebars-like templating solution like Vue or Angular templates where expressions need to be evaluated +with JavaScript in the browser instead of on the Server with Templates: + +{{ 'live-template' |> partial({ rows: 4, template: "{{#raw}}Hi {{ person.name }}, Welcome to {{ site.name }}!{{/raw}} + +{{#raw template}}Assign contents with {{ expressions }} into 'template' argument{{/raw}} +Captured Argument: {{template}}" + }) }} + +

    Multi-line Comments

    + +

    + Any text within {{#raw}}{{#noop}} ... {{/noop}}{{/raw}} block statements are ignored and can be used for temporarily removing + sections from pages without needing to delete it. +

    + +

    + Everything within multi-line comments {{#raw}}{{‌* and *‌}}{{/raw}} is ignored and removed from the page. +

    + +

    + An alternative way to temporarily disable an expression is to prefix the expression with the end method to + immediately short-circuit evaluation, e.g: {{#raw}}{{ end |> now |> dateFormat }}{{/raw}} +

    + +

    + See Ignoring Pages for different options for ignoring entire pages and + layout templates. +

    + + +{{#markdown}} + +### Script Methods can be used as Extension Methods + +A core feature of `#Script` is that it runs in a [sandbox](/sandbox) and only has access to functionality that's configured in its `ScriptContext` +that it runs in, so by design `#Script` is prohibited from calling instance methods so they only have a read-only view of your objects +unless you explicitly register `ScriptMethods` that allows them to change them. + +This frees up the `instance.method()` syntax to be put to other use which can now be used to call every script method as an extension method. +This can greatly improve the readability and execution flow of code, e,g. we can rewrite our previous +[JS Utils Eval](https://docs.servicestack.net/js-utils#eval) example: + + itemsOf(3, padRight(reverse(arg), 8, '_')) + +into the more readable form using the same methods as extension methods off the first argument, e.g: + + 3.itemsOf(arg.reverse().padRight(8, '_')) + +{{/markdown}} + +{{ 'gfm/syntax/04.md' |> githubMarkdown }} + +{{#markdown}} + +### JavaScript Array Support + +We can use extension methods to define instance methods that can be called on any object to implement JS Array methods support +to further improve `#Script` source compatibility with JavaScript. + +Here are [Mozilla's Array examples](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) in `#Script` +utilizing its [Code Blocks](#code-blocks) feature to reduce the amount of boilerplate required: + +{{/markdown}} + +{{ 'gfm/syntax/03.md' | githubMarkdown | convertScriptToCodeBlocks }} + +{{#markdown}} + +Which can be run with `x run {script}.ss` to view its expected output: + + Create an Array + 2 + + Access (index into) an Array Item + Apple + + Loop over an Array + Apple, 0 + Banana, 1 + + Apple, 0 + Banana, 1 + + Add to the end of an Array + 3 + + Remove from the end of an Array + Orange + + Remove from the front of an Array + Apple + + Add to the front of an Array + 2 + + Find the index of an item in the Array + 1 + + Remove an item by index position + Banana + Strawberry,Mango + + Remove items from an index position + Cabbage,Turnip,Radish,Carrot + Cabbage,Carrot + Turnip,Radish + + Copy an Array + Strawberry,Mango + +Most JS Array methods are supported, including the latest additions from ES2019: + + - `concat` + - `every` + - `filter` + - `find` + - `findIndex` + - `flat` + - `flatMap` + - `forEach` + - `includes` + - `indexOf` + - `join` + - `keys` + - `lastIndexOf` + - `map` + - `pop` + - `push` + - `reduce` + - `reverse` + - `shift` + - `slice` + - `some` + - `sort` + - `splice` + - `toString` + - `unshift` + - `values` + + + +## Language Blocks and Expressions + +We've caught a glimpse using language blocks with the `code` JavaScript Array Example above which allows us to invert `#Script` from **"Template Mode"** +where all text is emitted as-is with only code within Template Expressions `{{ ... }}` are evaluated and changed it to **"Code Mode"** where all code +is evaluated a code expression. + +This is akin to Razor's statement blocks which inverts Razor's **mode** of emitting text to treating text inside statement blocks as code, e.g: + +{{/markdown}} + +{{ 'gfm/syntax/05.md' | githubMarkdown | convertScriptToCodeBlocks }} + +{{#markdown}} + +Which is useful in reducing boilerplate when you need to evaluate code blocks with 2+ or more lines without the distracting boilerplate of wrapping +each expression within a `{{ ... }}` Template Expression. + +### Languages + +Refer to the languages page to learn about alternative languages you can use within Language Blocks and Expressions: + + - [#Script Code](/scode/) - use `code` language identifier + - [#Script Lisp](/lisp/) - use `lisp` language identifier + +### Language Blocks + +`code` fragments are executed using `#Script` language blocks in the format: + + ``` + + ``` + +Where `#Script` will parse the statement body with the language registered in its ScriptContext's `ScriptLanguages` collection that's pre-registered +with `ScriptCode.Language` which is used to process `code` block statements, e.g: + +{{/markdown}} + +{{ 'gfm/syntax/07.md' | githubMarkdown | convertScriptToCodeBlocks }} + +

    + Code Statement Blocks are evaluated within the same scope so any arguments that are assigned are also accessible within the containing page + as seen above. +

    + +

    Evaluate Lisp

    + +

    + You can use language blocks to embed and evaluate any of the languages registered in your ScriptContext, e,g. to evaluate + #Script Lisp register it in your ScriptContext or SharpPagesFeature: +

    + +{{ 'gfm/lisp/01.md' |> githubMarkdown }} + +

    + Where it will let you use Language Blocks to evaluate LISP code within #Script: +

    + +{{ 'gfm/syntax/08.md' | githubMarkdown | convertScriptToLispBlocks }} + +{{#markdown}} + +Unlike `code` blocks, Lisp evaluates its code using its own Symbols table, it's able to reference arguments not in its Global symbols +by resolving them from the containing scope, but in order for the outer `#Script` to access its local bindings they need +to be exported as seen above which registers the value of its `local-arg` value into the `result` argument. + +#### Language Block Modifiers + +You can provide any additional modifiers to language blocks by specifying them after them after the `|` operator, languages +can use these modifiers to change how it evaluates the script. By default the only modifiers the built-in languages support are `|quiet` +and its shorter `|mute` and `|q` aliases which you can use to discard any output from being rendered within the page. + +If you use Lisp's [setq special form](http://www.lispworks.com/documentation/HyperSpec/Body/s_setq.htm) to assign a variable, +that value is also returned which would be rendered in the page, you can ignore this output by using one of the above modifiers, e.g: + +{{/markdown}} + +{{ 'gfm/syntax/09.md' | githubMarkdown | convertScriptToLispBlocks }} + +{{#markdown}} +### Language Expressions + +If you wanted to instead embed an expression in a different language instead of executing an entire statement block, +you can embed them within Language Expressions `{| ... |}`, e.g: +{{/markdown}} + +{{ 'gfm/syntax/10.md' |> githubMarkdown }} + +{{#markdown}} +You could also use them to evaluate `code` expressions, e.g. `{|code 1 + 2 |}`, but that's also how `#Script` +more concise Template Expressions are evaluated `{{ 1 + 2 }}`. + +### Multi-language support + +Despite being implemented in different languages a `#Script` page containing multiple languages, e.g: + +{{/markdown}} + +{{ "linq-preview" |> partial({ rows:11, example: "linq01-langs", lang: 'template' }) }} + +{{#markdown}} + +Still only produces a **single page AST**, where when first loaded `#Script` parses the page contents as a contiguous +`ReadOnlyMemory` where page slices of any [Language Blocks and Expressions](/docs/syntax#language-blocks-and-expressions) +on the page are delegated to the `ScriptContext` registered `ScriptLanguages` for parsing which returns a fragment which is +added to the pages AST: + +[![](/assets/img/multi-langs.svg)](/assets/img/multi-langs.svg) + +When executing the page, each language is responsible for rendering its own fragments which all write directly to the pages `OutputStream` +to generate the pages output. + +The multi-languages support in `#Script` is designed to be extensible where everything about the language is encapsulated within its +`ScriptLanguage` implementation so that if you omit its registration: + + var context = new ScriptContext { + // ScriptLanguages = { ScriptLisp.Language } + }.Init(); + +Any language expressions and language blocks referencing it become inert and its source code emitted as plain-text. + +### Language Comparisons + +Whilst all languages have access to the same [Script Methods](/docs/methods) and [.NET Scripting interop](/docs/script-net), they all have +unique characteristics that make them suitable for different use-cases. The default template syntax is ideal for generating text output where +you need to embed logic when generating text output, [#Script Code](/scode/) is ideal when logic is more important then the text output like in +[Sharp Scripts](/docs/sharp-scripts), whilst [#Script Lisp](/lisp/) is ideal for defining algorithms or for developers who prefer Lisp syntax +or for devs who prefer development in a functional style. + +A good comparison showing the differences, strengths and weaknesses of each language can be seen with the implementation of FizzBuzz in each language: + +{{/markdown}} + +{{#raw template}} +Template: +{{#each range(1,15) }} +{{#if it % 3 == 0 && it % 5 == 0}} + FizzBuzz +{{else if it % 3 == 0}} + Fizz +{{else if it % 5 == 0}} + Buzz +{{else}} + {{it}} +{{/if}} +{{/each}} + +Code: +```code +#each range(1,15) + #if it % 3 == 0 && it % 5 == 0 + "FizzBuzz" + else if it % 3 == 0 + "Fizz" + else if it % 5 == 0 + "Buzz" + else + it + /if +/each +``` + +Lisp: +```lisp +(defn fizzbuzz [i] + (cond ((and (zero? (mod i 3)) + (zero? (mod i 5))) "FizzBuzz") + ((zero? (mod i 3)) "Fizz") + ((zero? (mod i 5)) "Buzz") + (t i))) + +(dorun println (map fizzbuzz (range 1 16))) +```{{/raw}} + +{{ 'live-template' |> partial({ rows:39, template }) }} + +{{#markdown}} + +#### Template + +`#Script` is a **"template-first"** language where only expressions within `{{ ... }}` literals are evaluated, everything outside +it is emitted verbatim, as consequence it's white-space sensitive where the indention of logic blocks affect its output as +seen above where all lines are indented with the same 2 spaces as its free-text contents. + +#### Code + +By contrast [#Script Code](/scode/) is an inverted Templates where it's a **"code-first"** language where all statements are treated as code blocks +so statements don't require the `{{ ... }}` boilerplate and are not white-space sensitive. Like Templates each content block +still generates output (unless its suppressed with the [quiet modifier](/docs/syntax#language-block-modifiers)) but needs to be a valid expression +like a string literal, argument or expression as seen in the **code** example above. + +#### Lisp + +[#Script Lisp](/lisp/) lets you use the venerable and highly dynamic and functional [Lisp language](https://en.wikipedia.org/wiki/Lisp_%28programming_language%29) +that's particularly strong at capturing algorithms and composing functions, although FizzBuzz isn't a particularly +good showcase of either of its dynamism of functional strengths, the last line shows a glimpse of its natural functional composition. + +### Combine strengths of all Languages + +As all languages compile down into the same AST block we can freely use all languages within the same Script block to take advantage +of the strengths of each, e.g. **Template** is great at generating text and **Lisp** excels at algorithms which make a great combination if you +ever need to combine the 2, so you could easily for example use the [defn Script Block](/docs/blocks#defn) to define a function +in Lisp, compiled to a .NET delegate so its invocable as a regular function inside a Template Block that generates markdown +captured by the [capture Script Block](/docs/blocks#capture) and converted to HTML using the [markdown filter transformer](/docs/sharp-pages#markdown): +{{/markdown}} + +{{#raw template}} +{{#defn fizzbuzz [i] }} + (cond ((and (zero? (mod i 3)) + (zero? (mod i 5))) "FizzBuzz") + ((zero? (mod i 3)) "Fizz") + ((zero? (mod i 5)) "Buzz") + (t i)) +{{/defn}} + +{{#capture md}} +## FizzBuzz +{{#each range(1,15) }} + - {{ fizzbuzz(it) }} +{{/each}} +{{/capture}} +{{ md |> markdown }}{{/raw}} + +{{ 'live-template' |> partial({ output:'html-md no-pre', rows:15, template }) }} + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/docs/test.html b/wwwroot/docs/test.html new file mode 100644 index 0000000..8bb76e6 --- /dev/null +++ b/wwwroot/docs/test.html @@ -0,0 +1,9 @@ +{{ "live-pages" |> partial({ + page: 'page', + files: + { + '_layout.html': 'I am the Layout: {{ page }}', + 'page.html' : 'I am the Page' + } + }) +}} diff --git a/wwwroot/docs/transformers.html b/wwwroot/docs/transformers.html new file mode 100644 index 0000000..f2a87c0 --- /dev/null +++ b/wwwroot/docs/transformers.html @@ -0,0 +1,103 @@ + + +

    + You can apply a chain of multiple Stream transformations to transform output using Transformers which are just functions that + accept an Input Stream and return a modified Output Stream. The MarkdownPageFormat's TransformToHtml shows an example + of a Transformer which converts Markdown Input and returns a Stream of HTML output: +

    + +{{ 'gfm/transformers/01.md' |> githubMarkdown }} + +

    Output Transformers

    + +

    + You can transform the entire output of a Script with Output Transformers which you would do if both your _layout + and page both contain markdown, e.g: +

    + +{{ 'gfm/transformers/02.md' |> githubMarkdown }} + +
    PageResult with Output Transformer
    + +{{ 'gfm/transformers/03.md' |> githubMarkdown }} + +

    + After the Script is evaluated it's entire output gets passed into the chain of OutputTransformers defined, which in this case will + send a MemoryStream of the generated Markdown Output into the MarkdownPageFormat.TransformToHtml transformer which returns + a Stream of converted HTML which is what's written to the OutputStream. +

    + +

    Page Transformers

    + +

    + You can also apply Transformations to only the Page's output using Page Transformers which you would do if only the page was in + Markdown and the _layout was already in HTML, e.g: +

    + +{{ 'gfm/transformers/04.md' |> githubMarkdown }} + +
    PageResult with Page Transformer
    + +{{ 'gfm/transformers/05.md' |> githubMarkdown }} + +

    Filter Transformers

    + +

    + Filter Transformers are used to apply Stream Transformations to Block Methods which you + could use if you only wanted to convert an embedded Markdown file inside a Page to HTML. You can register Filter Transformers + in either the ScriptContext's or PageResult's FilterTransformers Dictionary by assigning it the name you want it available in + your Scripts under, e.g: +

    + +{{ 'gfm/transformers/06.md' |> githubMarkdown }} + +
    PageResult with Filter Transformer
    + +{{ 'gfm/transformers/07.md' |> githubMarkdown }} + +

    htmlencode

    + +

    + The htmlencode Filter Transformer is pre-registered in ScriptContext which lets you encode Block Method outputs + which is useful when you want to HTML Encode a text file before embedding it in the page, e.g: +

    + +
    {{ pass: "page.txt" |> includeFile |> htmlencode }}
    + +

    Preprocessor Code Transformations

    + +{{#markdown}} +Preprocessors allows you to perform source-code transformations before `#Script` evaluates it which is useful when you want to +convert any placeholder markers into HTML or `#Script` code at runtime, e.g. this feature could be used to replace translated text with the +language that the App's configured with to use on Startup. + +With this feature we can also change how [#Script's Language Block Feature](/docs/syntax#language-blocks-and-expressions) is implemented where +we could instead use a [simple TransformCodeBlocks PreProcessor](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Common/Script/ScriptPreprocessors.cs) +to process any `code` bocks by registering in the `Preprocessors` list: + + var context = new ScriptContext { + Preprocessors = { ScriptPreprocessors.TransformCodeBlocks } + }.Init(); + +There by overriding the existing implementation with the code transformation below: + +#### Code Transformation + +This preprocessor performs a basic transformation that assumes every statement is an expression and wraps them in an `{{...}}` +expression block, the exception are expressions which are already within an expression block which are ignored and you still +need to use to wrap multi-line expressions in `code` blocks. + +The other transformation `code` blocks perform is collapsing new lines and trimming each line, this is so scripts which are primarily +structured and indented for readability aren't reflected in its text output. + +We can see an example of how `code` blocks work from the example below: +{{/markdown}} + +{{ 'gfm/syntax/06.md' | githubMarkdown | convertScriptToCodeBlocks }} + + + +{{ "doc-links" |> partial({ order }) }} diff --git a/wwwroot/examples/adhoc-query-db.html b/wwwroot/examples/adhoc-query-db.html new file mode 100644 index 0000000..2e424f6 --- /dev/null +++ b/wwwroot/examples/adhoc-query-db.html @@ -0,0 +1,14 @@ +
    +{{ "select customerId,companyName,city,country from customer where country=@country"
    +   |> to => selectSql }}
    +{{ selectSql }}
    +{{ selectSql |> dbSelect({ country: 'UK' }) |> textDump({ caption:"HTML Results (select):" }) }}
    +{{ "select * from customer c join [order] o on c.customerId = o.customerId 
    +         order by total desc limit 1" |> to => singleSql }}{{ singleSql }}:
    +
    +{{ singleSql |> dbSingle |> textDump({ caption:"Text Results (single):" }) }}
    +
    +Object Value (scalar):
    +{{ "select min(orderDate) from [order]" |> to => scalarSql }}
    +{{ scalarSql }}: {{ scalarSql |> dbScalar |> dateFormat }}
    +
    \ No newline at end of file diff --git a/wwwroot/examples/customer-card.txt b/wwwroot/examples/customer-card.txt new file mode 100644 index 0000000..eb28ef3 --- /dev/null +++ b/wwwroot/examples/customer-card.txt @@ -0,0 +1 @@ +{{ 'customerCard' |> partial({ customerId: "ALFKI" }) }} \ No newline at end of file diff --git a/src/wwwroot/examples/customer.html b/wwwroot/examples/customer.html similarity index 82% rename from src/wwwroot/examples/customer.html rename to wwwroot/examples/customer.html index b748204..d7d55b3 100644 --- a/src/wwwroot/examples/customer.html +++ b/wwwroot/examples/customer.html @@ -26,5 +26,5 @@

    {{ CompanyName }}'s Orders

    {{ Orders - | select: }} + |> select: }}
    { it.OrderId }{ it.OrderDate }{ it.Total | currency }
    { it.OrderId }{ it.OrderDate }{ it.Total |> currency }
    diff --git a/src/wwwroot/examples/email-html-template.html b/wwwroot/examples/email-html-template.html similarity index 88% rename from src/wwwroot/examples/email-html-template.html rename to wwwroot/examples/email-html-template.html index cd75021..54cc5d1 100644 --- a/src/wwwroot/examples/email-html-template.html +++ b/wwwroot/examples/email-html-template.html @@ -4,7 +4,7 @@ diff --git a/wwwroot/examples/email-template.txt b/wwwroot/examples/email-template.txt new file mode 100644 index 0000000..d12bc4d --- /dev/null +++ b/wwwroot/examples/email-template.txt @@ -0,0 +1,18 @@ + +Hello {{ customer.CompanyName }}, + +Thank you for shopping with us. We'll send a confirmation when your item ships. + +# Ship to: +{{ customer.Address }} +{{ customer.City }}, {{ customer.PostalCode }}, {{ customer.Country }} + +# Details: +Order #{{ order.OrderId }} on {{ order.OrderDate |> dateFormat('dd/MM/yyyy') }} +Total: {{ order.Total |> currency }} + +We hope to see you again soon. + +[acme.org][1] + +[1]: http://acme.org \ No newline at end of file diff --git a/wwwroot/examples/introspect-drive.html b/wwwroot/examples/introspect-drive.html new file mode 100644 index 0000000..584c183 --- /dev/null +++ b/wwwroot/examples/introspect-drive.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/wwwroot/examples/introspect-process.html b/wwwroot/examples/introspect-process.html new file mode 100644 index 0000000..9fd151a --- /dev/null +++ b/wwwroot/examples/introspect-process.html @@ -0,0 +1,10 @@ +{{ "h':'mm':'ss'.'FFF" |> to => fmtTime }} + + + + + + + + + \ No newline at end of file diff --git a/wwwroot/examples/introspect.html b/wwwroot/examples/introspect.html new file mode 100644 index 0000000..513ff93 --- /dev/null +++ b/wwwroot/examples/introspect.html @@ -0,0 +1,32 @@ +
    - +

    Acme, Inc.

    Order Confirmation
    {{ it.Name }}{{ it.DriveType }} #{{ it.VolumeLabel }} ({{ it.DriveFormat }}){{ it.AvailableFreeSpace |> divide(1024) |> format('n0') }} KB{{ it.TotalFreeSpace |> divide(1024) |> format('n0') }} KB{{ it.TotalSize |> divide(1024) |> format('n0') }} KB
    {{ it.Id }}{{ it.ProcessName |> substringWithElipsis(15) }}{{ it.TotalProcessorTime |> timeFormat(fmtTime) }}{{ it.UserProcessorTime |> timeFormat(fmtTime) }}{{ it.WorkingSet64 |> divide(1024) |> format('n0') }} KB{{ it.PeakWorkingSet64 |> divide(1024) |> format('n0') }} KB{{ it.Threads.Count }}
    + + + {{#each drives orderby it.TotalSize descending take 5}} + + + + + + + + {{/each}} +
    Top 5 Local Disks
    NameTypeAvailable SpaceTotal Free SpaceTotal Size
    {{ Name |> substringWithEllipsis(50) }}{{ DriveType }} #{{ VolumeLabel }} ({{ DriveFormat }}){{ AvailableFreeSpace / 1024 |> format('n0') }} KB{{ TotalFreeSpace / 1024 |> format('n0') }} KB{{ TotalSize / 1024 |> format('n0') }} KB
    + + + + + + {{#if currentProcess}} + {{ currentProcess |> to => p }} + {{ "h':'mm':'ss'.'FFF" |> to => fmtTime }} + + + + + + + + + + {{/if}} +
    Current Process
    IdNameCPU TimeUser TimeMemory (current)Memory (peak)Active Threads
    {{ Id }}{{ p.ProcessName |> substringWithEllipsis(15) }}{{ p.TotalProcessorTime |> timeFormat(fmtTime) }}{{ p.UserProcessorTime |> timeFormat(fmtTime) }}{{ p.WorkingSet64 / 1024 |> format('n0') }} KB{{ p.PeakWorkingSet64 / 1024 |> format('n0') }} KB{{ p.Threads.Count }}
    \ No newline at end of file diff --git a/wwwroot/examples/monthly-budget.txt b/wwwroot/examples/monthly-budget.txt new file mode 100644 index 0000000..f940c0b --- /dev/null +++ b/wwwroot/examples/monthly-budget.txt @@ -0,0 +1,36 @@ +```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: {{ balance |> currency }} + +Monthly Revenues: +{{monthlyRevenues |> select: {it.Key.padRight(16)} {it.Value.currency()}\n }} +Total {{ totalRevenues |> currency }} + +Monthly Expenses: +{{monthlyExpenses |> select: {it.Key.padRight(16)} {it.Value.currency()}\n }} +Total {{ totalExpenses |> currency }} + +Monthly Savings: {{ totalSavings |> currency }} + +Projected Cash Position: +{{projectedMonths.times() |> map => index + 1 |> map => +`${now.addMonths(it).dateFormat()} ${(it * totalSavings + balance).currency()}`|> joinln}} \ No newline at end of file diff --git a/wwwroot/examples/nav-links.txt b/wwwroot/examples/nav-links.txt new file mode 100644 index 0000000..17843e4 --- /dev/null +++ b/wwwroot/examples/nav-links.txt @@ -0,0 +1,9 @@ +{{ 'navLinks' |> partial( + { + links: { + '/docs/model-view-controller': 'MVC', + '/docs/sharp-pages': '#Script Pages', + '/docs/code-pages': 'Sharp Code Pages' + } + }) +}} diff --git a/wwwroot/examples/qotd.html b/wwwroot/examples/qotd.html new file mode 100644 index 0000000..dd27ae1 --- /dev/null +++ b/wwwroot/examples/qotd.html @@ -0,0 +1,15 @@ + + +

    Before

    + +

    {{ 'select url from quote where id= @id' |> dbScalar({ qs.id }) |> urlContents |> htmlencode }}

    + +

    Code

    + +
    {{ 'examples/quote.html' |> includeFile }}
    + +

    After

    + +{{ 'examples/quote' |> partial({ qs.id }) }} diff --git a/wwwroot/examples/query-github.txt b/wwwroot/examples/query-github.txt new file mode 100644 index 0000000..e45c905 --- /dev/null +++ b/wwwroot/examples/query-github.txt @@ -0,0 +1,13 @@ +{{#markdown}} +## [ServiceStack](https://github.com/ServiceStack) GitHub Repos +{{/markdown}} + +```code +"https://api.github.com/orgs/ServiceStack/repos"|> urlContentsWithCache({userAgent:'Script'})|> to => json + +json |> parseJson |> orderByDesc => it.watchers |> to => repos + +repos |> take(5) |> map => { it.name, it.watchers, it.forks } |> htmlDump({caption:'Top 5 Repos'}) +``` + +Total Repos: {{ repos.count() }}, Watchers: {{ repos |> map => it.watchers |> sum }} \ No newline at end of file diff --git a/wwwroot/examples/quote.html b/wwwroot/examples/quote.html new file mode 100644 index 0000000..301fdd0 --- /dev/null +++ b/wwwroot/examples/quote.html @@ -0,0 +1,3 @@ +{{ 'select url from quote where id= @id' |> dbScalar({ qs.id }) |> urlContents |> markdown |> to =>quote}} + +{{ quote |> replace('Razor', 'Templates') |> replace('2010', now.Year) |> raw }} \ No newline at end of file diff --git a/src/wwwroot/examples/select-customers.txt b/wwwroot/examples/select-customers.txt similarity index 100% rename from src/wwwroot/examples/select-customers.txt rename to wwwroot/examples/select-customers.txt diff --git a/wwwroot/examples/sendToAutoQuery-data.html b/wwwroot/examples/sendToAutoQuery-data.html new file mode 100644 index 0000000..c682892 --- /dev/null +++ b/wwwroot/examples/sendToAutoQuery-data.html @@ -0,0 +1,5 @@ +{{ { UserName:'ServiceStack', NameStartsWith:'ServiceStack', OrderBy:'-Watchers_Count',take:10 } + |> sendToAutoQuery: QueryGitHubRepos + |> toResults + |> selectFields: Name, Homepage, Language, Watchers_Count + |> htmlDump({ caption: "Most popular ServiceStack repos starting with 'ServiceStack'" }) }} \ No newline at end of file diff --git a/wwwroot/examples/sendToAutoQuery-rdms.html b/wwwroot/examples/sendToAutoQuery-rdms.html new file mode 100644 index 0000000..3f7584f --- /dev/null +++ b/wwwroot/examples/sendToAutoQuery-rdms.html @@ -0,0 +1,5 @@ +{{ { cityIn:['London','Madrid','Rio de Janeiro'], faxContains:'555' } + |> sendToAutoQuery: QueryCustomers + |> toResults + |> selectFields: CustomerId, CompanyName, City, Fax + |> htmlDump({ caption: 'Implicit AutoQuery Conventions' }) }} diff --git a/wwwroot/examples/sendtogateway-customers.html b/wwwroot/examples/sendtogateway-customers.html new file mode 100644 index 0000000..6a9cc00 --- /dev/null +++ b/wwwroot/examples/sendtogateway-customers.html @@ -0,0 +1,10 @@ +{{ { customerId } |> sendToGateway('QueryCustomers') |> toResults |> get(0) + |> htmlDump({ caption: 'ALFKI' }) }} + +{{ { countryIn:['UK','Germany'], orderBy:'customerId',take:5 } |> sendToGateway: QueryCustomers + |> toResults |> selectFields(['CustomerId', 'CompanyName', 'City']) + |> htmlDump({ caption: 'Implicit AutoQuery Conventions' }) }} + +{{ { companyNameContains:'the',orderBy:'-Country,CustomerId' } |> sendToGateway: QueryCustomers + |> toResults |> selectFields: CustomerId, CompanyName, Country + |> htmlDump }} diff --git a/src/wwwroot/examples/webapps-menu.html b/wwwroot/examples/webapps-menu.html similarity index 76% rename from src/wwwroot/examples/webapps-menu.html rename to wwwroot/examples/webapps-menu.html index 0fd51ee..ce9b514 100644 --- a/src/wwwroot/examples/webapps-menu.html +++ b/wwwroot/examples/webapps-menu.html @@ -2,4 +2,4 @@ '/about': 'About', '/services': 'Services', '/contact': 'Contact', - } | toList | assignTo: links }} \ No newline at end of file + } |> toList |> to => links }} \ No newline at end of file diff --git a/wwwroot/gfm/_scratch/01.md b/wwwroot/gfm/_scratch/01.md new file mode 100644 index 0000000..e236dfc --- /dev/null +++ b/wwwroot/gfm/_scratch/01.md @@ -0,0 +1,12 @@ +| App | URL Scheme | Command line (x-plat) | +|-------------------------------------------|----------------------------------|-----------------------| +| [Spirals](/sharp-apps/spirals) | [app://spirals](app://spirals) | $ x open spirals | +| [Redis](/sharp-apps/redis) | [app://redis](app://redis) | $ x open redis | +| [Rockwind](/sharp-apps/rockwind) | [app://rockwind](app://rockwind) | $ x open rockwind | +| [Plugins](/sharp-apps/plugins) | [app://plugins](app://plugins) | $ x open plugins | +| [Chat](/sharp-apps/chat) | [app://chat](app://chat) | $ x open chat | +| [Blog](/sharp-apps/blog) | [app://blog](app://blog) | $ x open blog | +| [Win32](/sharp-apps/win32) | [app://win32](app://win32) | $ x open win32 | +| [ServiceStack Studio](/sharp-apps/studio) | [app://studio](app://studio) | $ x open studio | +| [SharpData](/sharp-apps/sharpdata) | [app://sharpdata?mix=northwind.sharpdata](app://sharpdata?mix=northwind.sharpdata) | $ x open sharpdata mix northwind.sharpdata | + diff --git a/wwwroot/gfm/_scratch/02.html b/wwwroot/gfm/_scratch/02.html new file mode 100644 index 0000000..1348181 --- /dev/null +++ b/wwwroot/gfm/_scratch/02.html @@ -0,0 +1,161 @@ +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namedefaultdescription
    bindlocalhostWhich hostname to bind .NET Core Server to
    ssltrueUse https for .NET Core Server
    port5001Which port to bind .NET Core Server to
    nameSharp AppAppHost name (also used in Shortcuts)
    debugASP.NET DefaultEnable additional logging & diagnostics
    contentRootapp.settings dirASP.NET Content Root Directory
    webRootwwwroot/ASP.NET Web Root Directory
    apiPath/apiPath of Sharp APIs
    defaultRedirectDefault Fallback RedirectPath
    dbOrmLite Dialect: sqlite, sqlserver, mysql postgres
    db.connectionRDBMS Connection String
    redis.connectionServiceStack.Redis Connection String
    filesVFS provider: filesystem, s3, azureblob
    files.configVirtual File System JS Object Configuration
    checkForModifiedPagesAfterSecsHow long to check backing VFS provider for changes
    defaultFileCacheExpirySecsHow long to preserve static file caches for
    defaultUrlCacheExpirySecsHow long to preserve URL caches for
    featuresList of plugins to load
    markdownProviderMarkDigMarkdown provider: MarkdownDeep, Markdig
    jsMinifierNUglifyJS Minifier: NUglify, ServiceStack
    cssMinifierNUglifyCSS Minifier: NUglify, ServiceStack
    htmlMinifierNUglifyHTML Minifier: NUglify, ServiceStack
    iconfavicon.icoRelative or Absolute Path to Shortcut & Desktop icon
    appNameUnique Id to identify Desktop App (snake-case)
    descriptionShort Description of Desktop App (20-150 chars)
    tagsDesktop App Tags space-delimited, snake-case, 3 max
    +
    Plugins.Add(new DesktopFeature {
    +    // access role for Script, File & Download services
    +    AccessRole = Config.DebugMode 
    +        ? RoleNames.AllowAnon
    +        : RoleNames.Admin,
    +    ImportParams = { // app.settings you want auto-populated in your App
    +        "debug",
    +        "connect",
    +    },
    +    // Create a URL Scheme proxy rule for each registered site
    +    ProxyConfigs = sites.Keys.Map(baseUrl => new Uri(baseUrl))
    +        .Map(uri => new ProxyConfig {
    +            Scheme = uri.Scheme,
    +            TargetScheme = uri.Scheme,
    +            Domain = uri.Host,
    +            AllowCors = true,
    +            IgnoreHeaders = { "X-Frame-Options", "Content-Security-Policy" }, 
    +        })
    +});
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/adhoc-querying/01.html b/wwwroot/gfm/adhoc-querying/01.html new file mode 100644 index 0000000..6147f1a --- /dev/null +++ b/wwwroot/gfm/adhoc-querying/01.html @@ -0,0 +1,9 @@ +
    var context = new ScriptContext {
    +    ScriptMethods = {
    +        new DbScriptsAsync(),
    +    }
    +};
    +context.Container.AddSingleton<IDbConnectionFactory>(() => new OrmLiteConnectionFactory(
    +    connectionString, SqlServer2012Dialect.Provider));
    +context.Init();
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/adhoc-querying/01.md b/wwwroot/gfm/adhoc-querying/01.md new file mode 100644 index 0000000..9f83d81 --- /dev/null +++ b/wwwroot/gfm/adhoc-querying/01.md @@ -0,0 +1,10 @@ +```csharp +var context = new ScriptContext { + ScriptMethods = { + new DbScriptsAsync(), + } +}; +context.Container.AddSingleton(() => new OrmLiteConnectionFactory( + connectionString, SqlServer2012Dialect.Provider)); +context.Init(); +``` diff --git a/wwwroot/gfm/adhoc-querying/02.html b/wwwroot/gfm/adhoc-querying/02.html new file mode 100644 index 0000000..5bd4846 --- /dev/null +++ b/wwwroot/gfm/adhoc-querying/02.html @@ -0,0 +1,3 @@ +
    container.Register<IDbConnectionFactory>(c => new OrmLiteConnectionFactory(
    +    $"Data Source={filePath};Read Only=true", SqliteDialect.Provider));
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/adhoc-querying/02.md b/wwwroot/gfm/adhoc-querying/02.md new file mode 100644 index 0000000..033029a --- /dev/null +++ b/wwwroot/gfm/adhoc-querying/02.md @@ -0,0 +1,4 @@ +```csharp +container.Register(c => new OrmLiteConnectionFactory( + $"Data Source={filePath};Read Only=true", SqliteDialect.Provider)); +``` diff --git a/wwwroot/gfm/adhoc-querying/03.html b/wwwroot/gfm/adhoc-querying/03.html new file mode 100644 index 0000000..89f7b1f --- /dev/null +++ b/wwwroot/gfm/adhoc-querying/03.html @@ -0,0 +1,12 @@ +
    container.Register<IDbConnectionFactory>(c => 
    +    new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider));
    +
    +using (var db = container.Resolve<IDbConnectionFactory>().Open())
    +{
    +    db.CreateTable<Order>();
    +    db.CreateTable<Customer>();
    +    db.CreateTable<Product>();
    +    TemplateQueryData.Customers.Each(x => db.Save(x, references:true));
    +    db.InsertAll(TemplateQueryData.Products);
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/adhoc-querying/03.md b/wwwroot/gfm/adhoc-querying/03.md new file mode 100644 index 0000000..cc826b5 --- /dev/null +++ b/wwwroot/gfm/adhoc-querying/03.md @@ -0,0 +1,13 @@ +```csharp +container.Register(c => + new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider)); + +using (var db = container.Resolve().Open()) +{ + db.CreateTable(); + db.CreateTable(); + db.CreateTable(); + TemplateQueryData.Customers.Each(x => db.Save(x, references:true)); + db.InsertAll(TemplateQueryData.Products); +} +``` diff --git a/wwwroot/gfm/adhoc-querying/04.html b/wwwroot/gfm/adhoc-querying/04.html new file mode 100644 index 0000000..4fa86a1 --- /dev/null +++ b/wwwroot/gfm/adhoc-querying/04.html @@ -0,0 +1,121 @@ +

    The combination of features in the new Templates makes easy work of typically tedious tasks, e.g. if you were tasked to create a report +that contained all information about a Northwind Order displayed on a +single page, you can create a new page at:

    + +

    packed with all Queries you need to run and execute them with a DB Script and display them +with a HTML Scripts:

    +
    <!--
    +title: Order Report
    +-->
    +
    +{{ `SELECT o.Id, OrderDate, CustomerId, Freight, e.Id as EmployeeId, s.CompanyName as ShipVia, 
    +           ShipAddress, ShipCity, ShipPostalCode, ShipCountry
    +      FROM ${sqlQuote("Order")} o
    +           INNER JOIN
    +           Employee e ON o.EmployeeId = e.Id
    +           INNER JOIN
    +           Shipper s ON o.ShipVia = s.Id
    +     WHERE o.Id = @id` 
    +  |> dbSingle({ id }) |> to => order }}
    +
    +{{#with order}}
    +  {{ "table table-striped" |> to => className }}
    +  <style>table {border: 5px solid transparent} th {white-space: nowrap}</style>
    +  
    +  <div style="display:flex">
    +      {{ order |> htmlDump({ caption: 'Order Details', className }) }}
    +      {{ `SELECT * FROM Customer WHERE Id = @CustomerId` 
    +         |> dbSingle({ CustomerId }) |> htmlDump({ caption: `Customer Details`, className }) }}
    +      {{ `SELECT Id,LastName,FirstName,Title,City,Country,Extension FROM Employee WHERE Id = @EmployeeId` 
    +         |> dbSingle({ EmployeeId }) |> htmlDump({ caption: `Employee Details`, className }) }}
    +  </div>
    +
    +  {{ `SELECT p.ProductName, ${sqlCurrency("od.UnitPrice")} UnitPrice, Quantity, Discount
    +        FROM OrderDetail od
    +             INNER JOIN
    +             Product p ON od.ProductId = p.Id
    +       WHERE OrderId = @id`
    +      |> dbSelect({ id }) 
    +      |> htmlDump({ caption: "Line Items", className }) }}
    +{{else}}
    +  {{ `There is no Order with id: ${id}` }}
    +{{/with}}
    +

    This will let you view the complete details of any order at the following URL:

    + +

    +

    +SQL Studio Example

    +

    To take the ad hoc SQL Query example even further, it also becomes trivial to implement a SQL Viewer to run ad hoc queries on your App's configured database.

    +

    +

    The Northwind SQL Viewer above was developed using the 2 #Script Pages below:

    +

    +/northwind/sql/index.html +

    +

    A Sharp Page to render the UI, shortcut links to quickly see the last 10 rows of each table, a <textarea/> to capture the SQL Query which +is sent to an API on every keystroke where the results are displayed instantly:

    +
    <h2>Northwind SQL Viewer</h2>
    +
    +<textarea name="sql">select * from "Customer" order by Id desc limit 10</textarea>
    +<ul class="tables">
    +  <li>Category</li>
    +  <li>Customer</li>
    +  <li>Employee</li>
    +  <li>Order</li>
    +  <li>Product</li>
    +  <li>Supplier</li>
    +</ul>
    +
    +<div class="preview"></div>
    +<style>/*...*/</style>
    +
    +<script>
    +let textarea = document.querySelector("textarea");
    +let listItems = document.querySelectorAll('.tables li');
    +for (let i=0; i<listItems.length; i++) {
    +  listItems[i].addEventListener('click', function(e){
    +    var table = e.target.innerHTML;
    +    textarea.value = 'select * from "' + table + '" order by Id desc limit 10';
    +    textarea.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
    +  });
    +}
    +// Enable Live Preview of SQL
    +textarea.addEventListener("input", livepreview, false);
    +livepreview({ target: textarea });
    +function livepreview(e) {
    +  let el = e.target;
    +  let sel = '.preview';
    +  if (el.value.trim() == "") {
    +    document.querySelector(sel).innerHTML = "";
    +    return;
    +  }
    +  let formData = new FormData();
    +  formData.append("sql", el.value);
    +  fetch("api", {
    +    method: "post",
    +    body: formData
    +  }).then(function(r) { return r.text(); })
    +    .then(function(r) { document.querySelector(sel).innerHTML = r; });
    +}
    +</script>
    +

    +/northwind/sql/api.html +

    +

    All that's left is to implement the API which just needs to check to ensure the SQL does not contain any destructive operations using the +isUnsafeSql DB filter, if it doesn't execute the SQL with the dbSelect DB Filter, generate a HTML Table with htmlDump and return +the partial HTML fragment with return:

    +
    {{#if isUnsafeSql(sql) }} 
    +    {{ `<div class="alert alert-danger">Potentially unsafe SQL detected</div>` |> return }}
    +{{/if}}
    +
    +{{ sql |> dbSelect |> htmlDump |> return }}
    +

    +Live Development Workflow

    +

    Thanks to the live development workflow of #Script Pages, this is the quickest way we've previously been able to implement any of this functionality. +Where all development can happen at runtime with no compilation or builds, yielding a highly productive iterative workflow to implement common functionality +like viewing ad hoc SQL Queries in Excel or even just to rapidly prototype APIs so they can be consumed immediately by Client Applications before +formalizing them into Typed ServiceStack Services where they can take advantage of its rich typed metadata and ecosystem.

    +
    \ No newline at end of file diff --git a/wwwroot/gfm/adhoc-querying/04.md b/wwwroot/gfm/adhoc-querying/04.md new file mode 100644 index 0000000..ec786ba --- /dev/null +++ b/wwwroot/gfm/adhoc-querying/04.md @@ -0,0 +1,134 @@ +The combination of features in the new Templates makes easy work of typically tedious tasks, e.g. if you were tasked to create a report +that contained all information about a [Northwind Order](http://rockwind-sqlite.web-app.io/northwind/order?id=10643) displayed on a +single page, you can create a new page at: + + - [/northwind/order-report/_id.html](https://github.com/ServiceStack/dotnet-app/blob/master/src/apps/rockwind/northwind/order-report/_id.html) + +packed with all Queries you need to run and execute them with a [DB Script](http://sharpscript.net/docs/db-scripts) and display them +with a [HTML Scripts](http://sharpscript.net/docs/html-scripts): + +```hbs + + +{{ `SELECT o.Id, OrderDate, CustomerId, Freight, e.Id as EmployeeId, s.CompanyName as ShipVia, + ShipAddress, ShipCity, ShipPostalCode, ShipCountry + FROM ${sqlQuote("Order")} o + INNER JOIN + Employee e ON o.EmployeeId = e.Id + INNER JOIN + Shipper s ON o.ShipVia = s.Id + WHERE o.Id = @id` + |> dbSingle({ id }) |> to => order }} + +{{#with order}} + {{ "table table-striped" |> to => className }} + + +
    + {{ order |> htmlDump({ caption: 'Order Details', className }) }} + {{ `SELECT * FROM Customer WHERE Id = @CustomerId` + |> dbSingle({ CustomerId }) |> htmlDump({ caption: `Customer Details`, className }) }} + {{ `SELECT Id,LastName,FirstName,Title,City,Country,Extension FROM Employee WHERE Id = @EmployeeId` + |> dbSingle({ EmployeeId }) |> htmlDump({ caption: `Employee Details`, className }) }} +
    + + {{ `SELECT p.ProductName, ${sqlCurrency("od.UnitPrice")} UnitPrice, Quantity, Discount + FROM OrderDetail od + INNER JOIN + Product p ON od.ProductId = p.Id + WHERE OrderId = @id` + |> dbSelect({ id }) + |> htmlDump({ caption: "Line Items", className }) }} +{{else}} + {{ `There is no Order with id: ${id}` }} +{{/with}} +``` + +This will let you view the complete details of any order at the following URL: + + - [/northwind/order-report/10643](http://rockwind-sqlite.web-app.io/northwind/order-report/10643) + +[![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/rockwind/order-report.png)](http://rockwind-sqlite.web-app.io/northwind/order-report/10643) + +### SQL Studio Example + +To take the ad hoc SQL Query example even further, it also becomes trivial to implement a SQL Viewer to run ad hoc queries on your App's configured database. + +[![](https://raw.githubusercontent.com/ServiceStack/Assets/master/img/livedemos/rockwind/sql-viewer.png)](http://rockwind-sqlite.web-app.io/northwind/sql/) + +The [Northwind SQL Viewer](http://rockwind-sqlite.web-app.io/northwind/sql/) above was developed using the 2 #Script Pages below: + +#### [/northwind/sql/index.html](https://github.com/sharp-apps/rockwind/blob/master/northwind/sql/index.html) + +A Sharp Page to render the UI, shortcut links to quickly see the last 10 rows of each table, a ` +
      +
    • Category
    • +
    • Customer
    • +
    • Employee
    • +
    • Order
    • +
    • Product
    • +
    • Supplier
    • +
    + +
    + + + +``` + +#### [/northwind/sql/api.html](https://github.com/sharp-apps/rockwind/blob/master/northwind/sql/api.html) + +All that's left is to implement the API which just needs to check to ensure the SQL does not contain any destructive operations using the +`isUnsafeSql` DB filter, if it doesn't execute the SQL with the `dbSelect` DB Filter, generate a HTML Table with `htmlDump` and return +the partial HTML fragment with `return`: + +```hbs +{{#if isUnsafeSql(sql) }} + {{ `
    Potentially unsafe SQL detected
    ` |> return }} +{{/if}} + +{{ sql |> dbSelect |> htmlDump |> return }} +``` + +### Live Development Workflow + +Thanks to the live development workflow of #Script Pages, this is the quickest way we've previously been able to implement any of this functionality. +Where all development can happen at runtime with no compilation or builds, yielding a highly productive iterative workflow to implement common functionality +like viewing ad hoc SQL Queries in Excel or even just to rapidly prototype APIs so they can be consumed immediately by Client Applications before +formalizing them into Typed ServiceStack Services where they can take advantage of its rich typed metadata and ecosystem. \ No newline at end of file diff --git a/wwwroot/gfm/api-reference/01.html b/wwwroot/gfm/api-reference/01.html new file mode 100644 index 0000000..0d58f88 --- /dev/null +++ b/wwwroot/gfm/api-reference/01.html @@ -0,0 +1,18 @@ +
    var context = new ScriptContext { 
    +    Args = {
    +        [ScriptConstants.MaxQuota] = 10000,
    +        [ScriptConstants.DefaultCulture] = CultureInfo.CurrentCulture,
    +        [ScriptConstants.DefaultDateFormat] = "yyyy-MM-dd",
    +        [ScriptConstants.DefaultDateTimeFormat] = "u",
    +        [ScriptConstants.DefaultTimeFormat] = "h\\:mm\\:ss",
    +        [ScriptConstants.DefaultFileCacheExpiry] = TimeSpan.FromMinutes(1),
    +        [ScriptConstants.DefaultUrlCacheExpiry] = TimeSpan.FromMinutes(1),
    +        [ScriptConstants.DefaultIndent] = "\t",
    +        [ScriptConstants.DefaultNewLine] = Environment.NewLine,
    +        [ScriptConstants.DefaultJsConfig] = "excludetypeinfo",
    +        [ScriptConstants.DefaultStringComparison] = StringComparison.Ordinal,
    +        [ScriptConstants.DefaultTableClassName] = "table",
    +        [ScriptConstants.DefaultErrorClassName] = "alert alert-danger",
    +    }
    +}.Init();
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/api-reference/01.md b/wwwroot/gfm/api-reference/01.md new file mode 100644 index 0000000..1d81fc3 --- /dev/null +++ b/wwwroot/gfm/api-reference/01.md @@ -0,0 +1,19 @@ +```csharp +var context = new ScriptContext { + Args = { + [ScriptConstants.MaxQuota] = 10000, + [ScriptConstants.DefaultCulture] = CultureInfo.CurrentCulture, + [ScriptConstants.DefaultDateFormat] = "yyyy-MM-dd", + [ScriptConstants.DefaultDateTimeFormat] = "u", + [ScriptConstants.DefaultTimeFormat] = "h\\:mm\\:ss", + [ScriptConstants.DefaultFileCacheExpiry] = TimeSpan.FromMinutes(1), + [ScriptConstants.DefaultUrlCacheExpiry] = TimeSpan.FromMinutes(1), + [ScriptConstants.DefaultIndent] = "\t", + [ScriptConstants.DefaultNewLine] = Environment.NewLine, + [ScriptConstants.DefaultJsConfig] = "excludetypeinfo", + [ScriptConstants.DefaultStringComparison] = StringComparison.Ordinal, + [ScriptConstants.DefaultTableClassName] = "table", + [ScriptConstants.DefaultErrorClassName] = "alert alert-danger", + } +}.Init(); +``` diff --git a/wwwroot/gfm/api-reference/02.html b/wwwroot/gfm/api-reference/02.html new file mode 100644 index 0000000..2b5c1b1 --- /dev/null +++ b/wwwroot/gfm/api-reference/02.html @@ -0,0 +1,10 @@ +
    var fs = new FileSystemVirtualFiles("~/template-files".MapProjectPath());
    +foreach (var file in fs.GetAllMatchingFiles("*.html"))
    +{
    +    if (!MyAllowFile(file)) continue;
    +    using (var stream = file.OpenRead())
    +    {
    +        context.VirtualFiles.WriteFile(file.VirtualPath, stream);
    +    }
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/api-reference/02.md b/wwwroot/gfm/api-reference/02.md new file mode 100644 index 0000000..953c912 --- /dev/null +++ b/wwwroot/gfm/api-reference/02.md @@ -0,0 +1,11 @@ +```csharp +var fs = new FileSystemVirtualFiles("~/template-files".MapProjectPath()); +foreach (var file in fs.GetAllMatchingFiles("*.html")) +{ + if (!MyAllowFile(file)) continue; + using (var stream = file.OpenRead()) + { + context.VirtualFiles.WriteFile(file.VirtualPath, stream); + } +} +``` diff --git a/wwwroot/gfm/api-reference/03.html b/wwwroot/gfm/api-reference/03.html new file mode 100644 index 0000000..a5e7814 --- /dev/null +++ b/wwwroot/gfm/api-reference/03.html @@ -0,0 +1,22 @@ +
    public class MarkdownScriptPlugin : IScriptPlugin
    +{
    +    public bool RegisterPageFormat { get; set; } = true;
    +
    +    public void Register(ScriptContext context)
    +    {
    +        if (RegisterPageFormat)
    +            context.PageFormats.Add(new MarkdownPageFormat());
    +        
    +        context.FilterTransformers["markdown"] = MarkdownPageFormat.TransformToHtml;
    +        
    +        context.ScriptMethods.Add(new MarkdownScriptMethods());
    +
    +        context.ScriptBlocks.Add(new MarkdownScriptBlock());
    +    }
    +}
    +

    The MarkdownScriptPlugin is pre-registered when using the #Script Pages, for +all other contexts it can be registered and customized with:

    +
    var context = new ScriptContext {
    +    Plugins = { new MarkdownScriptPlugin { RegisterPageFormat = false } }
    +}.Init();
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/api-reference/03.md b/wwwroot/gfm/api-reference/03.md new file mode 100644 index 0000000..0150545 --- /dev/null +++ b/wwwroot/gfm/api-reference/03.md @@ -0,0 +1,27 @@ +```csharp +public class MarkdownScriptPlugin : IScriptPlugin +{ + public bool RegisterPageFormat { get; set; } = true; + + public void Register(ScriptContext context) + { + if (RegisterPageFormat) + context.PageFormats.Add(new MarkdownPageFormat()); + + context.FilterTransformers["markdown"] = MarkdownPageFormat.TransformToHtml; + + context.ScriptMethods.Add(new MarkdownScriptMethods()); + + context.ScriptBlocks.Add(new MarkdownScriptBlock()); + } +} +``` + +The `MarkdownScriptPlugin` is pre-registered when using the [#Script Pages](/docs/sharp-pages), for +all other contexts it can be registered and customized with: + +```csharp +var context = new ScriptContext { + Plugins = { new MarkdownScriptPlugin { RegisterPageFormat = false } } +}.Init(); +``` diff --git a/wwwroot/gfm/apps/01.html b/wwwroot/gfm/apps/01.html new file mode 100644 index 0000000..3311b92 --- /dev/null +++ b/wwwroot/gfm/apps/01.html @@ -0,0 +1,312 @@ +

    Spirals is a good example showing how easy it is to create .NET Core Desktop Web Apps utilizing HTML5's familiar and simple development +model to leverage advanced Web Technologies like SVG in a fun, interactive and live development experience.

    +
    +

    YouTube: youtu.be/2FFRLxs7orU

    +
    +

    +

    You can run this Gist Desktop App via URL Scheme from (Windows Desktop App):

    +

    app://spirals

    +

    Or via command-line:

    +
    $ app open spirals
    +
    +

    Cross platform (Default Browser):

    +
    $ x open spirals
    +
    +

    +The Making of Spirals

    +

    Spirals is a good example showing how easy it is to create .NET Core Desktop Web Apps utilizing HTML5's familiar and simple development model to +leverage advanced Web Technologies like SVG in a fun, interactive and live development experience.

    +

    To start install the dotnet app global tool:

    +
    $ dotnet tool install -g app
    +
    +

    Then create a folder for our app called spirals and initialize and empty Web App with app init:

    +
    $ md spirals
    +$ cd spirals && app init
    +
    +

    This generates a minimal Web App but you could also start from any of the more complete +Web App Templates.

    +

    Now let's open the folder up for editing in our preferred text editor, VS Code:

    +
    $ code .
    +
    +

    +

    To start developing our App we just have to run app on the command-line which in VS Code we can open with Terminal > New Terminal or the Ctrl+Shift+` shortcut key. This will open our minimal App:

    +

    +

    The _layout.html shown above is currently where all +the action is which we'll quickly walk through:

    +
    <i hidden>{{ '/js/hot-fileloader.js' |> ifDebugIncludeScript }}</i>
    +

    This gives us a Live Development experience in debug mode where it injects a script that will detect file changes on Save and automatically reload the page at the current scroll offset.

    +
    {{ 'menu' |> partial({ 
    +        '/':           'Home',
    +        '/metadata':   '/metadata',
    +    }) 
    +}}
    +

    This evaluates the included _menu-partial.html with the links +to different routes we want in our Menu on top of the page.

    +
    <div id="body" class="container">
    +    <h2>{{title}}</h2>
    +    
    +    {{ page }}
    +</div>
    +

    The body of our App is used to render the title and contents of each page.

    +
    {{ scripts |> raw }}
    +

    If pages include any scripts they'll be rendered in the bottom of the page.

    +
    +

    The raw filter prevents the output from being HTML encoded.

    +
    +

    The other 2 files included is app.settings containing the +name of our App and debug true setting to run our App in Debug mode:

    +
    debug true
    +name My App
    +
    +

    The template only has one page index.html containing the title +of the page in a page argument which the _layout.html has access to without evaluating the page, anything after is the page contents:

    +
    <!-- 
    +title: Home Page
    +-->
    +
    +This is the home page.
    +

    We can now save changes to any of the pages and see our changes reflected instantly in the running App. But we also have access to an even better +live-development experience than preview as you save with preview as you type :)

    +

    +Live Previews

    +

    To take advantage of this we can exploit one of the features available in all ServiceStack Apps by clicking on /metadata Menu Item to view the +Metadata page containing links to our Apps Services, links to Metadata Services and any registered plugins:

    +

    +

    Then click on Debug Inspector to open a real-time REPL, which is normally used to get rich insights from a running App:

    +

    +

    But can also be used for executing ad hoc Template Scripts. Here we can drop in any mix of HTML and templates to view results in real-time.

    +

    In this case we want to generate SVG spirals by drawing a circle at each point along a +Archimedean spiral function which was initially used as a base and with the help of the live REPL was +quickly able to apply some constants to draw the tall & narrow spirals we want:

    +
    <svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((1) * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((1) * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="10" fill="rgb(0,100,0)" stroke="black" stroke-width="1"/>
    +{{/each}} 
    +</svg>
    +

    We can further explore different spirals by modifying x and y cos/sin constants:

    +

    +

    Out of the spirals we've seen lets pick one of the interesting ones and add it to our index.html, let's also enhance them by modifying the fill and +radius properties with different weightings and compare them side-by-side:

    +
    <svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((5) * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((1) * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="10" fill="rgb(0,100,0)" stroke="black" stroke-width="1"/>
    +{{/each}} 
    +</svg>
    +
    +<svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((5) * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((1) * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="10" fill="rgb(0,{{it*1.4}},0)" stroke="black" stroke-width="1"/>
    +{{/each}} 
    +</svg>
    +
    +<svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((5) * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((1) * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="{{it*0.1}}" fill="rgb(0,{{it*1.4}},0)" stroke="black" stroke-width="1"/>
    +{{/each}} 
    +</svg>
    +
    +

    You can use ALT+LEFT + ALT+RIGHT shortcut keys to navigate back and forward to the home page.

    +
    +

    Great, hitting save again will show us the effects of each change side-by-size:

    +

    +

    +Multiplying

    +

    Now that we have the effect that we want, let's go back to the debug inspector and see what a number of different spirals look side-by-side +by wrapping our last svg snippet in another each block:

    +
    <table>{{#each i in range(0, 4) }}
    +<svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((1)   * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((1+i) * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="{{it*0.1}}" fill="rgb(0,{{it*1.4}},0)" stroke="black" stroke-width="1"/>
    +{{/each}} 
    +</svg>
    +{{/each}}
    +

    We can prefix our snippet with <table> as a temp workaround to force them to display side-by-side in Debug Inspector. In order to +for spirals to distort we'll only change 1 of the axis, as they're tall & narrow lets explore along the y-axis:

    +

    +

    We're all setup to begin our pattern explorer expedition where we can navigate across the range() index both incrementally and logarithmically across +to quickly discover new aesthetically pleasing patterns :)

    +

    +

    Let's go back to our App and embody our multi spiral viewer in a new multi.html page containing:

    +
    {{#each i in range(0, 4) }}
    +<svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((5)   * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((1+i) * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="{{it*0.1}}" fill="rgb(0,{{it*1.4}},0)" stroke="black" stroke-width="1"/>
    +{{/each}} 
    +</svg>
    +{{/each}}
    +

    Then make it navigable by adding a link to our new page in the _layout.html menu:

    +
    {{ 'menu' |> partial({
    +     '/':           'Home',
    +     '/multi':      'Multi',
    +     '/metadata':   '/metadata',
    +   })
    +}}
    +

    Where upon save, our creation will reveal itself in the App's menu:

    +

    +

    +Animating

    +

    With the help of SVG's <animate> we can easily bring our spirals to life +by animating different properties on the generated SVG circles.

    +

    As we have to wait for the animation to complete before trying out different effects, we won't benefit from Debug Inspector's live REPL +so let's jump straight in and create a new animated.html and add a link to it in the menu:

    +
    {{ 'menu' |> partial({
    +     '/':           'Home',
    +     '/multi':      'Multi',
    +     '/animated':   'Animated',
    +     '/metadata':   '/metadata',
    +   })
    +}}
    +

    Then populate it with a copy of multi.html and sprinkle in some <animate> elements to cycle through different <circle> property values. +We're entering the "creative process" of our App where we can try out different values, hit Save and watch the effects of our tuning eventually +arriving at a combination we like:

    +
    {{#each i in range(0, 4) }}
    +<svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((5)   * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((1+i) * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="{{it*0.1}}" fill="rgb(0,{{it*1.4}},0)" stroke="black" stroke-width="1">
    +    <animate attributeName="fill" values="green;yellow;red;green" dur="{{it%10}}s" repeatCount="indefinite" />
    +    <animate attributeName="opacity" values="1;.5;1" dur="5s" repeatCount="indefinite" />
    +    <animate attributeName="cx" values="{{x}};{{x*1.02}};{{x*0.98}};{{x}}" dur="3s" repeatCount="indefinite" />
    +    <animate attributeName="cy" values="{{y}};{{y*1.02}};{{y*0.98}};{{y}}" dur="3s" repeatCount="indefinite" />
    +  </circle>
    +{{/each}} 
    +</svg>
    +{{/each}}
    +

    Although hard to capture in a screenshot, we can sit back and watch our living, breathing Spirals :)

    +

    +
    +

    Checkout spirals.web-app.io for the animated version.

    +
    +

    +Navigating

    +

    Lets expand our App beyond these static Spirals by enabling some navigation, this is easily done by adding the snippet below on the top of the home page:

    +
    {{ from ?? 1 | toInt |> to => from }}
    +<div style="text-align:right;margin:-54px 0 30px 0">
    +  {{#if from > 1}} <a href="?from={{ max(from-1,0) }}" title="{{max(from-1,0)}}">previous</a> |{{/if}}
    +  {{from}} | <a href="?from={{ from+1 }}" title="{{max(from-1,0)}}">next</a>
    +</div>
    +

    Whilst the multi.html and animated.html pages can skip by 4:

    +
    {{ from ?? 1 | toInt |> to => from }}
    +<div style="text-align:right;margin:-54px 0 30px 0">
    +  {{#if from > 1}} <a href="?from={{ max(from-4,0) }}" title="{{max(from-1,0)}}">previous</a> |{{/if}}
    +  {{from}} | <a href="?from={{ from+4 }}" title="{{max(from-1,0)}}">next</a>
    +</div>
    +

    Then changing the index.html SVG fragment to use the from value on the y-axis:

    +
    <svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((5)    * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((from) * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="10" fill="rgb(0,100,0)" stroke="black" stroke-width="1"/>
    +{{/each}} 
    +</svg>
    +

    Whilst the multi.html and animated.html pages can use it in its range(from, 4) function:

    +
    {{#each i in range(from, 4) }}
    +<svg height="640" width="240">
    +{{#each range(180) }}
    +  {{ var x = 120 + 100 * cos((5) * it * 0.02827) }}
    +  {{ var y = 320 + 300 * sin((1+i)  * it * 0.02827) }}
    +  <circle cx="{{x}}" cy="{{y}}" r="{{it*0.1}}" fill="rgb(0,{{it*1.4}},0)" stroke="black" stroke-width="1"/>
    +{{/each}} 
    +</svg>
    +{{/each}}
    +

    With navigation activated we can quickly scroll through and explore different spirals. To save ourselves the effort of finding them again lets +catalog our favorite picks and add them to a bookmarked list at the bottom of the page. Here are some interesting ones I've found for the home page:

    +
    <div>
    +  Jump to favorites: 
    +  {{#each [1,5,101,221,222,224,298,441,443,558,663,665,666,783,888] }}
    +    {{#if index > 0}} | {{/if}} {{#if from == it }} {{it}} {{else}} <a href="?from={{it}}">{{it}</a> {{/if}}
    +  {{/each}}
    +</div>
    +

    and my top picks for the multi.html and animated.html pages:

    +
    <div>
    +  Jump to favorites: 
    +  {{#each [1,217,225,229,441,449,661,669,673,885,1338,3326,3338,4330,8662,9330,11998] }}
    +    {{#if index > 0}} | {{/if}} {{#if from == it }} {{it}} {{else}} <a href="?from={{it}}">{{it}</a> {{/if}}
    +  {{/each}}
    +</div>
    +
    +

    If you've found more interesting ones, let me know!

    +
    +

    Now it's just a matter of signing off our digital piece by giving it a name in your app.settings:

    +
    name Spirals
    +
    +

    Which replaces the name in the menu and used in any shortcuts that are created, and with those finishing touches our App's journey +into the rich colorful world of SVG is complete:

    +

    +

    +Recap

    +

    If you've reached this far you've created a beautiful Desktop App without waiting for a single build or App restart, in a live and +interactive environment which let you rapidly prototype and get real-time feedback of any experimental changes, to produce a +rich graphical app with fast and fluid animations taking advantage of the impressive engineering invested in Chrome, implemented +declaratively by just generating SVG markup.

    +

    As Bret Victor shows us in his seminal Inventing on Principle presentation, being able to immediately visualize +changes gives us a deep connection and intuition with our creations that's especially important when creating visual software like UIs where +the rapid iterations lets us try out and perfect different ideas in the moment, as soon as we can think of them.

    +

    It's a good example of the type of App that would rarely be considered to be created with a native GUI toolkit and 2D drawing API, the amount +of effort and friction to achieve the same result would take the enjoyment out of the experience and the end result would have much less utility +where it wouldn't be able to be re-used on different OS's or other websites.

    +

    +Publishing your App

    +

    To share our digital masterpiece with the world we just need to publish it in a GitHub repo, which I've already done for my Spirals app at: +https://github.com/mythz/spirals.

    +

    Anyone will then be able to install your App by first downloading the app tool themselves (.NET Core 2.1 Required):

    +
    $ dotnet tool install -g app
    +
    +

    Then running install with the name of the repo and your GitHub User or Organization name in the --source argument:

    +
    $ app install spirals --source mythz
    +
    +

    Which installs instantly thanks to the 7kb .zip download that can then be opened by double-clicking on the generated Spirals Desktop Shortcut:

    +

    +

    +Publishing your App with binaries

    +

    The unique characteristics of Web Apps affords us different ways of publishing your App, e.g. to save users from needing to install the +app tool you can run publish in your App's directory:

    +
    $ app publish
    +
    +

    Which will copy your App to the publish/app folder and the app tool binaries in the publish/cef folder:

    +

    +

    A Desktop shortcut is also generated for convenience although this is dependent on where the App is installed on the end users computer. +If you know it will be in a fixed location you can update the Target and Start in properties to reference the cef\app.dll and +/app folder:

    +

    +

    This includes all app binaries needed to run Web Apps which compresses to 89 MB in a .zip or 61 MB in 7-zip .7z archive.

    +

    +Publishing a self-contained Windows 64 executable

    +

    But that's not all, we can even save end users who want to run your app the inconvenience of installing .NET Core :) by creating a self-contained +executable with:

    +
    $ app publish-exe
    +
    +

    +

    This downloads the WebWin self-contained .NET Core 2.1 binaries and copies then to publish/win folder with +the app copied to publish/app.

    +

    This publishing option includes a self-contained .NET Core with all app binaries which compresses to 121 MB in a .zip or 83 MB in 7-zip .7z archive.

    +

    +Publish to the world

    +

    To maximize reach and accessibility of your App leave a comment on the App Gallery +where after we link to it on Sharp Apps it will available to all users when they look for available apps in:

    +
    $ app list
    +
    +

    Which can then be installed with:

    +
    $ app install spirals
    +
    +

    +Learnable Living Apps

    +

    A beautiful feature of Web Apps is that they can be enhanced and customized in real-time whilst the App is running. No pre-installation of dev tools +or knowledge of .NET and C# is required, anyone can simply use any text editor to peek inside .html files to see how it works and see any of their +customizations visible in real-time.

    +

    Apps can be customized using both the JavaScript-like #Script language syntax for any "server" HTML generation +which can be further customized by any JavaScript on the client.

    +
    \ No newline at end of file diff --git a/wwwroot/gfm/apps/01.md b/wwwroot/gfm/apps/01.md new file mode 100644 index 0000000..1aee461 --- /dev/null +++ b/wwwroot/gfm/apps/01.md @@ -0,0 +1,438 @@ +Spirals is a good example showing how easy it is to create .NET Core Desktop Web Apps utilizing HTML5's familiar and simple development +model to leverage advanced Web Technologies like SVG in a fun, interactive and live development experience. + +> YouTube: [youtu.be/2FFRLxs7orU](https://youtu.be/Cf-vstYXrmY) + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/spirals/animated.png)](https://youtu.be/Cf-vstYXrmY) + +You can run this Gist Desktop App via URL Scheme from (Windows Desktop App): + +app://spirals + +Or via command-line: + + $ app open spirals + +Cross platform (Default Browser): + + $ x open spirals + +## The Making of Spirals + +Spirals is a good example showing how easy it is to create .NET Core Desktop Web Apps utilizing HTML5's familiar and simple development model to +leverage advanced Web Technologies like SVG in a fun, interactive and live development experience. + +To start install the dotnet `app` global tool: + + $ dotnet tool install -g app + +Then create a folder for our app called `spirals` and initialize and empty Web App with `app init`: + + $ md spirals + $ cd spirals && app init + +This generates a minimal Web App but you could also start from any of the more complete +[Web App Templates](http://sharpscript.net/docs/sharp-apps#getting-started). + +Now let's open the folder up for editing in our preferred text editor, VS Code: + + $ code . + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/app-init-code.png) + +To start developing our App we just have to run `app` on the command-line which in VS Code we can open with `Terminal > New Terminal` or the **Ctrl+Shift+`** shortcut key. This will open our minimal App: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/app-init-run.png) + +The [_layout.html](https://gist.github.com/gistlyn/5c9ee9031e53cd8f85bd0e14881ddaa8#file-_layout-html) shown above is currently where all +the action is which we'll quickly walk through: + +```html + +``` + +This gives us a Live Development experience in `debug` mode where it injects a script that will detect file changes **on Save** and automatically reload the page at the current scroll offset. + +```html +{{ 'menu' |> partial({ + '/': 'Home', + '/metadata': '/metadata', + }) +}} +``` + +This evaluates the included [_menu-partial.html](https://gist.github.com/gistlyn/5c9ee9031e53cd8f85bd0e14881ddaa8#file-_menu-partial-html) with the links +to different routes we want in our Menu on top of the page. + +```html +
    +

    {{title}}

    + + {{ page }} +
    +``` + +The body of our App is used to render the title and contents of each page. + +```html +{{ scripts |> raw }} +``` + +If pages include any `scripts` they'll be rendered in the bottom of the page. + +> The `raw` filter prevents the output from being HTML encoded. + +The other 2 files included is [app.settings](https://gist.github.com/gistlyn/5c9ee9031e53cd8f85bd0e14881ddaa8#file-app-settings) containing the +name of our App and `debug true` setting to run our App in Debug mode: + + debug true + name My App + +The template only has one page [index.html](https://gist.github.com/gistlyn/5c9ee9031e53cd8f85bd0e14881ddaa8#file-index-html) containing the `title` +of the page in a page argument which the `_layout.html` has access to without evaluating the page, anything after is the page contents: + +```html + + +This is the home page. +``` + +We can now **save** changes to any of the pages and see our changes reflected instantly in the running App. But we also have access to an even better +live-development experience than **preview as you save** with **preview as you type** :) + +### Live Previews + +To take advantage of this we can exploit one of the features available in all ServiceStack Apps by clicking on `/metadata` Menu Item to view the +[Metadata page](/metadata-page) containing links to our Apps Services, links to Metadata Services and any registered plugins: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/app-init-metadata.png) + +Then click on [Debug Inspector](/debugging#debug-inspector) to open a real-time REPL, which is normally used to get rich insights from a running App: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/app-init-debug-inspector.png) + +But can also be used for executing ad hoc Template Scripts. Here we can drop in any mix of HTML and templates to view results in real-time. + +In this case we want to generate SVG spirals by drawing a `circle` at each point along a +[Archimedean spiral function](https://stackoverflow.com/a/6824451/85785) which was initially used as a base and with the help of the live REPL was +quickly able to apply some constants to draw the **tall & narrow** spirals we want: + +```hbs + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((1) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((1) * it * 0.02827) }} + +{{/each}} + +``` + +We can further explore different spirals by modifying `x` and `y` cos/sin constants: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/spirals/single.gif) + +Out of the spirals we've seen lets pick one of the interesting ones and add it to our `index.html`, let's also enhance them by modifying the `fill` and +`radius` properties with different weightings and compare them side-by-side: + +```hbs + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((5) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((1) * it * 0.02827) }} + +{{/each}} + + + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((5) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((1) * it * 0.02827) }} + +{{/each}} + + + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((5) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((1) * it * 0.02827) }} + +{{/each}} + +``` + +> You can use `ALT+LEFT` + `ALT+RIGHT` shortcut keys to navigate back and forward to the home page. + +Great, hitting `save` again will show us the effects of each change side-by-size: + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/spirals/single-fill-radius.png)](http://spirals.web-app.io) + +### Multiplying + +Now that we have the effect that we want, let's go back to the debug inspector and see what a number of different spirals look side-by-side +by wrapping our last svg snippet in another each block: + +```hbs +{{#each i in range(0, 4) }} + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((1) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((1+i) * it * 0.02827) }} + +{{/each}} + +{{/each}} +``` + +We can prefix our snippet with `
    ` as a temp workaround to force them to display side-by-side in Debug Inspector. In order to +for spirals to distort we'll only change 1 of the axis, as they're tall & narrow lets explore along the y-axis: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/spirals/multi-01.png) + +We're all setup to begin our pattern explorer expedition where we can navigate across the `range()` index both incrementally and logarithmically across +to quickly discover new aesthetically pleasing patterns :) + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/spirals/multi.gif) + +Let's go back to our App and embody our multi spiral viewer in a new `multi.html` page containing: + +```hbs +{{#each i in range(0, 4) }} + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((5) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((1+i) * it * 0.02827) }} + +{{/each}} + +{{/each}} +``` + +Then make it navigable by adding a link to our new page in the `_layout.html` menu: + +```hbs +{{ 'menu' |> partial({ + '/': 'Home', + '/multi': 'Multi', + '/metadata': '/metadata', + }) +}} +``` + +Where upon save, our creation will reveal itself in the App's menu: + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/spirals/multi.png)](http://spirals.web-app.io/multi) + +### Animating + +With the help of SVG's [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate) we can easily bring our spirals to life +by animating different properties on the generated SVG circles. + +As we have to wait for the animation to complete before trying out different effects, we won't benefit from Debug Inspector's live REPL +so let's jump straight in and create a new `animated.html` and add a link to it in the menu: + +```hbs +{{ 'menu' |> partial({ + '/': 'Home', + '/multi': 'Multi', + '/animated': 'Animated', + '/metadata': '/metadata', + }) +}} +``` + +Then populate it with a copy of `multi.html` and sprinkle in some `` elements to cycle through different `` property values. +We're entering the "creative process" of our App where we can try out different values, hit **Save** and watch the effects of our tuning eventually +arriving at a combination we like: + +```hbs +{{#each i in range(0, 4) }} + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((5) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((1+i) * it * 0.02827) }} + + + + + + +{{/each}} + +{{/each}} +``` + +Although hard to capture in a screenshot, we can sit back and watch our living, breathing Spirals :) + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/spirals/animated.png)](http://spirals.web-app.io/animated?from=0) + +> Checkout [spirals.web-app.io](http://spirals.web-app.io/animated?from=0) for the animated version. + +### Navigating + +Lets expand our App beyond these static Spirals by enabling some navigation, this is easily done by adding the snippet below on the top of the home page: + +```hbs +{{ from ?? 1 | toInt |> to => from }} +
    + {{#if from > 1}} previous |{{/if}} + {{from}} | next +
    +``` + +Whilst the `multi.html` and `animated.html` pages can skip by 4: + +```hbs +{{ from ?? 1 | toInt |> to => from }} +
    + {{#if from > 1}} previous |{{/if}} + {{from}} | next +
    +``` + +Then changing the `index.html` SVG fragment to use the `from` value on the y-axis: + +```hbs + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((5) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((from) * it * 0.02827) }} + +{{/each}} + +``` + +Whilst the `multi.html` and `animated.html` pages can use it in its `range(from, 4)` function: + +```hbs +{{#each i in range(from, 4) }} + +{{#each range(180) }} + {{ var x = 120 + 100 * cos((5) * it * 0.02827) }} + {{ var y = 320 + 300 * sin((1+i) * it * 0.02827) }} + +{{/each}} + +{{/each}} +``` + +With navigation activated we can quickly scroll through and explore different spirals. To save ourselves the effort of finding them again lets +catalog our favorite picks and add them to a bookmarked list at the bottom of the page. Here are some interesting ones I've found for the home page: + +```hbs +
    + Jump to favorites: + {{#each [1,5,101,221,222,224,298,441,443,558,663,665,666,783,888] }} + {{#if index > 0}} | {{/if}} {{#if from == it }} {{it}} {{else}} {{it} {{/if}} + {{/each}} +
    +``` + +and my top picks for the `multi.html` and `animated.html` pages: + +```hbs +
    + Jump to favorites: + {{#each [1,217,225,229,441,449,661,669,673,885,1338,3326,3338,4330,8662,9330,11998] }} + {{#if index > 0}} | {{/if}} {{#if from == it }} {{it}} {{else}} {{it} {{/if}} + {{/each}} +
    +``` + +> If you've found more interesting ones, [let me know](https://github.com/mythz/spirals/issues)! + +Now it's just a matter of signing off our digital piece by giving it a name in your `app.settings`: + + name Spirals + +Which replaces the name in the `menu` and used in any shortcuts that are created, and with those finishing touches our App's journey +into the rich colorful world of SVG is complete: + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/spirals/spiral-nav.png)](http://spirals.web-app.io/animated) + +### Recap + +If you've reached this far you've created a beautiful Desktop App without waiting for a single build or App restart, in a live and +interactive environment which let you rapidly prototype and get real-time feedback of any experimental changes, to produce a +rich graphical app with fast and fluid animations taking advantage of the impressive engineering invested in Chrome, implemented +declaratively by just generating SVG markup. + +As Bret Victor shows us in his seminal [Inventing on Principle](https://vimeo.com/36579366) presentation, being able to immediately visualize +changes gives us a deep connection and intuition with our creations that's especially important when creating visual software like UIs where +the rapid iterations lets us try out and perfect different ideas in the moment, as soon as we can think of them. + +It's a good example of the type of App that would rarely be considered to be created with a native GUI toolkit and 2D drawing API, the amount +of effort and friction to achieve the same result would take the enjoyment out of the experience and the end result would have much less utility +where it wouldn't be able to be re-used on different OS's or other websites. + +## Publishing your App + +To share our digital masterpiece with the world we just need to publish it in a GitHub repo, which I've already done for my Spirals app at: +[https://github.com/mythz/spirals](github.com/mythz/spirals). + +Anyone will then be able to install your App by first downloading the `app` tool themselves ([.NET Core 2.1 Required](https://www.microsoft.com/net/download/dotnet-core/2.1)): + + $ dotnet tool install -g app + +Then running `install` with the name of the **repo** and your GitHub **User** or **Organization** name in the `--source` argument: + + $ app install spirals --source mythz + +Which installs instantly thanks to the `7kb` .zip download that can then be opened by double-clicking on the generated **Spirals** Desktop Shortcut: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/app-install-spirals.png) + +### Publishing your App with binaries + +The unique characteristics of Web Apps affords us different ways of publishing your App, e.g. to save users from needing to install the +`app` tool you can run `publish` in your App's directory: + + $ app publish + +Which will copy your App to the `publish/app` folder and the `app` tool binaries in the `publish/cef` folder: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/app-publish.png) + +A Desktop shortcut is also generated for convenience although this is dependent on where the App is installed on the end users computer. +If you know it will be in a fixed location you can update the **Target** and **Start in** properties to reference the `cef\app.dll` and +`/app` folder: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/app-shortcut-properties.png) + +This includes all `app` binaries needed to run Web Apps which compresses to **89 MB** in a `.zip` or **61 MB** in 7-zip `.7z` archive. + +### Publishing a self-contained Windows 64 executable + +But that's not all, we can even save end users who want to run your app the inconvenience of installing .NET Core :) by creating a self-contained +executable with: + + $ app publish-exe + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/app/app-publish-exe.png) + +This downloads the [WebWin](https://github.com/ServiceStack/WebWin) self-contained .NET Core 2.1 binaries and copies then to `publish/win` folder with +the app copied to `publish/app`. + +This publishing option includes a self-contained .NET Core with all `app` binaries which compresses to **121 MB** in a `.zip` or **83 MB** in 7-zip `.7z` archive. + +### Publish to the world + +To maximize reach and accessibility of your App leave a comment on the [App Gallery](https://gist.github.com/gistlyn/f555677c98fb235dccadcf6d87b9d098) +where after we link to it on [Sharp Apps](https://github.com/sharp-apps) it will available to all users when they look for available apps in: + + $ app list + +Which can then be installed with: + + $ app install spirals + +### Learnable Living Apps + +A beautiful feature of Web Apps is that they can be enhanced and customized in real-time whilst the App is running. No pre-installation of dev tools +or knowledge of .NET and C# is required, anyone can simply use any text editor to peek inside `.html` files to see how it works and see any of their +customizations visible in real-time. + +Apps can be customized using both the JavaScript-like [#Script language syntax](/docs/syntax) for any "server" HTML generation +which can be further customized by any JavaScript on the client. + diff --git a/wwwroot/gfm/apps/02.html b/wwwroot/gfm/apps/02.html new file mode 100644 index 0000000..e8b4441 --- /dev/null +++ b/wwwroot/gfm/apps/02.html @@ -0,0 +1,577 @@ +

    SharpData is a generic app for providing an instant UI around multiple RDBMS's:

    +
    +

    YouTube: youtu.be/GjVipOqwZMA

    +
    +

    +

    It makes use of the app dotnet tool for running Chromium +Gist Desktop Apps on-the-fly without installation, from a single URL that can also +mix in additional gists which can be used in SharpData to configure RDBMS's, copy SQLite databases and +apply per-database customizations to add navigable deep links and customized UI Views to each table resultset.

    +

    Whilst SharpData supports connecting to most popular RDBMS's, it's +especially useful for being able to deploy an instant stand-alone UI with an embedded SQLite databases which can be published independently in a gist and +launched from a single URL.

    +

    For an example of this in action we've published customized gists for the +Northwind and +Chinook SQLite databases which after installing the latest +app dotnet tool:

    +
    $ dotnet tool install -g app
    +$ app -version
    +
    +

    First time app is run it registers the app:// URL scheme allowing Windows x64 Desktop Apps to be launched from URLs:

    + +

    Or via command-line:

    +
    $ app open sharpdata mix northwind.sharpdata
    +$ app open sharpdata mix chinook.sharpdata
    +
    +

    Cross platform using the x dotnet tool (in Default Browser):

    +
    $ x open sharpdata mix northwind.sharpdata
    +$ x open sharpdata mix chinook.sharpdata
    +
    +

    Each of these options will download & run the latest version of SharpData along with a +copy of the northwind.sharpdata or +chinook.sharpdata gists on-the-fly containing the embedded SQLite DB along with any +UI customizations.

    +

    +Hosted as a .NET Core App

    +

    As NetCoreApps/SharpData is also a standard .NET Core project, it can also be deployed as a +normal stand-alone .NET Core Web App:

    +

    +https://sharpdata.netcore.io +

    +

    +Tiny footprint

    +

    An impressively capable .NET Core App that fits into a tiny 20kb .zip footprint thanks to Gist Desktop App's Architecture. It's small dynamic #Script & Vue TypeScript code-base also makes it highly customizable to tailor & further extend with +App-specific requirements - suitable for offering advanced system users a quick, capable customized read-only UI of your DBs.

    +

    SharpData started as a demonstration showing how productive #Script can be in the number of areas where +dynamic languages offer far superior productivity then the typical .NET approach of using C# to type an entire code-base & models.

    +

    For example a single #Script page provides a lot of the functionality in AutoQuery where it provides an instant HTTP API +(in all registered ServiceStack formats) around all registered RDBMS tables, in all OrmLite supported RBDMS's, that includes support for custom fields, +multiple querying options, paging, multi OrderBy's in a parameterized SQL query executed with OrmLite's SQL async DB APIs:

    +

    +AutoQuery Script

    +

    +/db/_db/_table/index.html +

    +
    {{ {namedConnection:db} |> if (db && db != 'main') |> useDb }}
    +
    +```code|quiet
    +var ignore = ['db','fields','format','skip','take','orderBy']
    +var fields = qs.fields ? qs.fields.split(',').map(x => sqlQuote(x)).join(',') : '*'
    +var sql = `SELECT ${fields} FROM ${sqlQuote(table)}`
    +var filters = []
    +var queryMap = qs.toObjectDictionary().withoutKeys(ignore)
    +#each queryMap.Keys.toList()
    +    var search = queryMap[it.sqlVerifyFragment()].sqlVerifyFragment();
    +    #if search == '=null' || search == '!=null'
    +        `${sqlQuote(it)} ${search=='=null' ? 'IS' : 'IS NOT'} NULL` |> addTo => filters
    +        queryMap[it] = null
    +    else if search.startsWith('=')
    +        `${sqlQuote(it)} = @${it}` |> addTo => filters
    +        queryMap[it] = search.substring(1).coerce()
    +    else if search.startsWith('<=') || search.startsWith('>=') || search.startsWith('!=')
    +        `${sqlQuote(it)} ${search.substring(0,2)} @${it}` |> addTo => filters
    +        queryMap[it] = search.substring(2).coerce()
    +    else if search.startsWith('<') || search.startsWith('>')
    +        `${sqlQuote(it)} ${search.substring(0,1)} @${it}` |> addTo => filters
    +        queryMap[it] = search.substring(1).coerce()
    +    else if search.endsWith(',')
    +        `${sqlQuote(it)} IN (${search.trimEnd(',').split(',').map(i=>i.toLong()).join(',')})` |>addTo=>filters
    +        queryMap[it] = null
    +    else if search.startsWith('%') || search.endsWith('%')
    +        `${sqlQuote(it).sqlCast('varchar')} LIKE @${it}` |> addTo => filters
    +    else
    +        `${sqlQuote(it).sqlCast('varchar')} = @${it}` |> addTo => filters
    +    /if
    +/each
    +#if !filters.isEmpty()
    +    sql = `${sql} WHERE ${filters.join(' AND ')}`
    +/if
    +#if qs.orderBy
    +    sql = `${sql} ORDER BY ${sqlOrderByFields(qs.orderBy)}`
    +/if
    +#if qs.skip || qs.take
    +    sql = `${sql} ${sqlLimit(qs.skip,qs.take)}`
    +/if
    +sql |> dbSelect(queryMap) |> return
    +```
    +{{ ifError |> show(sql) }}
    +{{htmlError}}
    +
    +

    The _ prefixes in the path utilizes Page Based Routing allowing for +CoC based +Clean URL routes without needing to define & maintain separate routes where the +same script supports querying all registered multitenancy databases.

    +

    +Instant Customizable RDBMS UI

    +

    The SharpData project essentially provides a UI around this script, surfacing its features & give +it instant utility which ended up being so useful that it's become the quickest way to perform fast adhoc DB queries as it's easy to configure +which RDBMS's & tables to show in a simple text file, easy to customize its UI, enables 1-click export into Excel and its shortcut syntax +support in column filters is a fast way to perform quick adhoc queries.

    +

    +Quick Tour

    +

    We'll quickly go through some of its features to give you an idea of its capabilities, from the above screenshot we can some of its +filtering capabilities. All results displayed in the UI are queried using the above +sharpdata #Script HTTP API +which supports the following features:

    +

    +Filters

    +

    All query string parameter except for db,fields,format,skip,take,orderBy are treated as filters, where you can:

    +
      +
    • Use =null or !=null to search NULL columns
    • +
    • Use <=, <, >, >=, <>, != prefix to search with that operator
    • +
    • Use , trailing comma to perform an IN (values) search (integer columns only)
    • +
    • Use % suffix or prefix to perform a LIKE search
    • +
    • Use = prefix to perform a coerced "JS" search, for exact number, boolean, null and WCF date comparisons
    • +
    • Otherwise by default performs a "string equality" search where columns are casted and compared as strings
    • +
    +

    Here's the filtered list used in the above screenshot:

    +

    /db/northwind/Order?Id=>10200&CustomerId=V%&Freight=<=30&OrderDate=>1997-01-01

    +

    +Custom Field Selection

    +

    The column selection icon on the top left of the results lets you query custom select columns which is specified using ?fields:

    + +

    +Multiple OrderBy's

    +

    You can use AutoQuery Syntax to specify multiple Order By's:

    + +

    +Paging

    +

    Use ?skip and ?take to page through a result set

    +

    +Format

    +

    Use ?format to specify which Content-Type to return the results in, e.g:

    + +

    +Multitenancy

    +

    You can specify which registered DB to search using the path info, use main to query the default database:

    +
    /db/<named-db>/<table>
    +
    +

    +Open in Excel

    +

    SharpData detects if Excel is installed and lets you open the un-paged filtered resultset directly by clicking the Excel button

    +

    +

    This works seamlessly as it's able to "by-pass" the browser download where the query is performed by the back-end .NET Core Server who streams the response directly to the Users Downloads folder and launches it in Excel as soon as it's finished.

    +

    +Launching SharpData

    +

    To run SharpData in a .NET Core Desktop App you'll need latest app dotnet tool:

    +
    $ dotnet tool update -g app
    +
    +
    +

    If on macOS/Linux you can use the x dotnet tool instead to view SharpData in your default browser

    +
    +

    +Configure RDBMS from command-line

    +

    You can override which database to connect to by specifying it on the command line, e.g. here's an example of connecting to https://techstacks.io RDBMS:

    +
    $ app open sharpdata -db postgres -db.connection $TECHSTACKS_DB
    +
    +

    Which will open SharpData listing all of TechStack's RDBMS tables. If you have a lot of tables the Sidebar filter provides a quick way to +find the table you want, e.g:

    +

    +

    +app URL Schemes

    +

    What can be done with the open command on the command-line can also be done from a custom URL Scheme, a feature that opens up a myriad of new +possibilities as app can open Gist Desktop Apps from Gists or in public & private GitHub repositories, +where it's able to download and launch Apps on the fly with custom arguments - allowing a single URL to run a never installed Desktop App stored in a +Gist & pass it custom params to enable deep linking.

    +

    With this organizations could maintain a dashboard of links to its different Desktop Apps that anyone can access, especially useful as the +only software that's needed to run any Sharp Apps is the app dotnet tool which thanks to all +ServiceStack .dll's & dependencies being bundled with the tool, (including Vue/React/Bootstrap fontawesome and Material SVG Icon assets), +the only files that need to be published are the App's specific resources, which is how Apps like SharpData can be compressed in a +20kb .zip - a tiny payload that's viable to download the latest app each on each run, removing the pain & friction to distribute updates as +everyone's already running the latest version every time it's run.

    +

    Should you need to (e.g. large Sharp App or github.com is down) you can run your previously locally cached App using run:

    +
    $ app run sharpdata
    +
    +

    With Custom URL Schemes everyone with app installed can view any database they have network access to from specifying the db type and connection string in the URL:

    +
    app://sharpdata?db=postgres&db.connection={CONNECTION_STRING}
    +
    +
    +

    CONNECTION_STRING needs to be URL Encoded, e.g. with JS's encodeURIComponent()

    +
    +

    or by specifying an Environment variable containing the connection string:

    +
    app://sharpdata?db=postgres&db.connection=$TECHSTACKS_DB
    +
    +

    +Mix in Gists

    +

    In addition to Sharp Apps being downloaded and run on the fly, they're also able to take advantage of the dotnet tools mix support to +also download another Gist's content into the Sharp App's working directory.

    +

    With this you can publish a custom dataset in an SQLite database save it as a gist and generate a single URL that everyone can use to +download the database and open it in SharpData, e.g:

    +

    +app://sharpdata?mix=northwind.sqlite&db=sqlite&db.connection=northwind.sqlite +

    +

    It's possible to use the user-friendly northwind.sqlite alias here as it's published in the global mix.md directory where it links to the northwind.sqlite gist.

    +

    For your custom databases you use the Gist Id instead or if you plan to use this feature a lot you can override which mix.md document that +app should source its links from by specifying another Gist Id in the MIX_SOURCE Environment variable (or see below - to create a local alias).

    +

    But if you're already mixing in an external gist you may as well include a custom app.settings in the Gist so it's pre-configured with custom +RDBMS registrations and table lists, e.g:

    +

    +app://sharpdata?mix=northwind.sharpdata +

    +

    Which applies the northwind.sharpdata gist, which can also be referenced by Gist Id:

    +

    +app://sharpdata?mix=0ce0d5b828303f1cb4637450b563adbd +

    +

    Alternatively you may instead prefer to publish it to a private GitHub repo instead of a Gist which anyone can open up with:

    +
    app://user/sharpdata-private?token={TOKEN}
    +
    +

    The app dotnet tools will use the latest published GitHub release if there are any, otherwise will use the master.zip archive, +this feature can be used to maintain a working master repo and maintain control ver when to publish new versions of your custom SharpData App.

    +

    +app local aliases

    +

    Where ever you can use a Gist Id, you can assign a local user-friendly alias to use instead. So if you had a custom sqlite database and +sharpdata app.settings you could assign it to a local db alias with:

    +
    $ app alias db 0ce0d5b828303f1cb4637450b563adbd
    +
    +

    Which you'll be able to use in place of the Gist Id, e.g. via command-line:

    +
    $ app open sharpdata mix db
    +
    +

    or via URL Scheme:

    +
    app://sharpdata?mix=db
    +
    +

    Likewise the gist alias can also be used for referencing Gist Desktop Apps, e.g. we can assign the +redis gist app to use our preferred alias:

    +
    $ app alias local-redis 6de7993333b457445793f51f6f520ea8
    +
    +

    That we can open via command-line:

    +
    $ app open local-redis
    +
    +

    Or URL Scheme:

    +
    app://local-redis
    +
    +

    Or if we want to run our own modified copy of the Redis Desktop App, we can mix the Gist files to our +local directory:

    +
    $ app mix local-redis
    +
    +

    Make the changes we want, then run our local copy by running app (or x) without arguments:

    +
    $ app
    +
    +

    Other alias command include:

    +

    +View all aliases

    +
    $ app alias
    +
    +

    +View single alias

    +
    $ app alias mydb
    +
    +

    +Remove an alias

    +
    $ app unalias mydb
    +
    +

    +Custom SharpData UI

    +

    Each time a Gist Desktop App is opened it downloads and overrides the existing Gist with the latest version which it loads in a Gist VFS where any of its files can be overridden with a local copy.

    +

    As the App's working directory is preserved between restarts you can provide a custom app.settings at:

    +
    %USERPROFILE%\.sharp-apps\sharpdata\app.settings
    +
    +

    +Custom app.settings

    +

    Where you can perform basic customizations like which RDBMS's and tables you want to be able to access, e.g:

    +
    debug false
    +name Northwind & TechStacks UI
    +appName sharpdata
    +
    +db.connections[northwind]  { db:sqlite,   connection:'northwind.sqlite' }
    +db.connections[techstacks] { db:postgres, connection:$TECHSTACKS_DB }
    +
    +args.tables Customer,Order,OrderDetail,Category,Product,Employee,EmployeeTerritory,Shipper,Supplier,Region,Territory
    +args.tables_techstacks technology,technology_stack,technology_choice,organization,organization_member,post,post_comment,post_vote,custom_user_auth,user_auth_details,user_activity,page_stats
    +
    +

    Which will display both RDBMS Databases, showing only the user-specified tables in app.settings above:

    +

    +

    +Advanced Customizations

    +

    More advanced customizations can be added via dropping TypeScript/JavaScript source files in the /custom folder, e.g:

    + +

    Which is how the northwind.sharpdata and +chinook.sharpdata mix gists enable Customized Views for the Northwind +& Chinook databases via their dbConfig registrations below:

    +

    +chinook

    +
    dbConfig('chinook', {
    +    showTables: 'albums,artists,playlists,tracks,genres,media_types,customers,employees,invoices'.split(','),
    +    tableName: splitPascalCase,
    +    links: {
    +        albums: {
    +            ArtistId: (id:number) => `artists?filter=ArtistId:${id}`
    +        },
    +        employees: {
    +            ReportsTo: (id:number) => `employees?filter=EmployeeId:${id}`
    +        },
    +        invoices: {
    +            CustomerId: (id:number) => `customers?filter=CustomerId:${id}`
    +        },
    +        tracks: {
    +            AlbumId: (id:number) => `albums?filter=AlbumId:${id}`,
    +            MediaTypeId: (id:number) => `media_types?filter=MediaTypeId:${id}`,
    +            GenreId: (id:number) => `genres?filter=GenreId:${id}`,
    +        }
    +    },
    +    rowComponents: {
    +        albums: Album,
    +        artists: Artist,
    +        playlists: Playlist,
    +    }
    +});
    +

    +northwind

    +
    dbConfig('northwind', {
    +    showTables: 'Customer,Order,OrderDetail,Category,Product,Employee,Shipper,Supplier,Region'.split(','),
    +    tableName: splitPascalCase,
    +    links: {
    +        Order: {
    +            CustomerId: (id:string) => `Customer?filter=Id:${id}`,
    +            EmployeeId: (id:string) => `Employee?filter=Id:${id}`,
    +            ShipVia: (id:number) => `Shipper?filter=Id:${id}`,
    +        },
    +        OrderDetail: {
    +            OrderId: (id:string) => `Order?filter=Id:${id}`,
    +            ProductId: (id:string) => `Product?filter=Id:${id}`,
    +        },
    +        Product: {
    +            SupplierId: (id:number) => `Supplier?filter=Id:${id}`,
    +            CategoryId: (id:number) => `Category?filter=Id:${id}`,
    +        },
    +        Territory: {
    +            RegionId: (id:number) => `Region?filter=Id:${id}`,
    +        },
    +    },
    +    rowComponents: {
    +        Order,
    +        Customer,
    +    }
    +});
    +

    These db customizations let you specify which RDBMS tables & the order that they should be displayed, the table names text casing function, +which columns to linkify & any custom Row Components for different tables.

    +

    +Deploying Customizations

    +

    When deploying as a .NET Core project the customizations are deployed with your /wwwroot +as normal.

    +

    To make customizations available to load with the SharpData Gist Desktop App you'll need to publish the directory of customizations to a gist. +Here are the customizations for the northwind.sharpdata and +chinook.sharpdata gists:

    +

    +/dist-mix +

    + +

    You can publish a directory of files to a GitHub Gist using the x publish command with the +GitHub AccessToken with gist write access you want to write to, e.g:

    +
    $ cd northwind
    +$ x publish -token %TOKEN%
    +
    +

    +Viewing Customizations

    +

    When published these Gist Customizations can be viewed by gist id directly or by a user friendly gist mix or local alias:

    + +

    +Custom Row Components

    +

    Whilst a tabular grid view might be a natural UI for browsing a database for devs, we can do better since we have the full UI source code of the Vue components. +A filtered tabular view makes it fast to find the record you're interested in, but it's not ideal for quickly finding related information about an Entity.

    +

    To provide a more customized UX for different App UIs, SharpData includes support for "Row Components" +(defined in /wwwroot/custom) to be able to quickly drill down & view +richer info on any record.

    +

    For example when viewing an Order, it's natural to want to view the Order Details with it, enabled with the custom Vue component registration below:

    +
    @Component({ template:
    +`<div v-if="id">
    +    <jsonviewer :value="details" />
    +</div>
    +<div v-else class="alert alert-danger">Order Id needs to be selected</div>`
    +})
    +class Order extends RowComponent {
    +    details:any[] = [];
    +
    +    get id() { return this.row.Id; }
    +
    +    async mounted() {
    +        this.details = await sharpData(this.db,'OrderDetail',{ OrderId: this.id });
    +    }
    +}
    +

    All Row components are injected with the db, table properties, the entire row object that was selected as well as the Column Schema definition for that table. Inside the component you're free to display anything, in this case we're using the sharpData helper for calling the server #Script HTTP API to get it to fetch all OrderDetail entries for this order.

    +
    +

    If the resultset is filtered without the Order Id PK it can't fetch its referenced data, so displays an error instead

    +
    +

    The jsonviewer component used is similar to ServiceStack's +HTML5 auto pages to quickly view contents of any object.

    +

    The registerRowComponent(db,table,VueComponent,componentName) API is used to register this component with SharpData to make it available to render any order.

    +

    With the Order component registered we can now drill down into any Order to view its Order Details:

    +

    +

    You're free to render any kind of UI in the row component, e.g. here's the Customer.ts row component used to render a richer view for Customers:

    +
    @Component({ template:
    +`<div v-if="id" class="pl-2">
    +    <h3 class="text-success">{{customer.ContactName}}</h3>
    +    <table class="table table-bordered" style="width:auto">
    +        <tr>
    +            <th>Contact</th>
    +            <td>{{ customer.ContactName }} ({{ customer.ContactTitle }})</td>
    +        </tr>
    +        <tr>
    +            <th>Address</th>
    +            <td>
    +                <div>{{ customer.Address }}</div>
    +                <div>{{ customer.City }}, {{ customer.PostalCode }}, {{ customer.Country }}</div>
    +            </td>
    +        </tr>
    +        <tr>
    +            <th>Phone</th>
    +            <td>{{ customer.Phone }}</td>
    +        </tr>
    +        <tr v-if="customer.Fax">
    +            <th>Fax</th>
    +            <td>{{ customer.Fax }}</td>
    +        </tr>
    +    </table>
    +    <jsonviewer :value="orders" />
    +</div>
    +<div v-else class="alert alert-danger">Customer Id needs to be selected</div>`
    +})
    +class Customer extends RowComponent {
    +
    +    customer:any = null;
    +    orders:any[] = [];
    +
    +    get id() { return this.row.Id; }
    +
    +    async mounted() {
    +        this.customer = (await sharpData(this.db,this.table,{ Id: this.id }))[0];
    +        const fields = 'Id,EmployeeId,OrderDate,Freight,ShipVia,ShipCity,ShipCountry';
    +        this.orders = await sharpData(this.db,'Order',{ CustomerId: this.id, fields })
    +    }
    +}
    +

    Which looks like:

    +

    +

    +SharpData .NET Core Project

    +

    Whilst NetCoreApps/SharpData can live a charmed life as a Desktop App, it's also just a regular ServiceStack .NET Core App with a Startup.cs and AppHost that can be developed, published and deployed as you're used to, here's an instance of it deployed as a .NET Core App on Linux:

    +

    +sharpdata.netcore.io +

    +
    +

    For best experience we recommend running against local network databases

    +
    +

    It's a unique ServiceStack App in that it doesn't contain any ServiceStack Services as it's only using pre-existing functionality already built into ServiceStack, +#Script for its HTTP APIs and a Vue SPA for its UI, so requires no .dll's need to be deployed with it.

    +

    It uses the same Vue SPA solution as vue-lite to avoid npm's size & complexity where you only need to run TypeScript's tsc -w to enable its live-reload dev UX which provides its instant feedback during development.

    +

    Some other of its unique traits is that instead of manually including all the Vue framework .js libraries, it instead references the new ServiceStack.Desktop.dll for its Vue framework libraries and its Material design SVG icons which are referenced as normal file references:

    +
    {{ [
    +    `/lib/js/vue/vue.min.js`,
    +    `/lib/js/vue-router/vue-router.min.js`,
    +    `/lib/js/vue-class-component/vue-class-component.min.js`,
    +    `/lib/js/vue-property-decorator/vue-property-decorator.min.js`,
    +    `/lib/js/@servicestack/desktop/servicestack-desktop.min.js`,
    +    `/lib/js/@servicestack/client/servicestack-client.min.js`,
    +    `/lib/js/@servicestack/vue/servicestack-vue.min.js`,
    +] |> map => `<script src="${it}"></script>` |> joinln |> raw }}
    +

    But instead of needing to exist on disk & deployed with your project it's referencing the embedded resources in ServiceStack.Desktop.dll and only the bundled assets need to be deployed with your project which is using the built-in NUglify support in the dotnet tools to produce its highly optimized/minified bundle without needing to rely on any npm tooling when publishing the .NET Core App:

    +
    <Target Name="Bundle" BeforeTargets="AfterPublish">
    +    <Exec Command="x run _bundle.ss -to /bin/Release/net5/publish/wwwroot" />
    +</Target>
    +

    The included /typings are just the TypeScript definitions for each library which +TypeScript uses for its static analysis & its great dev UX in IDEs & VSCode, but are only needed during development and not deployed with the project.

    +

    +Publish to Gist Desktop App

    +

    The primary way SharpData is distributed is as a Gist Desktop App, where it's able to provide instant utility by running on a users local machine inside a native Chromium Desktop App making it suitable for a much broader use-case as a fast, lightweight, always up-to-date Desktop App with deeper Windows integration all packaged in a tiny 20kb .zip footprint. There's no need to provision servers, setup CI, manage cloud hosting resources, you can simply run a script to update a Gist where its latest features are immediately available to your end users the next time it's run.

    +

    To run, test & publish it as a Desktop App you can use the pre-made scripts in package.json. +Rider provides a nice UX here as it lets you run each individual script directly from their json editor:

    +

    +

    Essentially to package it into a Sharp App you just need to run the pack script which will bundle & copy all required assets into the /dist folder which you can then test locally in a .NET Core Desktop App by running app in that folder:

    +
    $ cd dist
    +$ app
    +
    +

    The mix-* scripts copies the db customizations so you have something to test it with which you can run with the run-test script.

    +

    The publish-app script is if you want to publish it to a Gist Desktop App, where you will need it to provide the GitHub AccessToken with write access to the Gist User Account you want to publish it to. Adding an appName and description to app.settings will publish it to the Global App Registry, make it publicly discoverable and allow anyone to open your App using your user-friendly appName alias, otherwise they can run it using the Gist Id or Gist URL.

    +

    Alternatively the contents of the dist/ folder can be published to a GitHub repo (public or private) and run with:

    +
    $ app open <user>/<repo>
    +
    +

    Or link to it with its custom URL Scheme:

    +
    app://<user>/repo
    +
    +

    If it's in a private repo they'll need to either provide an AccessToken in the GITHUB_TOKEN Environment variable or using the -token argument:

    +
    $ app open <user>/<repo> -token <token>
    +
    +

    URL Scheme:

    +
    app://<user>/repo?token=<token>
    +
    +

    +RDBMS Configuration

    +

    When running as a .NET Core App you'd need to register which RDBMS's you want to use with OrmLite's configuration, e.g. the screenshot above registers an SQLite northwind.sqlite database and the https://techstacks.io PostgreSQL Database:

    +
    container.Register<IDbConnectionFactory>(c => new OrmLiteConnectionFactory(
    +    MapProjectPath("~/northwind.sqlite"), SqliteDialect.Provider));
    +
    +var dbFactory = container.Resolve<IDbConnectionFactory>();
    +dbFactory.RegisterConnection("techstacks",
    +     Environment.GetEnvironmentVariable("TECHSTACKS_DB"),
    +     PostgreSqlDialect.Provider);
    +

    By default it shows all Tables in each RDBMS, but you can limit it to only show a user-defined list of tables with #Script Arguments:

    +
    Plugins.Add(new SharpPagesFeature {
    +    //...
    +    Args = {
    +        //Only display user-defined list of tables:
    +        ["tables"] = "Customer,Order,OrderDetail,Category,Product,Employee,EmployeeTerritory,Shipper,Supplier,Region,Territory",
    +        ["tables_techstacks"] = "technology,technology_stack,technology_choice,organization,organization_member,post,post_comment,post_vote,custom_user_auth,user_auth_details,user_activity,page_stats",
    +    }
    +});
    +

    When running as a Sharp App it's instead configured in its +app.settings, here's equivalent settings for the above configuration:

    +
    # Configure below. Supported dialects: sqlite, mysql, postgres, sqlserver
    +db.connections[northwind]  { db:sqlite,   connection:'northwind.sqlite' }
    +db.connections[techstacks] { db:postgres, connection:$TECHSTACKS_DB }
    +
    +args.tables Customer,Order,OrderDetail,Category,Product,Employee,EmployeeTerritory,Shipper,Supplier,Region,Territory
    +args.tables_techstacks technology,technology_stack,technology_choice,organization,organization_member,post,post_comment,post_vote,custom_user_auth,user_auth_details,user_activity,page_stats
    +
    +

    +Feedback

    +

    We hope SharpData serves useful in some capacity, whether it's being able to quickly develop and Ship a UI to stakeholders or as a template to develop .NET Core Apps that you can distribute as Sharp Apps, as an example to explore the delivery and platform potential of URL schemes and install-less Desktop Apps or just as an inspiration for areas where #Script shines & the different kind of Apps you can create with it.

    +

    Whilst app is Windows 64 only, you can use the x cross-platform tool and its xapp:// URL scheme to run Sharp Apps on macOS/Linux, it just wont have access to any of its Window Integration features.

    +
    \ No newline at end of file diff --git a/wwwroot/gfm/apps/02.md b/wwwroot/gfm/apps/02.md new file mode 100644 index 0000000..71bb61c --- /dev/null +++ b/wwwroot/gfm/apps/02.md @@ -0,0 +1,663 @@ +[SharpData](https://github.com/NetCoreApps/SharpData) is a generic app for providing an instant UI around multiple RDBMS's: + +> YouTube: [youtu.be/GjVipOqwZMA](https://youtu.be/GjVipOqwZMA) + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/sharpdata-custom-appsettings.png)](https://youtu.be/GjVipOqwZMA) + +It makes use of the [app](https://docs.servicestack.net/netcore-windows-desktop) dotnet tool for running Chromium +[Gist Desktop Apps](https://sharpscript.net/sharp-apps/gist-desktop-apps) on-the-fly without installation, from a single URL that can also +[mix in additional gists](https://docs.servicestack.net/mix-tool) which can be used in SharpData to configure RDBMS's, copy SQLite databases and +apply per-database customizations to add navigable deep links and customized UI Views to each table resultset. + +Whilst SharpData supports [connecting to most popular RDBMS's](https://github.com/ServiceStack/ServiceStack.OrmLite#8-flavours-of-ormlite-is-on-nuget), it's +especially useful for being able to deploy an instant stand-alone UI with an embedded SQLite databases which can be published independently in a gist and +launched from a single URL. + +For an example of this in action we've published customized gists for the +[Northwind](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/linq/downloading-sample-databases) and +[Chinook](https://www.sqlitetutorial.net/sqlite-sample-database/) SQLite databases which after installing the latest +[app](https://docs.servicestack.net/netcore-windows-desktop) dotnet tool: + + $ dotnet tool install -g app + $ app -version + +First time `app` is run it registers the [app:// URL scheme](#app-url-schemes) allowing Windows x64 Desktop Apps to be launched from URLs: + + + +Or via command-line: + + $ app open sharpdata mix northwind.sharpdata + $ app open sharpdata mix chinook.sharpdata + +Cross platform using the [x dotnet tool](https://docs.servicestack.net/dotnet-tool) (in Default Browser): + + $ x open sharpdata mix northwind.sharpdata + $ x open sharpdata mix chinook.sharpdata + +Each of these options will download & run the latest version of [SharpData](https://github.com/NetCoreApps/SharpData) along with a +copy of the [northwind.sharpdata](https://gist.github.com/gistlyn/0ce0d5b828303f1cb4637450b563adbd) or +[chinook.sharpdata](https://gist.github.com/gistlyn/96b10369daf94897531810841cb097f2) gists on-the-fly containing the embedded SQLite DB along with any +UI customizations. + +#### Hosted as a .NET Core App + +As [NetCoreApps/SharpData](https://github.com/NetCoreApps/SharpData) is also a standard .NET Core project, it can also be deployed as a +normal stand-alone .NET Core Web App: + +### [https://sharpdata.netcore.io](https://sharpdata.netcore.io) + +### Tiny footprint + +An impressively capable .NET Core App that fits into a tiny **20kb .zip** footprint thanks to [Gist Desktop App's Architecture](/gist-desktop-apps). It's small dynamic `#Script` & Vue TypeScript code-base also makes it highly customizable to tailor & further extend with +App-specific requirements - suitable for offering advanced system users a quick, capable customized read-only UI of your DBs. + +**SharpData** started as a demonstration showing how productive [#Script](https://sharpscript.net) can be in the number of areas where +dynamic languages offer far superior productivity then the typical .NET approach of using C# to type an entire code-base & models. + +For example a single `#Script` page provides a lot of the functionality in [AutoQuery](https://docs.servicestack.net/autoquery-rdbms) where it provides an instant HTTP API +(in all registered ServiceStack formats) around all registered RDBMS tables, in all OrmLite supported RBDMS's, that includes support for custom fields, +multiple querying options, paging, multi OrderBy's in a parameterized SQL query executed with OrmLite's SQL async DB APIs: + +## AutoQuery Script + +### [/db/_db/_table/index.html](https://github.com/NetCoreApps/SharpData/blob/master/wwwroot/db/_db/_table/index.html) + + {{ {namedConnection:db} |> if (db && db != 'main') |> useDb }} + + ```code|quiet + var ignore = ['db','fields','format','skip','take','orderBy'] + var fields = qs.fields ? qs.fields.split(',').map(x => sqlQuote(x)).join(',') : '*' + var sql = `SELECT ${fields} FROM ${sqlQuote(table)}` + var filters = [] + var queryMap = qs.toObjectDictionary().withoutKeys(ignore) + #each queryMap.Keys.toList() + var search = queryMap[it.sqlVerifyFragment()].sqlVerifyFragment(); + #if search == '=null' || search == '!=null' + `${sqlQuote(it)} ${search=='=null' ? 'IS' : 'IS NOT'} NULL` |> addTo => filters + queryMap[it] = null + else if search.startsWith('=') + `${sqlQuote(it)} = @${it}` |> addTo => filters + queryMap[it] = search.substring(1).coerce() + else if search.startsWith('<=') || search.startsWith('>=') || search.startsWith('!=') + `${sqlQuote(it)} ${search.substring(0,2)} @${it}` |> addTo => filters + queryMap[it] = search.substring(2).coerce() + else if search.startsWith('<') || search.startsWith('>') + `${sqlQuote(it)} ${search.substring(0,1)} @${it}` |> addTo => filters + queryMap[it] = search.substring(1).coerce() + else if search.endsWith(',') + `${sqlQuote(it)} IN (${search.trimEnd(',').split(',').map(i=>i.toLong()).join(',')})` |>addTo=>filters + queryMap[it] = null + else if search.startsWith('%') || search.endsWith('%') + `${sqlQuote(it).sqlCast('varchar')} LIKE @${it}` |> addTo => filters + else + `${sqlQuote(it).sqlCast('varchar')} = @${it}` |> addTo => filters + /if + /each + #if !filters.isEmpty() + sql = `${sql} WHERE ${filters.join(' AND ')}` + /if + #if qs.orderBy + sql = `${sql} ORDER BY ${sqlOrderByFields(qs.orderBy)}` + /if + #if qs.skip || qs.take + sql = `${sql} ${sqlLimit(qs.skip,qs.take)}` + /if + sql |> dbSelect(queryMap) |> return + ``` + {{ ifError |> show(sql) }} + {{htmlError}} + + +The `_` prefixes in the path utilizes [Page Based Routing](https://sharpscript.net/docs/sharp-pages#page-based-routing) allowing for +[CoC](https://en.wikipedia.org/wiki/Convention_over_configuration) based +[Clean URL](https://en.wikipedia.org/wiki/Clean_URL) routes without needing to define & maintain separate routes where the +same script supports querying all [registered multitenancy databases](https://docs.servicestack.net/multitenancy#changedb-apphost-registration). + +### Instant Customizable RDBMS UI + +The [SharpData](https://github.com/NetCoreApps/SharpData) project essentially provides a UI around this script, surfacing its features & give +it instant utility which ended up being so useful that it's become the quickest way to perform fast adhoc DB queries as it's easy to configure +which RDBMS's & tables to show in a simple text file, easy to customize its UI, enables 1-click export into Excel and its shortcut syntax +support in column filters is a fast way to perform quick adhoc queries. + +### Quick Tour + +We'll quickly go through some of its features to give you an idea of its capabilities, from the above screenshot we can some of its +filtering capabilities. All results displayed in the UI are queried using the above +[sharpdata](https://github.com/NetCoreApps/SharpData/blob/master/wwwroot/db/_db/_table/index.html) `#Script` HTTP API +which supports the following features: + +### Filters + +All query string parameter except for `db,fields,format,skip,take,orderBy` are treated as filters, where you can: + + - Use `=null` or `!=null` to search `NULL` columns + - Use `<=`, `<`, `>`, `>=`, `<>`, `!=` prefix to search with that operator + - Use `,` trailing comma to perform an `IN (values)` search (integer columns only) + - Use `%` suffix or prefix to perform a `LIKE` search + - Use `=` prefix to perform a coerced "JS" search, for exact `number`, `boolean`, `null` and WCF date comparisons + - Otherwise by default performs a "string equality" search where columns are casted and compared as strings + +Here's the filtered list used in the above screenshot: + + [/db/northwind/Order?Id=>10200&CustomerId=V%&Freight=<=30&OrderDate=>1997-01-01](http://sharpdata.netcore.io/db/northwind/Order?format=json&Id=%3E10200&CustomerId=V%25&Freight=%3C%3D30&OrderDate=%3E1997-01-01&take=100) + +### Custom Field Selection + +The **column selection** icon on the top left of the results lets you query custom select columns which is specified using `?fields`: + + - [/db/northwind/Customer?fields=Id,CompanyName,ContactName,ContactTitle](https://sharpdata.netcore.io/db/northwind/Customer?format=json&fields=Id%2CCompanyName%2CContactName%2CContactTitle&take=100) + +### Multiple OrderBy's + +You can use [AutoQuery Syntax](https://docs.servicestack.net/autoquery-rdbms#multiple-orderbys) to specify multiple Order By's: + + - [/db/northwind/Customer?orderBy=-Id,CompanyName,-ContactName](https://sharpdata.netcore.io/db/northwind/Customer?format=json&orderBy=-Id,CompanyName,-ContactName) + +### Paging + +Use `?skip` and `?take` to page through a result set + +### Format + +Use `?format` to specify which **Content-Type** to return the results in, e.g: + + - [/db/northwind/Customer?format=html](https://sharpdata.netcore.io/db/northwind/Customer?format=html) + - [/db/northwind/Customer?format=json](https://sharpdata.netcore.io/db/northwind/Customer?format=json) + - [/db/northwind/Customer?format=csv](https://sharpdata.netcore.io/db/northwind/Customer?format=csv) + +### Multitenancy + +You can specify which registered DB to search using the path info, use `main` to query the default database: + + /db//
    + +### Open in Excel + +SharpData detects if **Excel** is installed and lets you open the un-paged filtered resultset directly by clicking the **Excel** button + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/sharpdata-excel.png) + +This works seamlessly as it's able to "by-pass" the browser download where the query is performed by the back-end .NET Core Server who streams the response directly to the Users **Downloads** folder and launches it in Excel as soon as it's finished. + +### Launching SharpData + +To run SharpData in a .NET Core Desktop App you'll need latest `app` dotnet tool: + + $ dotnet tool update -g app + +> If on macOS/Linux you can use the [x dotnet tool](https://docs.servicestack.net/dotnet-tool) instead to view SharpData in your default browser + +### Configure RDBMS from command-line + +You can override which database to connect to by specifying it on the command line, e.g. here's an example of connecting to https://techstacks.io RDBMS: + + $ app open sharpdata -db postgres -db.connection $TECHSTACKS_DB + +Which will open SharpData listing all of TechStack's RDBMS tables. If you have a lot of tables the **Sidebar filter** provides a quick way to +find the table you want, e.g: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/sharpdata-technology.png) + +### app URL Schemes + +What can be done with the `open` command on the command-line can also be done from a **custom URL Scheme**, a feature that opens up a myriad of new +possibilities as `app` can open [Gist Desktop Apps](https://sharpscript.net/docs/gist-desktop-apps) from Gists or in public & private GitHub repositories, +where it's able to download and launch Apps on the fly with custom arguments - allowing a single URL to run a **never installed** Desktop App stored in a +Gist & pass it custom params to enable **deep linking**. + +With this organizations could maintain a dashboard of links to its different Desktop Apps that anyone can access, especially useful as the +**only software** that's needed to run any [Sharp Apps](https://sharpscript.net/docs/sharp-apps) is the `app` dotnet tool which thanks to all +ServiceStack .dll's & dependencies being bundled with the tool, (including Vue/React/Bootstrap fontawesome and Material SVG Icon assets), +the only files that need to be published are the App's specific resources, which is how Apps like **SharpData** can be compressed in a +**20kb .zip** - a tiny payload that's viable to download the latest app each on each run, removing the pain & friction to distribute updates as +everyone's already running the latest version every time it's run. + +Should you need to (e.g. large Sharp App or github.com is down) you can run your previously locally cached App using `run`: + + $ app run sharpdata + +With Custom URL Schemes everyone with `app` installed can view any database they have network access to from specifying the db type and connection string in the URL: + + app://sharpdata?db=postgres&db.connection={CONNECTION_STRING} + +> CONNECTION_STRING needs to be URL Encoded, e.g. with JS's `encodeURIComponent()` + +or by specifying an Environment variable containing the connection string: + + app://sharpdata?db=postgres&db.connection=$TECHSTACKS_DB + +### Mix in Gists + +In addition to Sharp Apps being downloaded and run on the fly, they're also able to take advantage of the dotnet tools [mix support](https://docs.servicestack.net/mix-tool) to +also download another Gist's content into the Sharp App's working directory. + +With this you can publish a custom dataset in an SQLite database save it as a gist and **generate a single URL** that everyone can use to +download the database and open it in **SharpData**, e.g: + +

    app://sharpdata?mix=northwind.sqlite&db=sqlite&db.connection=northwind.sqlite

    + +It's possible to use the user-friendly `northwind.sqlite` alias here as it's published in the global [mix.md](https://gist.github.com/gistlyn/9b32b03f207a191099137429051ebde8) directory where it links to the [northwind.sqlite gist](https://gist.github.com/gistlyn/97d0bcd3ebd582e06c85f8400683e037). + +For your custom databases you use the **Gist Id** instead or if you plan to use this feature a lot you can override which `mix.md` document that +`app` should source its links from by specifying another **Gist Id** in the `MIX_SOURCE` Environment variable (or see below - to create a local alias). + +But if you're already mixing in an external gist you may as well include a custom `app.settings` in the Gist so it's pre-configured with custom +RDBMS registrations and table lists, e.g: + +

    app://sharpdata?mix=northwind.sharpdata

    + +Which applies the [northwind.sharpdata gist](https://gist.github.com/gistlyn/0ce0d5b828303f1cb4637450b563adbd), which can also be referenced by **Gist Id**: + +

    app://sharpdata?mix=0ce0d5b828303f1cb4637450b563adbd

    + +Alternatively you may instead prefer to publish it to a private GitHub repo instead of a Gist which anyone can open up with: + + app://user/sharpdata-private?token={TOKEN} + +The `app` dotnet tools will use the **latest published GitHub release** if there are any, otherwise will use the **master.zip** archive, +this feature can be used to maintain a working master repo and maintain control ver when to publish new versions of your custom SharpData App. + +### app local aliases + +Where ever you can use a Gist Id, you can assign a local user-friendly alias to use instead. So if you had a custom **sqlite** database and +sharpdata **app.settings** you could assign it to a local **db** alias with: + + $ app alias db 0ce0d5b828303f1cb4637450b563adbd + +Which you'll be able to use in place of the Gist Id, e.g. via command-line: + + $ app open sharpdata mix db + +or via URL Scheme: + + app://sharpdata?mix=db + +Likewise the gist alias can also be used for referencing [Gist Desktop Apps](https://sharpscript.net/docs/gist-desktop-apps), e.g. we can assign the +[redis gist app](https://gist.github.com/gistlyn/6de7993333b457445793f51f6f520ea8) to use our preferred alias: + + $ app alias local-redis 6de7993333b457445793f51f6f520ea8 + +That we can open via command-line: + + $ app open local-redis + +Or URL Scheme: + + app://local-redis + +Or if we want to run our own modified copy of the Redis Desktop App, we can [mix](https://docs.servicestack.net/mix-tool) the Gist files to our +local directory: + + $ app mix local-redis + +Make the changes we want, then run our local copy by running `app` (or `x`) without arguments: + + $ app + +Other alias command include: + +#### View all aliases + + $ app alias + +#### View single alias + + $ app alias mydb + +#### Remove an alias + + $ app unalias mydb + + +### Custom SharpData UI + +Each time a Gist Desktop App is opened it downloads and overrides the existing Gist with the latest version which it loads in a [Gist VFS](https://docs.servicestack.net/virtual-file-system#gistvirtualfiles) where any of its files can be overridden with a local copy. + +As the App's working directory is preserved between restarts you can provide a custom `app.settings` at: + + %USERPROFILE%\.sharp-apps\sharpdata\app.settings + +#### Custom app.settings + +Where you can perform basic customizations like which RDBMS's and tables you want to be able to access, e.g: + +``` +debug false +name Northwind & TechStacks UI +appName sharpdata + +db.connections[northwind] { db:sqlite, connection:'northwind.sqlite' } +db.connections[techstacks] { db:postgres, connection:$TECHSTACKS_DB } + +args.tables Customer,Order,OrderDetail,Category,Product,Employee,EmployeeTerritory,Shipper,Supplier,Region,Territory +args.tables_techstacks technology,technology_stack,technology_choice,organization,organization_member,post,post_comment,post_vote,custom_user_auth,user_auth_details,user_activity,page_stats +``` + +Which will display both RDBMS Databases, showing only the user-specified tables in app.settings above: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/sharpdata-custom-appsettings.png) + +### Advanced Customizations + +More advanced customizations can be added via dropping TypeScript/JavaScript source files in the `/custom` folder, e.g: + + - [/wwwroot/custom/chinook.ts](https://github.com/NetCoreApps/SharpData/blob/master/wwwroot/custom/chinook.ts) + - [/wwwroot/custom/northwind.ts](https://github.com/NetCoreApps/SharpData/blob/master/wwwroot/custom/northwind.ts) + +Which is how the [northwind.sharpdata](https://gist.github.com/gistlyn/0ce0d5b828303f1cb4637450b563adbd) and +[chinook.sharpdata](https://gist.github.com/gistlyn/96b10369daf94897531810841cb097f2) mix gists enable Customized Views for the Northwind +& Chinook databases via their dbConfig registrations below: + +#### chinook + +```ts +dbConfig('chinook', { + showTables: 'albums,artists,playlists,tracks,genres,media_types,customers,employees,invoices'.split(','), + tableName: splitPascalCase, + links: { + albums: { + ArtistId: (id:number) => `artists?filter=ArtistId:${id}` + }, + employees: { + ReportsTo: (id:number) => `employees?filter=EmployeeId:${id}` + }, + invoices: { + CustomerId: (id:number) => `customers?filter=CustomerId:${id}` + }, + tracks: { + AlbumId: (id:number) => `albums?filter=AlbumId:${id}`, + MediaTypeId: (id:number) => `media_types?filter=MediaTypeId:${id}`, + GenreId: (id:number) => `genres?filter=GenreId:${id}`, + } + }, + rowComponents: { + albums: Album, + artists: Artist, + playlists: Playlist, + } +}); +``` + +#### northwind + +```ts +dbConfig('northwind', { + showTables: 'Customer,Order,OrderDetail,Category,Product,Employee,Shipper,Supplier,Region'.split(','), + tableName: splitPascalCase, + links: { + Order: { + CustomerId: (id:string) => `Customer?filter=Id:${id}`, + EmployeeId: (id:string) => `Employee?filter=Id:${id}`, + ShipVia: (id:number) => `Shipper?filter=Id:${id}`, + }, + OrderDetail: { + OrderId: (id:string) => `Order?filter=Id:${id}`, + ProductId: (id:string) => `Product?filter=Id:${id}`, + }, + Product: { + SupplierId: (id:number) => `Supplier?filter=Id:${id}`, + CategoryId: (id:number) => `Category?filter=Id:${id}`, + }, + Territory: { + RegionId: (id:number) => `Region?filter=Id:${id}`, + }, + }, + rowComponents: { + Order, + Customer, + } +}); +``` + +These db customizations let you specify which RDBMS tables & the order that they should be displayed, the table names text casing function, +which columns to linkify & any custom Row Components for different tables. + +### Deploying Customizations + +When deploying as a .NET Core project the customizations are deployed with your [/wwwroot](https://github.com/NetCoreApps/SharpData/tree/master/wwwroot) +as normal. + +To make customizations available to load with the SharpData Gist Desktop App you'll need to publish the directory of customizations to a gist. +Here are the customizations for the [northwind.sharpdata](https://gist.github.com/gistlyn/0ce0d5b828303f1cb4637450b563adbd) and +[chinook.sharpdata](https://gist.github.com/gistlyn/96b10369daf94897531810841cb097f2) gists: + +### [/dist-mix](https://github.com/NetCoreApps/SharpData/tree/master/dist-mix) + - [/chinook](https://github.com/NetCoreApps/SharpData/tree/master/dist-mix/chinook) + - [/custom](https://github.com/NetCoreApps/SharpData/tree/master/dist-mix/chinook/custom) + - [chinook.js](https://github.com/NetCoreApps/SharpData/blob/master/dist-mix/chinook/custom/chinook.js) - UI Customizations + - [app.settings](https://github.com/NetCoreApps/SharpData/blob/master/dist-mix/chinook/app.settings) - Custom App Settings + - [chinook.sqlite](https://github.com/NetCoreApps/SharpData/blob/master/dist-mix/chinook/chinook.sqlite) - Embedded SQLite database + - [/northwind](https://github.com/NetCoreApps/SharpData/tree/master/dist-mix/northwind) + - [/custom](https://github.com/NetCoreApps/SharpData/tree/master/dist-mix/chinook/northwind) + - [northwind.js](https://github.com/NetCoreApps/SharpData/blob/master/dist-mix/northwind/custom/northwind.js) - UI Customizations + - [app.settings](https://github.com/NetCoreApps/SharpData/blob/master/dist-mix/northwind/app.settings) - Custom App Settings + - [northwind.sqlite](https://github.com/NetCoreApps/SharpData/blob/master/dist-mix/northwind/northwind.sqlite) - Embedded SQLite database + +You can [publish a directory of files to a GitHub Gist](/sharp-apps/app-index#publishing-gist-desktop-apps) using the `x publish` command with the +GitHub **AccessToken** with gist write access you want to write to, e.g: + + $ cd northwind + $ x publish -token %TOKEN% + +### Viewing Customizations + +When published these Gist Customizations can be viewed by gist id directly or by a user friendly gist mix or local alias: + + + +### Custom Row Components + +Whilst a tabular grid view might be a natural UI for browsing a database for devs, we can do better since we have the full UI source code of the Vue components. +A filtered tabular view makes it fast to find the record you're interested in, but it's not ideal for quickly finding related information about an Entity. + +To provide a more customized UX for different App UIs, **SharpData** includes support for **"Row Components"** +(defined in [/wwwroot/custom](https://github.com/NetCoreApps/SharpData/tree/master/wwwroot/custom)) to be able to quickly drill down & view +richer info on any record. + +For example when viewing an **Order**, it's natural to want to view the **Order Details** with it, enabled with the custom Vue component registration below: + +```ts +@Component({ template: +`
    + +
    +
    Order Id needs to be selected
    ` +}) +class Order extends RowComponent { + details:any[] = []; + + get id() { return this.row.Id; } + + async mounted() { + this.details = await sharpData(this.db,'OrderDetail',{ OrderId: this.id }); + } +} +``` + +All Row components are injected with the `db`, `table` properties, the entire `row` object that was selected as well as the Column Schema definition for that table. Inside the component you're free to display anything, in this case we're using the `sharpData` helper for calling the server `#Script` HTTP API to get it to fetch all `OrderDetail` entries for this order. + +> If the resultset is filtered without the Order `Id` PK it can't fetch its referenced data, so displays an error instead + +The [jsonviewer](https://github.com/NetCoreApps/SharpData/blob/master/src/JsonViewer.ts) component used is similar to ServiceStack's +[HTML5 auto pages](https://docs.servicestack.net/html5reportformat) to quickly view contents of any object. + +The `registerRowComponent(db,table,VueComponent,componentName)` API is used to register this component with **SharpData** to make it available to render any order. + +With the `Order` component registered we can now drill down into any **Order** to view its **Order Details**: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/sharpdata-custom-rowcomponents.png) + +You're free to render any kind of UI in the row component, e.g. here's the [Customer.ts](https://github.com/NetCoreApps/SharpData/blob/master/src/components/Custom/Customer.ts) row component used to render a richer view for Customers: + +```ts +@Component({ template: +`
    +

    {{customer.ContactName}}

    +
    + + + + + + + + + + + + + + + + +
    Contact{{ customer.ContactName }} ({{ customer.ContactTitle }})
    Address +
    {{ customer.Address }}
    +
    {{ customer.City }}, {{ customer.PostalCode }}, {{ customer.Country }}
    +
    Phone{{ customer.Phone }}
    Fax{{ customer.Fax }}
    + + +
    Customer Id needs to be selected
    ` +}) +class Customer extends RowComponent { + + customer:any = null; + orders:any[] = []; + + get id() { return this.row.Id; } + + async mounted() { + this.customer = (await sharpData(this.db,this.table,{ Id: this.id }))[0]; + const fields = 'Id,EmployeeId,OrderDate,Freight,ShipVia,ShipCity,ShipCountry'; + this.orders = await sharpData(this.db,'Order',{ CustomerId: this.id, fields }) + } +} +``` + +Which looks like: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/sharpdata-customer-rowcomponent.png) + +### SharpData .NET Core Project + +Whilst [NetCoreApps/SharpData](https://github.com/NetCoreApps/SharpData) can live a charmed life as a Desktop App, it's also just a regular ServiceStack .NET Core App with a [Startup.cs](https://github.com/NetCoreApps/SharpData/blob/master/Startup.cs) and `AppHost` that can be developed, published and deployed as you're used to, here's an instance of it [deployed as a .NET Core App on Linux](https://docs.servicestack.net/netcore-deploy-rsync): + +### [sharpdata.netcore.io](https://sharpdata.netcore.io) + +> For best experience we recommend running against local network databases + +It's a unique ServiceStack App in that it doesn't contain any ServiceStack Services as it's only using pre-existing functionality already built into ServiceStack, +`#Script` for its HTTP APIs and a Vue SPA for its UI, so requires no `.dll's` need to be deployed with it. + +It uses the same Vue SPA solution as [vue-lite](https://github.com/NetCoreTemplates/vue-lite) to avoid npm's size & complexity where you only need to run TypeScript's `tsc -w` to enable its [live-reload](https://docs.servicestack.net/hot-reloading) dev UX which provides its instant feedback during development. + +Some other of its unique traits is that instead of manually including all the Vue framework `.js` libraries, it instead references the new `ServiceStack.Desktop.dll` for its Vue framework libraries and its Material design SVG icons which are [referenced as normal file references](https://github.com/NetCoreApps/SharpData/blob/0499e7c66ca4289d17158e79bcc91815bbcd7a99/wwwroot/_layout.html#L60-L66): + +```js +{{ [ + `/lib/js/vue/vue.min.js`, + `/lib/js/vue-router/vue-router.min.js`, + `/lib/js/vue-class-component/vue-class-component.min.js`, + `/lib/js/vue-property-decorator/vue-property-decorator.min.js`, + `/lib/js/@servicestack/desktop/servicestack-desktop.min.js`, + `/lib/js/@servicestack/client/servicestack-client.min.js`, + `/lib/js/@servicestack/vue/servicestack-vue.min.js`, +] |> map => `` |> joinln |> raw }} +``` + +But instead of needing to exist on disk & deployed with your project it's referencing the embedded resources in `ServiceStack.Desktop.dll` and only the bundled assets need to be [deployed with your project](https://github.com/NetCoreApps/SharpData/blob/0499e7c66ca4289d17158e79bcc91815bbcd7a99/SharpData.csproj#L17) which is using the built-in [NUglify](https://github.com/xoofx/NUglify) support in the [dotnet tools](https://docs.servicestack.net/dotnet-tool) to produce its highly optimized/minified bundle without needing to rely on any npm tooling when publishing the .NET Core App: + +```xml + + + +``` + +The included [/typings](https://github.com/NetCoreApps/SharpData/tree/master/typings) are just the TypeScript definitions for each library which +TypeScript uses for its static analysis & its great dev UX in IDEs & VSCode, but are only needed during development and not deployed with the project. + +### Publish to Gist Desktop App + +The primary way **SharpData** is distributed is as a [Gist Desktop App](https://sharpscript.net/docs/gist-desktop-apps), where it's able to provide instant utility by running on a users local machine inside a native Chromium Desktop App making it suitable for a much broader use-case as a fast, lightweight, always up-to-date Desktop App with deeper Windows integration all packaged in a tiny **20kb .zip** footprint. There's no need to provision servers, setup CI, manage cloud hosting resources, you can simply run a script to update a Gist where its latest features are immediately available to your end users the next time it's run. + +To run, test & publish it as a Desktop App you can use the pre-made scripts in [package.json](https://github.com/NetCoreApps/SharpData/blob/master/package.json). +Rider provides a nice UX here as it lets you run each individual script directly from their json editor: + +![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/sharpdata-scripts.png) + +Essentially to package it into a [Sharp App](https://sharpscript.net/docs/sharp-apps) you just need to run the `pack` script which will bundle & copy all required assets into the `/dist` folder which you can then test locally in a [.NET Core Desktop App](https://docs.servicestack.net/netcore-windows-desktop) by running `app` in that folder: + + $ cd dist + $ app + +The `mix-*` scripts copies the db customizations so you have something to test it with which you can run with the `run-test` script. + +The `publish-app` script is if you want to publish it to a Gist Desktop App, where you will need it to provide the GitHub **AccessToken** with write access to the Gist User Account you want to publish it to. Adding an `appName` and `description` to `app.settings` will publish it to the [Global App Registry](https://sharpscript.net/docs/gist-desktop-apps#instant-run-without-installation), make it publicly discoverable and allow anyone to open your App using your user-friendly `appName` alias, otherwise they can run it using the **Gist Id** or **Gist URL**. + +Alternatively the contents of the `dist/` folder can be published to a GitHub repo (public or private) and run with: + + $ app open / + +Or link to it with its custom URL Scheme: + + app:///repo + +If it's in a private repo they'll need to either provide an **AccessToken** in the `GITHUB_TOKEN` Environment variable or using the `-token` argument: + + $ app open / -token + +URL Scheme: + + app:///repo?token= + +### RDBMS Configuration + +When running as a .NET Core App you'd need to register which RDBMS's you want to use with OrmLite's configuration, e.g. the screenshot above registers an SQLite `northwind.sqlite` database and the https://techstacks.io PostgreSQL Database: + +```csharp +container.Register(c => new OrmLiteConnectionFactory( + MapProjectPath("~/northwind.sqlite"), SqliteDialect.Provider)); + +var dbFactory = container.Resolve(); +dbFactory.RegisterConnection("techstacks", + Environment.GetEnvironmentVariable("TECHSTACKS_DB"), + PostgreSqlDialect.Provider); +``` + +By default it shows all Tables in each RDBMS, but you can limit it to only show a user-defined list of tables with `#Script` Arguments: + +```csharp +Plugins.Add(new SharpPagesFeature { + //... + Args = { + //Only display user-defined list of tables: + ["tables"] = "Customer,Order,OrderDetail,Category,Product,Employee,EmployeeTerritory,Shipper,Supplier,Region,Territory", + ["tables_techstacks"] = "technology,technology_stack,technology_choice,organization,organization_member,post,post_comment,post_vote,custom_user_auth,user_auth_details,user_activity,page_stats", + } +}); +``` + +When running as a Sharp App it's instead configured in its +[app.settings](https://github.com/NetCoreApps/SharpData/blob/master/scripts/deploy/app.northwind.settings), here's equivalent settings for the above configuration: + +``` +# Configure below. Supported dialects: sqlite, mysql, postgres, sqlserver +db.connections[northwind] { db:sqlite, connection:'northwind.sqlite' } +db.connections[techstacks] { db:postgres, connection:$TECHSTACKS_DB } + +args.tables Customer,Order,OrderDetail,Category,Product,Employee,EmployeeTerritory,Shipper,Supplier,Region,Territory +args.tables_techstacks technology,technology_stack,technology_choice,organization,organization_member,post,post_comment,post_vote,custom_user_auth,user_auth_details,user_activity,page_stats +``` + +### Feedback + +We hope [SharpData](https://github.com/NetCoreApps/SharpData) serves useful in some capacity, whether it's being able to quickly develop and Ship a UI to stakeholders or as a template to develop .NET Core Apps that you can distribute as **Sharp Apps**, as an example to explore the delivery and platform potential of URL schemes and install-less Desktop Apps or just as an inspiration for areas where `#Script` shines & the different kind of Apps you can create with it. + +Whilst `app` is Windows 64 only, you can use the `x` cross-platform tool and its `xapp://` URL scheme to run Sharp Apps on macOS/Linux, it just wont have access to any of its Window Integration features. diff --git a/wwwroot/gfm/apps/03.html b/wwwroot/gfm/apps/03.html new file mode 100644 index 0000000..77061b3 --- /dev/null +++ b/wwwroot/gfm/apps/03.html @@ -0,0 +1,120 @@ +

    The win32 Sharp App contains an examples dashboard of invoking different native Win32 functions:

    +

    +

    You can run this Gist Desktop App via URL Scheme from (Windows Desktop App):

    +

    app://win32

    +

    Or via command-line:

    +
    $ app open win32
    +
    +

    Cross platform (Default Browser):

    +
    $ x open win32
    +
    +

    +Kiosk Mode

    +

    This app starts in full-screen Kiosk mode, enabled in its app.settings by:

    +
    CefConfig { Kiosk:true }
    +
    +

    The main source code of this component is in Win32/index.ts, +which makes use of the built in TypeScript APIs below from @servicestack/desktop:

    +
    start('%USERPROFILE%\\\\.sharp-apps')
    +
    +openUrl('https://google.com')
    +
    +messageBox('The Title', 'Caption', MessageBoxType.YesNo | MessageBoxType.IconInformation)
    +
    +await openFile(  {
    +    title: 'Pick Images',
    +    filter: "Image files (*.png;*.jpeg)|*.png;*.jpeg|All files (*.*)|*.*",
    +    initialDir: await expandEnvVars('%USERPROFILE%\\\\Pictures'),
    +    defaultExt: '*.png',
    +})
    +
    +openFile({ isFolderPicker: true })
    +
    +deviceScreenResolution()
    +
    +primaryMonitorInfo()
    +
    +windowSetPosition(x, y)
    +
    +windowSetSize(width, height)
    +

    +Custom Win32 API

    +

    You're also not limited to calling the built-in Win32 APIs above as calling custom APIs just involves wrapping the C# inside +your preferred #Script method that you would like to make it available to JS as, +e.g. here's the win32 implementation for launching Win32's Color Dialog Box +and returning the selected color in HTML Color format:

    +
    public class CustomMethods : ScriptMethods
    +{
    +    [DllImport("ComDlg32.dll", CharSet = CharSet.Unicode)]
    +    internal static extern int CommDlgExtendedError();
    +
    +    [DllImport("ComDlg32.dll", CharSet = CharSet.Unicode)]
    +    internal static extern bool ChooseColor(ref ChooseColor cc);
    +    
    +    private int[] customColors = new int[16] {
    +        0x00FFFFFF, 0x00C0C0C0, 0x00808080, 0x00000000,
    +        0x00FF0000, 0x00800000, 0x00FFFF00, 0x00808000,
    +        0x0000FF00, 0x00008000, 0x0000FFFF, 0x00008080,
    +        0x000000FF, 0x00000080, 0x00FF00FF, 0x00800080,
    +    };
    +
    +    public string chooseColor(ScriptScopeContext scope) => chooseColor(scope, "#ffffff");
    +
    +    public string chooseColor(ScriptScopeContext scope, string defaultColor) => scope.DoWindow(w => {
    +        var cc = new ChooseColor();
    +        cc.lStructSize = Marshal.SizeOf(cc);
    +        var lpCustColors = Marshal.AllocCoTaskMem(16 * sizeof(int));
    +        try
    +        {
    +            Marshal.Copy(customColors, 0, lpCustColors,16);
    +            cc.hwndOwner = w;
    +            cc.lpCustColors = lpCustColors;
    +            cc.Flags = ChooseColorFlags.FullOpen | ChooseColorFlags.RgbInit;
    +            var c = ColorTranslator.FromHtml(defaultColor);
    +            cc.rgbResult = ColorTranslator.ToWin32(c);
    +
    +            if (!ChooseColor(ref cc)) 
    +                return (string) null;
    +        
    +            c = ColorTranslator.FromWin32(cc.rgbResult);
    +            return ColorTranslator.ToHtml(c);
    +        }
    +        finally
    +        {
    +            Marshal.FreeCoTaskMem(lpCustColors);
    +        }
    +    });
    +}
    +

    ServiceStack.Desktop's IPC takes care of invoking the #Script JS-compatible expression and returning the result:

    +
    var selectedColor = await evaluateCode('chooseColor(`#336699`)')
    +

    +

    The scope.DoWindow() extension method supports expressions being invoked in-process when launched by app.exe as well as when +invoked during development in "detached mode" if electing to run the .NET Core backend as a stand-alone Web App.

    +

    If your App calls your custom APIs a lot you can wrap it in a first-class TypeScript method that mirrors the server #Script method:

    +
    function chooseColor(defaultColor?:string) {
    +    return defaultColor
    +        ? evaluateCode(`chooseColor(${quote(defaultColor)})`)
    +        : evaluateCode(`chooseColor()`);
    +}
    +

    Where it can be called using the same syntax in JS and #Script:

    +
    var selectedColor = await chooseColor(`#336699`)
    +

    +Highly productive live-reloading Development experience

    +

    If it weren't for the productivity possible for being able to only needing to develop for Chrome's state-of-the-art rendering engine where you can use advanced features like CSS grid along with the productivity of high-level productive Reactive UI frameworks like Vue, the effort into create a Desktop App like ServiceStack Studio wouldn't be justifiable.

    +

    After the next release we'll create pre-packaged project templates for vue-desktop and react-desktop Desktop Apps to make it easy develop Vue & React Desktop Apps along with scripts to bundle it & publish it to gist. If preferred app.exe also lets you deploy the published app to your own private repo & limit access to only users accessible with a GitHub token which they can open with from a URL with:

    +
    app://user/repo?token={GITHUB_TOKEN}
    +
    +

    Or on the command line with:

    +
    $ app user/repo -token $GITHUB_TOKEN
    +
    +
    +

    Or without a token by setting it in the GITHUB_TOKEN Environment variable

    +
    +

    For offline deployments the published /dist folder can be copied and launched with app (or x) in the app's folder:

    +
    $ app
    +
    +

    For better Desktop integration this (or custom command-line arguments) can be wrapped in a new Windows Shortcut:

    +
    $ app shortcut
    +
    +

    For a look at example Desktop App projects built using this development model can checkout ServiceStack/Studio or NetCoreApps/SharpData.

    +
    \ No newline at end of file diff --git a/wwwroot/gfm/apps/03.md b/wwwroot/gfm/apps/03.md new file mode 100644 index 0000000..32f31f5 --- /dev/null +++ b/wwwroot/gfm/apps/03.md @@ -0,0 +1,152 @@ +The [win32](https://github.com/sharp-apps/win32) Sharp App contains an examples dashboard of invoking different native Win32 functions: + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/win32.png)](https://github.com/sharp-apps/win32) + +You can run this Gist Desktop App via URL Scheme from (Windows Desktop App): + +app://win32 + +Or via command-line: + + $ app open win32 + +Cross platform (Default Browser): + + $ x open win32 + +### Kiosk Mode + +This app starts in full-screen **Kiosk** mode, enabled in its [app.settings](https://github.com/sharp-apps/win32/blob/master/scripts/deploy/app.settings) by: + + CefConfig { Kiosk:true } + +The main source code of this component is in [Win32/index.ts](https://github.com/sharp-apps/win32/blob/master/src/components/Win32/index.ts), +which makes use of the built in TypeScript APIs below from `@servicestack/desktop`: + +```ts +start('%USERPROFILE%\\\\.sharp-apps') + +openUrl('https://google.com') + +messageBox('The Title', 'Caption', MessageBoxType.YesNo | MessageBoxType.IconInformation) + +await openFile( { + title: 'Pick Images', + filter: "Image files (*.png;*.jpeg)|*.png;*.jpeg|All files (*.*)|*.*", + initialDir: await expandEnvVars('%USERPROFILE%\\\\Pictures'), + defaultExt: '*.png', +}) + +openFile({ isFolderPicker: true }) + +deviceScreenResolution() + +primaryMonitorInfo() + +windowSetPosition(x, y) + +windowSetSize(width, height) +``` + +### Custom Win32 API + +You're also not limited to calling the built-in Win32 APIs above as calling custom APIs just involves wrapping the C# inside +your preferred [#Script method](/docs/methods) that you would like to make it available to JS as, +e.g. here's the **win32** implementation for launching [Win32's Color Dialog Box](https://docs.microsoft.com/en-us/windows/win32/dlgbox/color-dialog-box) +and returning the selected color in HTML Color format: + +```csharp +public class CustomMethods : ScriptMethods +{ + [DllImport("ComDlg32.dll", CharSet = CharSet.Unicode)] + internal static extern int CommDlgExtendedError(); + + [DllImport("ComDlg32.dll", CharSet = CharSet.Unicode)] + internal static extern bool ChooseColor(ref ChooseColor cc); + + private int[] customColors = new int[16] { + 0x00FFFFFF, 0x00C0C0C0, 0x00808080, 0x00000000, + 0x00FF0000, 0x00800000, 0x00FFFF00, 0x00808000, + 0x0000FF00, 0x00008000, 0x0000FFFF, 0x00008080, + 0x000000FF, 0x00000080, 0x00FF00FF, 0x00800080, + }; + + public string chooseColor(ScriptScopeContext scope) => chooseColor(scope, "#ffffff"); + + public string chooseColor(ScriptScopeContext scope, string defaultColor) => scope.DoWindow(w => { + var cc = new ChooseColor(); + cc.lStructSize = Marshal.SizeOf(cc); + var lpCustColors = Marshal.AllocCoTaskMem(16 * sizeof(int)); + try + { + Marshal.Copy(customColors, 0, lpCustColors,16); + cc.hwndOwner = w; + cc.lpCustColors = lpCustColors; + cc.Flags = ChooseColorFlags.FullOpen | ChooseColorFlags.RgbInit; + var c = ColorTranslator.FromHtml(defaultColor); + cc.rgbResult = ColorTranslator.ToWin32(c); + + if (!ChooseColor(ref cc)) + return (string) null; + + c = ColorTranslator.FromWin32(cc.rgbResult); + return ColorTranslator.ToHtml(c); + } + finally + { + Marshal.FreeCoTaskMem(lpCustColors); + } + }); +} +``` + +ServiceStack.Desktop's IPC takes care of invoking the `#Script` [JS-compatible expression](/docs/expression-viewer) and returning the result: + +```ts +var selectedColor = await evaluateCode('chooseColor(`#336699`)') +``` + +[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/release-notes/v5.9/win32-choosecolor.png)](https://github.com/sharp-apps/win32) + +The `scope.DoWindow()` extension method supports expressions being invoked in-process when launched by `app.exe` as well as when +invoked during development in "detached mode" if electing to run the .NET Core backend as a stand-alone Web App. + +If your App calls your custom APIs a lot you can wrap it in a first-class TypeScript method that mirrors the server #Script method: + +```ts +function chooseColor(defaultColor?:string) { + return defaultColor + ? evaluateCode(`chooseColor(${quote(defaultColor)})`) + : evaluateCode(`chooseColor()`); +} +``` + +Where it can be called using the same syntax in JS and #Script: + +```ts +var selectedColor = await chooseColor(`#336699`) +``` + +## Highly productive live-reloading Development experience + +If it weren't for the productivity possible for being able to only needing to develop for Chrome's state-of-the-art rendering engine where you can use advanced features like CSS grid along with the productivity of high-level productive Reactive UI frameworks like Vue, the effort into create a Desktop App like ServiceStack Studio wouldn't be justifiable. + +After the next release we'll create pre-packaged project templates for **vue-desktop** and **react-desktop** Desktop Apps to make it easy develop Vue & React Desktop Apps along with scripts to bundle it & publish it to gist. If preferred `app.exe` also lets you deploy the published app to your own private repo & limit access to only users accessible with a GitHub token which they can open with from a URL with: + + app://user/repo?token={GITHUB_TOKEN} + +Or on the command line with: + + $ app user/repo -token $GITHUB_TOKEN + +> Or without a token by setting it in the `GITHUB_TOKEN` Environment variable + +For offline deployments the published `/dist` folder can be copied and launched with `app` (or `x`) in the app's folder: + + $ app + +For better Desktop integration this (or custom command-line arguments) can be wrapped in a new Windows Shortcut: + + $ app shortcut + +For a look at example Desktop App projects built using this development model can checkout [ServiceStack/Studio](https://github.com/ServiceStack/Studio) or [NetCoreApps/SharpData](https://github.com/NetCoreApps/SharpData). \ No newline at end of file diff --git a/wwwroot/gfm/apps/04.html b/wwwroot/gfm/apps/04.html new file mode 100644 index 0000000..4cc7286 --- /dev/null +++ b/wwwroot/gfm/apps/04.html @@ -0,0 +1,169 @@ +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namedefaultdescription
    bindlocalhostWhich hostname to bind .NET Core Server to
    ssltrueUse https for .NET Core Server
    port5001Which port to bind .NET Core Server to
    nameSharp AppAppHost name (also used in Shortcuts)
    debugASP.NET DefaultEnable additional logging & diagnostics
    contentRootapp.settings dirASP.NET Content Root Directory
    webRootwwwroot/ASP.NET Web Root Directory
    apiPath/apiPath of Sharp APIs
    defaultRedirectDefault Fallback RedirectPath
    dbOrmLite Dialect: sqlite, sqlserver, mysql postgres
    db.connectionRDBMS Connection String
    redis.connectionServiceStack.Redis Connection String
    filesVFS provider: filesystem, s3, azureblob
    files.configVirtual File System JS Object Configuration
    checkForModifiedPagesAfterSecsHow long to check backing VFS provider for changes
    defaultFileCacheExpirySecsHow long to preserve static file caches for
    defaultUrlCacheExpirySecsHow long to preserve URL caches for
    featuresList of plugins to load
    markdownProviderMarkDigMarkdown provider: MarkdownDeep, Markdig
    jsMinifierNUglifyJS Minifier: NUglify, ServiceStack
    cssMinifierNUglifyCSS Minifier: NUglify, ServiceStack
    htmlMinifierNUglifyHTML Minifier: NUglify, ServiceStack
    iconfavicon.icoRelative or Absolute Path to Shortcut & Desktop icon
    appNameUnique Id to identify Desktop App (snake-case)
    descriptionShort Description of Desktop App (20-150 chars)
    tagsDesktop App Tags space-delimited, snake-case, 3 max
    args.*Any additional rich config args to define in App
    +

    Desktop specific Apps like ServiceStack/Studio enable additional Desktop functionality by +configuring the DesktopFeature plugin in +ServiceStack.Desktop, e.g:

    +
    Plugins.Add(new DesktopFeature {
    +    // access role for Script, File & Download services
    +    AccessRole = Config.DebugMode 
    +        ? RoleNames.AllowAnon
    +        : RoleNames.Admin,
    +    ImportParams = { // app.settings you want auto-populated in your App
    +        "debug",
    +        "connect",
    +    },
    +    // Create a URL Scheme proxy rule for each registered site
    +    ProxyConfigs = sites.Keys.Map(baseUrl => new Uri(baseUrl))
    +        .Map(uri => new ProxyConfig {
    +            Scheme = uri.Scheme,
    +            TargetScheme = uri.Scheme,
    +            Domain = uri.Host,
    +            AllowCors = true,
    +            IgnoreHeaders = { "X-Frame-Options", "Content-Security-Policy" }, 
    +        })
    +});
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/apps/04.md b/wwwroot/gfm/apps/04.md new file mode 100644 index 0000000..7b3eccd --- /dev/null +++ b/wwwroot/gfm/apps/04.md @@ -0,0 +1,56 @@ + +| name | default | description | +-------------------|-------------------------|------------------------------------------------------| +| bind | localhost | Which hostname to bind .NET Core Server to | +| ssl | true | Use https for .NET Core Server | +| port | 5001 | Which port to bind .NET Core Server to | +| name | Sharp App | AppHost name (also used in Shortcuts) | +| debug | ASP.NET Default | Enable additional logging & diagnostics | +| contentRoot | app.settings dir | ASP.NET Content Root Directory | +| webRoot | wwwroot/ | ASP.NET Web Root Directory | +| apiPath | /api | Path of Sharp APIs | +| defaultRedirect | | Default Fallback RedirectPath | +| db | | OrmLite Dialect: sqlite, sqlserver, mysql postgres | +| db.connection | | RDBMS Connection String | +| redis.connection | | ServiceStack.Redis Connection String | +| files | | VFS provider: filesystem, s3, azureblob | +| files.config | | Virtual File System JS Object Configuration | +| checkForModifiedPagesAfterSecs | | How long to check backing VFS provider for changes | +| defaultFileCacheExpirySecs | | How long to preserve static file caches for | +| defaultUrlCacheExpirySecs | | How long to preserve URL caches for | +| features | | List of plugins to load | +| markdownProvider | MarkDig | Markdown provider: MarkdownDeep, Markdig | +| jsMinifier | NUglify | JS Minifier: NUglify, ServiceStack | +| cssMinifier | NUglify | CSS Minifier: NUglify, ServiceStack | +| htmlMinifier | NUglify | HTML Minifier: NUglify, ServiceStack | +| icon | favicon.ico | Relative or Absolute Path to Shortcut & Desktop icon | +| appName | | Unique Id to identify Desktop App (snake-case) | +| description | | Short Description of Desktop App (20-150 chars) | +| tags | | Desktop App Tags space-delimited, snake-case, 3 max | +| args.* | | Any additional rich config args to define in App | + +Desktop specific Apps like [ServiceStack/Studio](https://github.com/ServiceStack/Studio) enable additional Desktop functionality by +configuring the [DesktopFeature](https://github.com/ServiceStack/ServiceStack/blob/master/src/ServiceStack.Desktop/DesktopFeature.cs) plugin in +[ServiceStack.Desktop](https://www.nuget.org/packages/ServiceStack.Desktop), e.g: + +```csharp +Plugins.Add(new DesktopFeature { + // access role for Script, File & Download services + AccessRole = Config.DebugMode + ? RoleNames.AllowAnon + : RoleNames.Admin, + ImportParams = { // app.settings you want auto-populated in your App + "debug", + "connect", + }, + // Create a URL Scheme proxy rule for each registered site + ProxyConfigs = sites.Keys.Map(baseUrl => new Uri(baseUrl)) + .Map(uri => new ProxyConfig { + Scheme = uri.Scheme, + TargetScheme = uri.Scheme, + Domain = uri.Host, + AllowCors = true, + IgnoreHeaders = { "X-Frame-Options", "Content-Security-Policy" }, + }) +}); +``` diff --git a/wwwroot/gfm/arguments/01.html b/wwwroot/gfm/arguments/01.html new file mode 100644 index 0000000..f47b46d --- /dev/null +++ b/wwwroot/gfm/arguments/01.html @@ -0,0 +1,6 @@ +
    var context = new ScriptContext { 
    +    Args = {
    +        ["arg"] = 1
    +    }
    +}.Init();
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/arguments/01.md b/wwwroot/gfm/arguments/01.md new file mode 100644 index 0000000..ae43616 --- /dev/null +++ b/wwwroot/gfm/arguments/01.md @@ -0,0 +1,7 @@ +```csharp +var context = new ScriptContext { + Args = { + ["arg"] = 1 + } +}.Init(); +``` \ No newline at end of file diff --git a/wwwroot/gfm/arguments/02.html b/wwwroot/gfm/arguments/02.html new file mode 100644 index 0000000..bc46700 --- /dev/null +++ b/wwwroot/gfm/arguments/02.html @@ -0,0 +1,6 @@ +
    var context = new PageResult(context.GetPage("page")) { 
    +    Args = {
    +        ["arg"] = 4
    +    }
    +};
    +
    \ No newline at end of file diff --git a/src/wwwroot/gfm/arguments/02.md b/wwwroot/gfm/arguments/02.md similarity index 100% rename from src/wwwroot/gfm/arguments/02.md rename to wwwroot/gfm/arguments/02.md diff --git a/wwwroot/gfm/blocks/00.html b/wwwroot/gfm/blocks/00.html new file mode 100644 index 0000000..0dcc8cf --- /dev/null +++ b/wwwroot/gfm/blocks/00.html @@ -0,0 +1,168 @@ +

    +Default Blocks

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    namebody
    noopverbatim
    withdefault
    ifdefault
    whiledefault
    rawverbatim
    functioncode
    defnlisp
    capturetemplate
    markdownverbatim
    csvverbatim
    partialtemplate
    htmltemplate
    +

    +ServiceStack Blocks

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    namebody
    minifyjsverbatim
    minifycssverbatim
    minifyhtmlverbatim
    svgtemplate
    +

    +Script Block Body

    +

    The Body of the Script Block specifies how the body is evaluated. Script Blocks can be used in both #Script Template and +Code Statement Blocks.

    +

    Below are examples of using a script block of each body type in both #Script template and code statement blocks:

    +

    +default

    +

    If unspecified Script Blocks are evaluated within the language they're used within. +By default #Script pages use template expression handlebars syntax:

    +
    {{#if test.isEven() }}
    +    {{test}} is even
    +{{else}}
    +    {{test}} is odd
    +{{/if}}
    +

    Whilst in Code Statement Blocks the body is executed as JS Expressions, which requires using +quoted strings or template literals for any text you want to emit, e.g:

    +
    <script>
    +#if test.isEven()
    +    `${test} is even`
    +else
    +    `${test} is odd`
    +/if
    +</script>
    +

    +verbatim

    +

    The contents of verbatim script blocks are unprocessed and evaluated as raw text by the script block:

    +
    {{#csv cars}}
    +Tesla,Model S,79990
    +Tesla,Model 3,38990
    +Tesla,Model X,84990
    +{{/csv}}
    +
    <script>
    +#csv cars
    +Tesla,Model S,79990
    +Tesla,Model 3,38990
    +Tesla,Model X,84990
    +/csv
    +</script>
    +

    +template

    +

    The contents of template Script Blocks are processed using Template Expression syntax:

    +
    {{#capture out}}
    +    {{#each range(3)}}
    +    - {{it + 1}}
    +    {{/each}}
    +{{/capture}}
    +
    <script>
    +#capture out
    +    {{#each range(3)}}
    +    - {{it + 1}}
    +    {{/each}}
    +/capture
    +</script>
    +

    +code

    +

    The contents of code Script Blocks are processed as JS Expression statements:

    +
    {{#function calc(a, b) }}
    +    a * b |> to => c
    +    a + b + c |> return
    +{{/function}}
    +
    <script>
    +#function calc(a, b)
    +    a * b |> to => c
    +    a + b + c |> return
    +/function 
    +</script>
    +

    +lisp

    +

    Finally the contents of lisp Script Blocks is processed by #Script Lisp:

    +
    {{#defn calc [a, b] }}
    +    (def c (* a b))
    +    (+ a b c)
    +{{/defn}}
    +
    <script>
    +#defn calc [a, b]
    +    (def c (* a b))
    +    (+ a b c)
    +/defn
    +</script>
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/00.md b/wwwroot/gfm/blocks/00.md new file mode 100644 index 0000000..9e880c7 --- /dev/null +++ b/wwwroot/gfm/blocks/00.md @@ -0,0 +1,143 @@ +## Default Blocks + +| name | body | +|---------------------------|----------| +| [noop](#noop) | verbatim | +| [with](#with) | default | +| [if](#if) | default | +| [while](#while) | default | +| [raw](#raw) | verbatim | +| [function](#function) | code | +| [defn](#defn) | lisp | +| [capture](#capture) | template | +| [markdown](#markdown) | verbatim | +| [csv](#csv) | verbatim | +| [partial](#partial) | template | +| [html](#html) | template | + +## ServiceStack Blocks + +| name | body | +|---------------------------|----------| +| [minifyjs](#minifyjs) | verbatim | +| [minifycss](#minifycss) | verbatim | +| [minifyhtml](#minifyhtml) | verbatim | +| [svg](#svg) | template | + +### Script Block Body + +The `Body` of the Script Block specifies how the body is evaluated. Script Blocks can be used in both `#Script` Template and +[Code Statement Blocks](/docs/syntax#language-blocks-and-expressions). + +Below are examples of using a script block of each body type in both `#Script` template and code statement blocks: + +### default + +If unspecified Script Blocks are evaluated within the language they're used within. +By default `#Script` pages use **template** expression handlebars syntax: + +```hbs +{{#if test.isEven() }} + {{test}} is even +{{else}} + {{test}} is odd +{{/if}} +``` + +Whilst in Code Statement Blocks the body is executed as JS Expressions, which requires using +[quoted strings or template literals](/docs/syntax#quotes) for any text you want to emit, e.g: + +```html + +``` + +### verbatim + +The contents of **verbatim** script blocks are unprocessed and evaluated as raw text by the script block: + +```hbs +{{#csv cars}} +Tesla,Model S,79990 +Tesla,Model 3,38990 +Tesla,Model X,84990 +{{/csv}} +``` + +```html + +``` + +### template + +The contents of **template** Script Blocks are processed using Template Expression syntax: + +```hbs +{{#capture out}} + {{#each range(3)}} + - {{it + 1}} + {{/each}} +{{/capture}} +``` + +```html + +``` + +### code + +The contents of **code** Script Blocks are processed as JS Expression statements: + +```hbs +{{#function calc(a, b) }} + a * b |> to => c + a + b + c |> return +{{/function}} +``` + +```html + +``` + +### lisp + +Finally the contents of **lisp** Script Blocks is processed by [#Script Lisp](/lisp/): + +```hbs +{{#defn calc [a, b] }} + (def c (* a b)) + (+ a b c) +{{/defn}} +``` + +```html + +``` + diff --git a/src/wwwroot/gfm/blocks/01.html b/wwwroot/gfm/blocks/01.html similarity index 100% rename from src/wwwroot/gfm/blocks/01.html rename to wwwroot/gfm/blocks/01.html diff --git a/src/wwwroot/gfm/blocks/01.md b/wwwroot/gfm/blocks/01.md similarity index 100% rename from src/wwwroot/gfm/blocks/01.md rename to wwwroot/gfm/blocks/01.md diff --git a/wwwroot/gfm/blocks/02.html b/wwwroot/gfm/blocks/02.html new file mode 100644 index 0000000..ac398ef --- /dev/null +++ b/wwwroot/gfm/blocks/02.html @@ -0,0 +1,8 @@ +
    public class NoopScriptBlock : ScriptBlock
    +{
    +    public override string Name => "noop";
    +
    +    public override Task WriteAsync(ScriptScopeContext scope, PageBlockFragment block, CancellationToken ct)
    +        => Task.CompletedTask;
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/02.md b/wwwroot/gfm/blocks/02.md new file mode 100644 index 0000000..73feccb --- /dev/null +++ b/wwwroot/gfm/blocks/02.md @@ -0,0 +1,9 @@ +```csharp +public class NoopScriptBlock : ScriptBlock +{ + public override string Name => "noop"; + + public override Task WriteAsync(ScriptScopeContext scope, PageBlockFragment block, CancellationToken ct) + => Task.CompletedTask; +} +``` diff --git a/wwwroot/gfm/blocks/03.html b/wwwroot/gfm/blocks/03.html new file mode 100644 index 0000000..1d85318 --- /dev/null +++ b/wwwroot/gfm/blocks/03.html @@ -0,0 +1,4 @@ +
    var context = new ScriptContext {
    +    ScriptBlocks = { new NoopScriptBlock() },
    +}.Init();
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/03.md b/wwwroot/gfm/blocks/03.md new file mode 100644 index 0000000..a5f26bb --- /dev/null +++ b/wwwroot/gfm/blocks/03.md @@ -0,0 +1,5 @@ +```csharp +var context = new ScriptContext { + ScriptBlocks = { new NoopScriptBlock() }, +}.Init(); +``` diff --git a/wwwroot/gfm/blocks/04.html b/wwwroot/gfm/blocks/04.html new file mode 100644 index 0000000..3421710 --- /dev/null +++ b/wwwroot/gfm/blocks/04.html @@ -0,0 +1,7 @@ +
    var context = new ScriptContext
    +{
    +    ScanTypes = { typeof(NoopScriptBlock) }
    +};
    +context.Container.AddSingleton<ICacheClient>(() => new MemoryCacheClient());
    +context.Init();
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/04.md b/wwwroot/gfm/blocks/04.md new file mode 100644 index 0000000..2125cfb --- /dev/null +++ b/wwwroot/gfm/blocks/04.md @@ -0,0 +1,8 @@ +```csharp +var context = new ScriptContext +{ + ScanTypes = { typeof(NoopScriptBlock) } +}; +context.Container.AddSingleton(() => new MemoryCacheClient()); +context.Init(); +``` diff --git a/wwwroot/gfm/blocks/05.html b/wwwroot/gfm/blocks/05.html new file mode 100644 index 0000000..d13211a --- /dev/null +++ b/wwwroot/gfm/blocks/05.html @@ -0,0 +1,5 @@ +
    var context = new ScriptContext
    +{
    +    ScanAssemblies = { typeof(MyBlock).Assembly }
    +};
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/05.md b/wwwroot/gfm/blocks/05.md new file mode 100644 index 0000000..2c0f128 --- /dev/null +++ b/wwwroot/gfm/blocks/05.md @@ -0,0 +1,6 @@ +```csharp +var context = new ScriptContext +{ + ScanAssemblies = { typeof(MyBlock).Assembly } +}; +``` diff --git a/wwwroot/gfm/blocks/06.html b/wwwroot/gfm/blocks/06.html new file mode 100644 index 0000000..655a787 --- /dev/null +++ b/wwwroot/gfm/blocks/06.html @@ -0,0 +1,13 @@ +
    public class BoldScriptBlock : ScriptBlock
    +{
    +    public override string Name => "bold";
    +    
    +    public override async Task WriteAsync(
    +        ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
    +    {
    +        await scope.OutputStream.WriteAsync("<b>", token);
    +        await WriteBodyAsync(scope, block, token);
    +        await scope.OutputStream.WriteAsync("</b>", token);
    +    }
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/06.md b/wwwroot/gfm/blocks/06.md new file mode 100644 index 0000000..f5ea4a7 --- /dev/null +++ b/wwwroot/gfm/blocks/06.md @@ -0,0 +1,14 @@ +```csharp +public class BoldScriptBlock : ScriptBlock +{ + public override string Name => "bold"; + + public override async Task WriteAsync( + ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) + { + await scope.OutputStream.WriteAsync("", token); + await WriteBodyAsync(scope, block, token); + await scope.OutputStream.WriteAsync("", token); + } +} +``` diff --git a/wwwroot/gfm/blocks/07.html b/wwwroot/gfm/blocks/07.html new file mode 100644 index 0000000..7e70755 --- /dev/null +++ b/wwwroot/gfm/blocks/07.html @@ -0,0 +1,21 @@ +
    public class WithScriptBlock : ScriptBlock
    +{
    +    public override string Name => "with";
    +
    +    public override async Task WriteAsync(
    +        ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
    +    {
    +        var result = block.Argument.GetJsExpressionAndEvaluate(scope,
    +            ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression"));
    +
    +        if (result != null)
    +        {
    +            var resultAsMap = result.ToObjectDictionary();
    +
    +            var withScope = scope.ScopeWithParams(resultAsMap);
    +             
    +            await WriteBodyAsync(withScope, block, token);
    +        }
    +    }
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/07.md b/wwwroot/gfm/blocks/07.md new file mode 100644 index 0000000..446a387 --- /dev/null +++ b/wwwroot/gfm/blocks/07.md @@ -0,0 +1,22 @@ +```csharp +public class WithScriptBlock : ScriptBlock +{ + public override string Name => "with"; + + public override async Task WriteAsync( + ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) + { + var result = block.Argument.GetJsExpressionAndEvaluate(scope, + ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression")); + + if (result != null) + { + var resultAsMap = result.ToObjectDictionary(); + + var withScope = scope.ScopeWithParams(resultAsMap); + + await WriteBodyAsync(withScope, block, token); + } + } +} +``` diff --git a/src/wwwroot/gfm/blocks/08.html b/wwwroot/gfm/blocks/08.html similarity index 100% rename from src/wwwroot/gfm/blocks/08.html rename to wwwroot/gfm/blocks/08.html diff --git a/wwwroot/gfm/blocks/08.md b/wwwroot/gfm/blocks/08.md new file mode 100644 index 0000000..431199c --- /dev/null +++ b/wwwroot/gfm/blocks/08.md @@ -0,0 +1,4 @@ +```csharp +block.Argument.ParseJsExpression(out token); +var result = token.Evaluate(scope); +``` diff --git a/wwwroot/gfm/blocks/09.html b/wwwroot/gfm/blocks/09.html new file mode 100644 index 0000000..179f022 --- /dev/null +++ b/wwwroot/gfm/blocks/09.html @@ -0,0 +1,34 @@ +
    public class WithScriptBlock : ScriptBlock
    +{
    +    public override string Name => "with";
    +
    +    public override async Task WriteAsync(
    +        ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
    +    {
    +        var result = await block.Argument.GetJsExpressionAndEvaluateAsync(scope,
    +            ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression"));
    +
    +        if (result != null)
    +        {
    +            var resultAsMap = result.ToObjectDictionary();
    +
    +            var withScope = scope.ScopeWithParams(resultAsMap);
    +            
    +            await WriteBodyAsync(withScope, block, token);
    +        }
    +        else
    +        {
    +            await WriteElseAsync(scope, block.ElseBlocks, token);
    +        }
    +    }
    +}
    +

    This enables the with block to also evaluate async responses like the async results returned in async Database scripts, +it's also able to evaluate custom else statements for rendering different results based on alternate conditions, e.g:

    +
    {{#with dbSingle("select * from Person where id = @id", { id }) }}
    +    Hi {{Name}}, your Age is {{Age}}.
    +{{else if id == 0}}
    +    id is required.
    +{{else}}
    +    No person with id {{id}} exists.
    +{{/with}}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/09.md b/wwwroot/gfm/blocks/09.md new file mode 100644 index 0000000..edd17cf --- /dev/null +++ b/wwwroot/gfm/blocks/09.md @@ -0,0 +1,40 @@ +```csharp +public class WithScriptBlock : ScriptBlock +{ + public override string Name => "with"; + + public override async Task WriteAsync( + ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) + { + var result = await block.Argument.GetJsExpressionAndEvaluateAsync(scope, + ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression")); + + if (result != null) + { + var resultAsMap = result.ToObjectDictionary(); + + var withScope = scope.ScopeWithParams(resultAsMap); + + await WriteBodyAsync(withScope, block, token); + } + else + { + await WriteElseAsync(scope, block.ElseBlocks, token); + } + } +} +``` + +This enables the `with` block to also evaluate async responses like the async results returned in [async Database scripts](/docs/db-scripts), +it's also able to evaluate custom else statements for rendering different results based on alternate conditions, e.g: + +```hbs +{{#with dbSingle("select * from Person where id = @id", { id }) }} + Hi {{Name}}, your Age is {{Age}}. +{{else if id == 0}} + id is required. +{{else}} + No person with id {{id}} exists. +{{/with}} +``` + diff --git a/wwwroot/gfm/blocks/10.html b/wwwroot/gfm/blocks/10.html new file mode 100644 index 0000000..4cb312b --- /dev/null +++ b/wwwroot/gfm/blocks/10.html @@ -0,0 +1,27 @@ +
    /// <summary>
    +/// Handlebars.js like if block
    +/// Usages: {{#if a > b}} max {{a}} {{/if}}
    +///         {{#if a > b}} max {{a}} {{else}} max {{b}} {{/if}}
    +///         {{#if a > b}} max {{a}} {{else if b > c}} max {{b}} {{else}} max {{c}} {{/if}}
    +/// </summary>
    +public class IfScriptBlock : ScriptBlock
    +{
    +    public override string Name => "if";
    +    
    +    public override async Task WriteAsync(
    +        ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
    +    {
    +        var result = await block.Argument.GetJsExpressionAndEvaluateToBoolAsync(scope,
    +            ifNone: () => throw new NotSupportedException("'if' block does not have a valid expression"));
    +
    +        if (result)
    +        {
    +            await WriteBodyAsync(scope, block, token);
    +        }
    +        else
    +        {
    +            await WriteElseAsync(scope, block.ElseBlocks, token);
    +        }
    +    }
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/10.md b/wwwroot/gfm/blocks/10.md new file mode 100644 index 0000000..fe571af --- /dev/null +++ b/wwwroot/gfm/blocks/10.md @@ -0,0 +1,28 @@ +```csharp +/// +/// Handlebars.js like if block +/// Usages: {{#if a > b}} max {{a}} {{/if}} +/// {{#if a > b}} max {{a}} {{else}} max {{b}} {{/if}} +/// {{#if a > b}} max {{a}} {{else if b > c}} max {{b}} {{else}} max {{c}} {{/if}} +/// +public class IfScriptBlock : ScriptBlock +{ + public override string Name => "if"; + + public override async Task WriteAsync( + ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) + { + var result = await block.Argument.GetJsExpressionAndEvaluateToBoolAsync(scope, + ifNone: () => throw new NotSupportedException("'if' block does not have a valid expression")); + + if (result) + { + await WriteBodyAsync(scope, block, token); + } + else + { + await WriteElseAsync(scope, block.ElseBlocks, token); + } + } +} +``` diff --git a/wwwroot/gfm/blocks/11.html b/wwwroot/gfm/blocks/11.html new file mode 100644 index 0000000..b5f70ad --- /dev/null +++ b/wwwroot/gfm/blocks/11.html @@ -0,0 +1,39 @@ +
    /// <summary>
    +/// Handlebars.js like each block
    +/// Usages: {{#each customers}} {{Name}} {{/each}}
    +///         {{#each customers}} {{it.Name}} {{/each}}
    +///         {{#each customers}} Customer {{index + 1}}: {{Name}} {{/each}}
    +///         {{#each numbers}} {{it}} {{else}} no numbers {{/each}}
    +///         {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}}
    +/// </summary>
    +public class SimpleEachScriptBlock : ScriptBlock
    +{
    +    public override string Name => "each";
    +
    +    public override async Task WriteAsync(
    +        ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
    +    {
    +        var collection = (IEnumerable) block.Argument.GetJsExpressionAndEvaluate(scope,
    +            ifNone: () => throw new NotSupportedException("'each' block does not have a valid expression"));
    +
    +        var index = 0;
    +        if (collection != null)
    +        {
    +            foreach (var element in collection)
    +            {
    +                var scopeArgs = element.ToObjectDictionary();
    +                scopeArgs["it"] = element;
    +                scopeArgs[nameof(index)] = index++;
    +                
    +                var itemScope = scope.ScopeWithParams(scopeArgs);
    +                await WriteBodyAsync(itemScope, block, token);
    +            }
    +        }
    +        
    +        if (index == 0)
    +        {
    +            await WriteElseAsync(scope, block.ElseBlocks, token);
    +        }
    +    }
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/11.md b/wwwroot/gfm/blocks/11.md new file mode 100644 index 0000000..5630708 --- /dev/null +++ b/wwwroot/gfm/blocks/11.md @@ -0,0 +1,40 @@ +```csharp +/// +/// Handlebars.js like each block +/// Usages: {{#each customers}} {{Name}} {{/each}} +/// {{#each customers}} {{it.Name}} {{/each}} +/// {{#each customers}} Customer {{index + 1}}: {{Name}} {{/each}} +/// {{#each numbers}} {{it}} {{else}} no numbers {{/each}} +/// {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}} +/// +public class SimpleEachScriptBlock : ScriptBlock +{ + public override string Name => "each"; + + public override async Task WriteAsync( + ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) + { + var collection = (IEnumerable) block.Argument.GetJsExpressionAndEvaluate(scope, + ifNone: () => throw new NotSupportedException("'each' block does not have a valid expression")); + + var index = 0; + if (collection != null) + { + foreach (var element in collection) + { + var scopeArgs = element.ToObjectDictionary(); + scopeArgs["it"] = element; + scopeArgs[nameof(index)] = index++; + + var itemScope = scope.ScopeWithParams(scopeArgs); + await WriteBodyAsync(itemScope, block, token); + } + } + + if (index == 0) + { + await WriteElseAsync(scope, block.ElseBlocks, token); + } + } +} +``` diff --git a/wwwroot/gfm/blocks/12.html b/wwwroot/gfm/blocks/12.html new file mode 100644 index 0000000..7995762 --- /dev/null +++ b/wwwroot/gfm/blocks/12.html @@ -0,0 +1,13 @@ +
    /// <summary>
    +/// Handlebars.js like each block
    +/// Usages: {{#each customers}} {{Name}} {{/each}}
    +///         {{#each customers}} {{it.Name}} {{/each}}
    +///         {{#each num in numbers}} {{num}} {{/each}}
    +///         {{#each num in [1,2,3]}} {{num}} {{/each}}
    +///         {{#each numbers}} {{it}} {{else}} no numbers {{/each}}
    +///         {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}}
    +///         {{#each n in numbers where n > 5}} {{it}} {{else}} no numbers > 5 {{/each}}
    +///         {{#each n in numbers where n > 5 orderby n skip 1 take 2}} {{it}} {{else}}no numbers > 5{{/each}}
    +/// </summary>
    +public class EachScriptBlock : ScriptBlock { ... }
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/12.md b/wwwroot/gfm/blocks/12.md new file mode 100644 index 0000000..db2d003 --- /dev/null +++ b/wwwroot/gfm/blocks/12.md @@ -0,0 +1,14 @@ +```csharp +/// +/// Handlebars.js like each block +/// Usages: {{#each customers}} {{Name}} {{/each}} +/// {{#each customers}} {{it.Name}} {{/each}} +/// {{#each num in numbers}} {{num}} {{/each}} +/// {{#each num in [1,2,3]}} {{num}} {{/each}} +/// {{#each numbers}} {{it}} {{else}} no numbers {{/each}} +/// {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}} +/// {{#each n in numbers where n > 5}} {{it}} {{else}} no numbers > 5 {{/each}} +/// {{#each n in numbers where n > 5 orderby n skip 1 take 2}} {{it}} {{else}}no numbers > 5{{/each}} +/// +public class EachScriptBlock : ScriptBlock { ... } +``` diff --git a/wwwroot/gfm/blocks/13.html b/wwwroot/gfm/blocks/13.html new file mode 100644 index 0000000..c1ba9f5 --- /dev/null +++ b/wwwroot/gfm/blocks/13.html @@ -0,0 +1,52 @@ +
    /// <summary>
    +/// Special block which captures the raw body as a string fragment
    +///
    +/// Usages: {{#raw}}emit {{ verbatim }} body{{/raw}}
    +///         {{#raw varname}}assigned to varname{{/raw}}
    +///         {{#raw appendTo varname}}appended to varname{{/raw}}
    +/// </summary>
    +public class RawScriptBlock : ScriptBlock
    +{
    +    public override string Name => "raw";
    +    
    +    public override ScriptLanguage Body => ScriptVerbatim.Language;
    +
    +    public override async Task WriteAsync(
    +        ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
    +    {
    +        var strFragment = (PageStringFragment)block.Body[0];
    +
    +        if (!block.Argument.IsNullOrWhiteSpace())
    +        {
    +            Capture(scope, block, strFragment);
    +        }
    +        else
    +        {
    +            await scope.OutputStream.WriteAsync(strFragment.Value.Span, token);
    +        }
    +    }
    +
    +    private static void Capture(
    +        ScriptScopeContext scope, PageBlockFragment block, PageStringFragment strFragment)
    +    {
    +        var literal = block.Argument.Span.AdvancePastWhitespace();
    +        bool appendTo = false;
    +        if (literal.StartsWith("appendTo "))
    +        {
    +            appendTo = true;
    +            literal = literal.Advance("appendTo ".Length);
    +        }
    +
    +        literal = literal.ParseVarName(out var name);
    +        var nameString = name.Value();
    +        if (appendTo && scope.PageResult.Args.TryGetValue(nameString, out var oVar)
    +                        && oVar is string existingString)
    +        {
    +            scope.PageResult.Args[nameString] = existingString + strFragment.Value;
    +            return;
    +        }
    +
    +        scope.PageResult.Args[nameString] = strFragment.Value.ToString();
    +    }
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/13.md b/wwwroot/gfm/blocks/13.md new file mode 100644 index 0000000..68212b2 --- /dev/null +++ b/wwwroot/gfm/blocks/13.md @@ -0,0 +1,53 @@ +```csharp +/// +/// Special block which captures the raw body as a string fragment +/// +/// Usages: {{#raw}}emit {{ verbatim }} body{{/raw}} +/// {{#raw varname}}assigned to varname{{/raw}} +/// {{#raw appendTo varname}}appended to varname{{/raw}} +/// +public class RawScriptBlock : ScriptBlock +{ + public override string Name => "raw"; + + public override ScriptLanguage Body => ScriptVerbatim.Language; + + public override async Task WriteAsync( + ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) + { + var strFragment = (PageStringFragment)block.Body[0]; + + if (!block.Argument.IsNullOrWhiteSpace()) + { + Capture(scope, block, strFragment); + } + else + { + await scope.OutputStream.WriteAsync(strFragment.Value.Span, token); + } + } + + private static void Capture( + ScriptScopeContext scope, PageBlockFragment block, PageStringFragment strFragment) + { + var literal = block.Argument.Span.AdvancePastWhitespace(); + bool appendTo = false; + if (literal.StartsWith("appendTo ")) + { + appendTo = true; + literal = literal.Advance("appendTo ".Length); + } + + literal = literal.ParseVarName(out var name); + var nameString = name.Value(); + if (appendTo && scope.PageResult.Args.TryGetValue(nameString, out var oVar) + && oVar is string existingString) + { + scope.PageResult.Args[nameString] = existingString + strFragment.Value; + return; + } + + scope.PageResult.Args[nameString] = strFragment.Value.ToString(); + } +} +``` diff --git a/wwwroot/gfm/blocks/14.html b/wwwroot/gfm/blocks/14.html new file mode 100644 index 0000000..2b6ed6e --- /dev/null +++ b/wwwroot/gfm/blocks/14.html @@ -0,0 +1,83 @@ +
    /// <summary>
    +/// Captures the output and assigns it to the specified variable.
    +/// Accepts an optional Object Dictionary as scope arguments when evaluating body.
    +///
    +/// Usages: {{#capture output}} {{#each args}} - [{{it}}](/path?arg={{it}}) {{/each}} {{/capture}}
    +///         {{#capture output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}}
    +///         {{#capture appendTo output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}}
    +/// </summary>
    +public class CaptureScriptBlock : ScriptBlock
    +{
    +    public override string Name => "capture";
    +
    +    public override ScriptLanguage Body => ScriptTemplate.Language;
    +
    +    internal struct Tuple
    +    {
    +        internal string name;
    +        internal Dictionary<string, object> scopeArgs;
    +        internal bool appendTo;
    +        internal Tuple(string name, Dictionary<string, object> scopeArgs, bool appendTo)
    +        {
    +            this.name = name;
    +            this.scopeArgs = scopeArgs;
    +            this.appendTo = appendTo;
    +        }
    +    }
    +
    +    public override async Task WriteAsync(
    +        ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
    +    {
    +        var tuple = Parse(scope, block);
    +        var name = tuple.name;
    +
    +        using (var ms = MemoryStreamFactory.GetStream())
    +        {
    +            var useScope = scope.ScopeWith(tuple.scopeArgs, ms);
    +
    +            await WriteBodyAsync(useScope, block, token);
    +
    +            var capturedOutput = ms.ReadToEnd();
    +
    +            if (tuple.appendTo && scope.PageResult.Args.TryGetValue(name, out var oVar)
    +                         && oVar is string existingString)
    +            {
    +                scope.PageResult.Args[name] = existingString + capturedOutput;
    +                return;
    +            }
    +        
    +            scope.PageResult.Args[name] = capturedOutput;
    +        }
    +    }
    +
    +    //Extract usages of Span outside of async method 
    +    private Tuple Parse(ScriptScopeContext scope, PageBlockFragment block)
    +    {
    +        if (block.Argument.IsNullOrWhiteSpace())
    +            throw new NotSupportedException("'capture' block is missing variable name to assign output to");
    +        
    +        var literal = block.Argument.AdvancePastWhitespace();
    +        bool appendTo = false;
    +        if (literal.StartsWith("appendTo "))
    +        {
    +            appendTo = true;
    +            literal = literal.Advance("appendTo ".Length);
    +        }
    +            
    +        literal = literal.ParseVarName(out var name);
    +        if (name.IsNullOrEmpty())
    +            throw new NotSupportedException("'capture' block is missing variable name to assign output to");
    +
    +        literal = literal.AdvancePastWhitespace();
    +
    +        var argValue = literal.GetJsExpressionAndEvaluate(scope);
    +
    +        var scopeArgs = argValue as Dictionary<string, object>;
    +
    +        if (argValue != null && scopeArgs == null)
    +            throw new NotSupportedException("Any 'capture' argument must be an Object Dictionary");
    +
    +        return new Tuple(name.ToString(), scopeArgs, appendTo);
    +    }
    +}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/14.md b/wwwroot/gfm/blocks/14.md new file mode 100644 index 0000000..552e02b --- /dev/null +++ b/wwwroot/gfm/blocks/14.md @@ -0,0 +1,84 @@ +```csharp +/// +/// Captures the output and assigns it to the specified variable. +/// Accepts an optional Object Dictionary as scope arguments when evaluating body. +/// +/// Usages: {{#capture output}} {{#each args}} - [{{it}}](/path?arg={{it}}) {{/each}} {{/capture}} +/// {{#capture output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}} +/// {{#capture appendTo output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}} +/// +public class CaptureScriptBlock : ScriptBlock +{ + public override string Name => "capture"; + + public override ScriptLanguage Body => ScriptTemplate.Language; + + internal struct Tuple + { + internal string name; + internal Dictionary scopeArgs; + internal bool appendTo; + internal Tuple(string name, Dictionary scopeArgs, bool appendTo) + { + this.name = name; + this.scopeArgs = scopeArgs; + this.appendTo = appendTo; + } + } + + public override async Task WriteAsync( + ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) + { + var tuple = Parse(scope, block); + var name = tuple.name; + + using (var ms = MemoryStreamFactory.GetStream()) + { + var useScope = scope.ScopeWith(tuple.scopeArgs, ms); + + await WriteBodyAsync(useScope, block, token); + + var capturedOutput = ms.ReadToEnd(); + + if (tuple.appendTo && scope.PageResult.Args.TryGetValue(name, out var oVar) + && oVar is string existingString) + { + scope.PageResult.Args[name] = existingString + capturedOutput; + return; + } + + scope.PageResult.Args[name] = capturedOutput; + } + } + + //Extract usages of Span outside of async method + private Tuple Parse(ScriptScopeContext scope, PageBlockFragment block) + { + if (block.Argument.IsNullOrWhiteSpace()) + throw new NotSupportedException("'capture' block is missing variable name to assign output to"); + + var literal = block.Argument.AdvancePastWhitespace(); + bool appendTo = false; + if (literal.StartsWith("appendTo ")) + { + appendTo = true; + literal = literal.Advance("appendTo ".Length); + } + + literal = literal.ParseVarName(out var name); + if (name.IsNullOrEmpty()) + throw new NotSupportedException("'capture' block is missing variable name to assign output to"); + + literal = literal.AdvancePastWhitespace(); + + var argValue = literal.GetJsExpressionAndEvaluate(scope); + + var scopeArgs = argValue as Dictionary; + + if (argValue != null && scopeArgs == null) + throw new NotSupportedException("Any 'capture' argument must be an Object Dictionary"); + + return new Tuple(name.ToString(), scopeArgs, appendTo); + } +} +``` diff --git a/wwwroot/gfm/blocks/15.html b/wwwroot/gfm/blocks/15.html new file mode 100644 index 0000000..7e7a13a --- /dev/null +++ b/wwwroot/gfm/blocks/15.html @@ -0,0 +1,9 @@ +
    {{#capture todoMarkdown { items:[1,2,3] } }}
    +## TODO List
    +{{#each items}}
    +  - Item {{it}}
    +{{/each}}
    +{{/capture}}
    +
    +{{todoMarkdown |> markdown}}
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/15.md b/wwwroot/gfm/blocks/15.md new file mode 100644 index 0000000..e2ba9d1 --- /dev/null +++ b/wwwroot/gfm/blocks/15.md @@ -0,0 +1,10 @@ +```hbs +{{#capture todoMarkdown { items:[1,2,3] } }} +## TODO List +{{#each items}} + - Item {{it}} +{{/each}} +{{/capture}} + +{{todoMarkdown |> markdown}} +``` diff --git a/wwwroot/gfm/blocks/16.html b/wwwroot/gfm/blocks/16.html new file mode 100644 index 0000000..1da7b25 --- /dev/null +++ b/wwwroot/gfm/blocks/16.html @@ -0,0 +1,55 @@ +
    /// <summary>
    +/// Converts markdown contents to HTML using the configured MarkdownConfig.Transformer.
    +/// If a variable name is specified the HTML output is captured and saved instead. 
    +///
    +/// Usages: {{#markdown}} ## The Heading {{/markdown}}
    +///         {{#markdown content}} ## The Heading {{/markdown}} HTML: {{content}}
    +/// </summary>
    +public class MarkdownScriptBlock : ScriptBlock
    +{
    +    public override string Name => "markdown";
    +    
    +    public override async Task WriteAsync(
    +        ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
    +    {
    +        var strFragment = (PageStringFragment)block.Body[0];
    +
    +        if (!block.Argument.IsNullOrWhiteSpace())
    +        {
    +            Capture(scope, block, strFragment);
    +        }
    +        else
    +        {
    +            await scope.OutputStream.WriteAsync(MarkdownConfig.Transform(strFragment.ValueString), token);
    +        }
    +    }
    +
    +    private static void Capture(
    +        ScriptScopeContext scope, PageBlockFragment block, PageStringFragment strFragment)
    +    {
    +        var literal = block.Argument.AdvancePastWhitespace();
    +
    +        literal = literal.ParseVarName(out var name);
    +        var nameString = name.ToString();
    +        scope.PageResult.Args[nameString] = MarkdownConfig.Transform(strFragment.ValueString).ToRawString();
    +    }
    +}
    +

    +Use Alternative Markdown Implementation

    +

    By default ServiceStack uses an interned implementation of MarkdownDeep for rendering markdown, you can get ServiceStack to use an alternate +Markdown implementation by overriding MarkdownConfig.Transformer.

    +

    E.g. to use the richer Markdig implementation, install the Markdig +NuGet package:

    +
    PM> Install-Package Markdig
    +
    +

    Then assign a custom IMarkdownTransformer:

    +
    public class MarkdigTransformer : IMarkdownTransformer
    +{
    +    private Markdig.MarkdownPipeline Pipeline { get; } = 
    +        Markdig.MarkdownExtensions.UseAdvancedExtensions(new Markdig.MarkdownPipelineBuilder()).Build();
    +
    +    public string Transform(string markdown) => Markdig.Markdown.ToHtml(markdown, Pipeline);
    +}
    +
    +MarkdownConfig.Transformer = new MarkdigTransformer();
    +
    \ No newline at end of file diff --git a/wwwroot/gfm/blocks/16.md b/wwwroot/gfm/blocks/16.md new file mode 100644 index 0000000..dca51a4 --- /dev/null +++ b/wwwroot/gfm/blocks/16.md @@ -0,0 +1,63 @@ +```csharp +/// +/// Converts markdown contents to HTML using the configured MarkdownConfig.Transformer. +/// If a variable name is specified the HTML output is captured and saved instead. +/// +/// Usages: {{#markdown}} ## The Heading {{/markdown}} +/// {{#markdown content}} ## The Heading {{/markdown}} HTML: {{content}} +/// +public class MarkdownScriptBlock : ScriptBlock +{ + public override string Name => "markdown"; + + public override async Task WriteAsync( + ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) + { + var strFragment = (PageStringFragment)block.Body[0]; + + if (!block.Argument.IsNullOrWhiteSpace()) + { + Capture(scope, block, strFragment); + } + else + { + await scope.OutputStream.WriteAsync(MarkdownConfig.Transform(strFragment.ValueString), token); + } + } + + private static void Capture( + ScriptScopeContext scope, PageBlockFragment block, PageStringFragment strFragment) + { + var literal = block.Argument.AdvancePastWhitespace(); + + literal = literal.ParseVarName(out var name); + var nameString = name.ToString(); + scope.PageResult.Args[nameString] = MarkdownConfig.Transform(strFragment.ValueString).ToRawString(); + } +} +``` + +### Use Alternative Markdown Implementation + +By default ServiceStack uses an interned implementation of `MarkdownDeep` for rendering markdown, you can get ServiceStack to use an alternate +Markdown implementation by overriding `MarkdownConfig.Transformer`. + +E.g. to use the richer [Markdig](https://github.com/lunet-io/markdig) implementation, install the [Markdig](https://www.nuget.org/packages/Markdig/) +NuGet package: + + PM> Install-Package Markdig + +Then assign a custom `IMarkdownTransformer`: + +```csharp +public class MarkdigTransformer : IMarkdownTransformer +{ + private Markdig.MarkdownPipeline Pipeline { get; } = + Markdig.MarkdownExtensions.UseAdvancedExtensions(new Markdig.MarkdownPipelineBuilder()).Build(); + + public string Transform(string markdown) => Markdig.Markdown.ToHtml(markdown, Pipeline); +} + +MarkdownConfig.Transformer = new MarkdigTransformer(); +``` + diff --git a/src/wwwroot/gfm/blocks/17.md b/wwwroot/gfm/blocks/17.md similarity index 89% rename from src/wwwroot/gfm/blocks/17.md rename to wwwroot/gfm/blocks/17.md index 0da24b7..5f14bb2 100644 --- a/src/wwwroot/gfm/blocks/17.md +++ b/wwwroot/gfm/blocks/17.md @@ -6,11 +6,11 @@ /// {{#partial mypartial {format:'html'} }} contents {{/partial}} /// {{#partial mypartial {format:'html', pageArg:1} }} contents {{/partial}} /// -public class TemplatePartialBlock : TemplateBlock +public class PartialScriptBlock : ScriptBlock { public override string Name => "partial"; - public override Task WriteAsync(TemplateScopeContext scope, PageBlockFragment block, CancellationToken token) + public override Task WriteAsync(ScriptScopeContext scope, PageBlockFragment block, CancellationToken token) { var literal = block.Argument.ParseVarName(out var name); if (name.IsNullOrEmpty()) @@ -38,4 +38,4 @@ public class TemplatePartialBlock : TemplateBlock return TypeConstants.EmptyTask; } } -``` \ No newline at end of file +``` diff --git a/wwwroot/gfm/blocks/18.html b/wwwroot/gfm/blocks/18.html new file mode 100644 index 0000000..e8597c4 --- /dev/null +++ b/wwwroot/gfm/blocks/18.html @@ -0,0 +1,7 @@ +
    {{#ul {if:hasAccess, each:items, where:'Age > 27', 
    +        class:['nav', !disclaimerAccepted ? 'blur' : ''], id:`menu-${id}`, selected:true} }}
    +    {{#li {class: {alt:isOdd(index), active:Name==highlight} }} {{Name}} {{/li}}
    +{{else}}
    +    <div>no items</div>
    +{{/ul}}
    +
    \ No newline at end of file diff --git a/src/wwwroot/gfm/blocks/18.md b/wwwroot/gfm/blocks/18.md similarity index 96% rename from src/wwwroot/gfm/blocks/18.md rename to wwwroot/gfm/blocks/18.md index bdb1545..7818679 100644 --- a/src/wwwroot/gfm/blocks/18.md +++ b/wwwroot/gfm/blocks/18.md @@ -1,8 +1,8 @@ -```js +```hbs {{#ul {if:hasAccess, each:items, where:'Age > 27', class:['nav', !disclaimerAccepted ? 'blur' : ''], id:`menu-${id}`, selected:true} }} {{#li {class: {alt:isOdd(index), active:Name==highlight} }} {{Name}} {{/li}} {{else}}
    no items
    {{/ul}} -``` \ No newline at end of file +``` diff --git a/src/wwwroot/gfm/blocks/19.html b/wwwroot/gfm/blocks/19.html similarity index 84% rename from src/wwwroot/gfm/blocks/19.html rename to wwwroot/gfm/blocks/19.html index ebd25e7..057dac1 100644 --- a/src/wwwroot/gfm/blocks/19.html +++ b/wwwroot/gfm/blocks/19.html @@ -1,9 +1,9 @@
    {{#if hasAccess}}
    -    {{ items | where => it.Age > 27 | assignTo: items }}
    +    {{ items |> where => it.Age > 27 |> to => items }}
         {{#if !isEmpty(items)}}
    -        <ul {{ ['nav', !disclaimerAccepted ? 'blur' : ''] | htmlClass }} id="menu-{{id}}">
    +        <ul {{ ['nav', !disclaimerAccepted ? 'blur' : ''] |> htmlClass }} id="menu-{{id}}">
             {{#each items}}
    -            <li {{ {alt:isOdd(index), active:Name==highlight} | htmlClass }}>{{Name}}</li>
    +            <li {{ {alt:isOdd(index), active:Name==highlight} |> htmlClass }}>{{Name}}</li>
             {{/each}}
             </ul>
         {{else}}
    diff --git a/src/wwwroot/gfm/blocks/19.md b/wwwroot/gfm/blocks/19.md
    similarity index 78%
    rename from src/wwwroot/gfm/blocks/19.md
    rename to wwwroot/gfm/blocks/19.md
    index 165b801..fe7152a 100644
    --- a/src/wwwroot/gfm/blocks/19.md
    +++ b/wwwroot/gfm/blocks/19.md
    @@ -1,10 +1,10 @@
     ```hbs
     {{#if hasAccess}}
    -    {{ items | where => it.Age > 27 | assignTo: items }}
    +    {{ items |> where => it.Age > 27 |> to => items }}
         {{#if !isEmpty(items)}}
    -