TouchDevelop/ast/help.ts

2334 строки
96 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev {
export interface JsonProgress {
kind: string;
userid: string;
progressid: string;
//guid?: string;
index: number;
completed?: number;
}
export interface JsonProgressStep {
index: number;
text: string;
count: number;
minDuration?: number;
medDuration?: number;
medModalDuration?: number;
medPlayDuration?: number;
}
export interface JsonProgressStats {
kind: string; // progressstats
publicationId: string;
count: number;
steps: JsonProgressStep[];
}
export interface JsonCapability
{
name: string;
iconurl: string;
}
export interface JsonCanExportApp {
canExport: boolean;
reason?: string;
}
export interface JsonIdObject
{
kind:string;
id:string; // id
url:string; // website for human consumption
}
export interface JsonPublication extends JsonIdObject
{
time:number;// time when publication was created
userid:string; // user id of user who published
userscore : number;
username:string;
userhaspicture:boolean;
}
// lite only
export interface JsonNotification extends JsonPubOnPub
{
notificationkind: string;
// if publicationkind == 'review', this will hold the script data
supplementalid: string;
supplementalkind: string;
supplementalname: string;
}
export interface JsonDocument
{
url:string; // website for human consumption
kind:string;
name: string; // document name
abstract: string; // document description
mimetype: string; // mimetype of document given by url
views: number; // approximate number of document views
thumburl: string;
}
export interface JsonArt extends JsonPublication
{
name: string;
description: string;
// if picture
pictureurl: string;
mediumthumburl: string;
thumburl: string;
flags: string[];
// if sound
wavurl: string;
aacurl: string;
// cloud.lite
bloburl?: string;
arttype?: string;
}
export interface JsonUser extends JsonIdObject
{
name:string; // user name
about:string; // user's about-me text
features:number; // number of features used by that user
receivedpositivereviews: number; // number of ♥ given to this user's scripts and comments
activedays: number;
subscribers:number; // number of users subscribed to this user
score:number; // overall score of this user
haspicture:boolean; // whether this use has a picture
}
export interface JsonScore {
points: number;
}
export interface JsonReceivedPositiveReviewsScore extends JsonScore {
scripts : JsonScript[];
}
export interface JsonFeature {
name:string;
title:string;
text:string;
count:number;
}
export interface JsonLanguageFeaturesScore extends JsonScore {
features: JsonFeature[];
}
export interface JsonUserScore
{
receivedPositiveReviews : JsonReceivedPositiveReviewsScore;
receivedSubscriptions : JsonScore;
languageFeatures : JsonLanguageFeaturesScore;
activeDays : JsonScore;
}
export interface JsonGroup extends JsonPublication {
name: string;
description: string;
allowexport: boolean;
allowappstatistics: boolean;
isrestricted : boolean;
isclass: boolean;
pictureid : string;
comments : number;
positivereviews : number;
subscribers: number;
}
export interface JsonCode {
kind: string; // “code”
time: number; // creation time in seconds since 1970
expiration: number; // in seconds since 1970
userid: string; // creator
username: string;
userscore: number;
userhaspicture: boolean;
verb: string; // “JoinGroup” for group invitation codes
data: string; // groupid for group invitation codes
}
export interface JsonScriptMeta {
youtube?: string;
instagram?: string;
}
export interface JsonScript extends JsonPublication
{
name:string;
description:string;
icon:string; // script icon name
iconbackground:string; // script icon background color in HTML notation
iconurl: string; // script icon picture url (obsolete)
iconArtId?: string; // art id for script icon
splashArtId?: string; // art id for script splash screen
positivereviews:number; // number of users who added ♥ to this script
cumulativepositivereviews:number;
comments:number; // number of discussion threads
subscribers:number;
capabilities:JsonCapability[]; // array of capabilities used by this script; each capability has two fields: name, iconurl
flows:any[]; // ???
haserrors:boolean; // whether this script has any compilation errors
rootid:string; // refers to the earliest script along the chain of script bases
updateid:string; // refers to the latest published successor (along any path) of that script with the same name and from the same user
updatetime:number;
ishidden:boolean; // whether the user has indicated that this script should be hidden
islibrary:boolean; // whether the user has indicated that this script is a reusable library
installations:number; // an approximation of how many TouchDevelop users have currently installed this script
runs:number; // an estimate of how often users have run this script
platforms:string[];
userplatform?:string[];
screenshotthumburl:string;
screenshoturl:string;
mergeids:string[];
editor?: string; // convention where empty means touchdevelop, for backwards compatibility
meta?: JsonScriptMeta; // only in lite, bag of metadata
updateroot: string; // lite-only
unmoderated?: boolean;
}
export interface JsonHistoryItem
{
kind: string; // InstalledScriptHistory
time: number; // seconds since 1970; indicates when code was backed up
historyid: string; // identifier of this item
scriptstatus: string; // “published”, “unpublished”
scriptname: string; // script name, mined from the script code
scriptdescription: string; // script description, mined from the script code
scriptid: string; // publication id if scriptstatus==”published”
scriptsize?: number;
isactive: boolean; // whether this history item is the currently active backup
entryNo?: number; // assigned when the thing is displayed
}
export function getScriptHeartCount(j:JsonScript)
{
if (!j) return -1;
if (j.updateid && j.updateid == j.id) return j.cumulativepositivereviews;
else return j.positivereviews || 0;
}
export interface JsonPubOnPub extends JsonPublication
{
publicationid:string; // script id that is being commented on
publicationname:string; // script name
publicationkind:string; //
}
export interface JsonPointer extends JsonPublication
{
path: string; // "td/contents"
scriptid: string; // where is it pointing to
redirect: string; // full URL or /something/on/the/same/host
description: string; // set to script title from the client
}
export interface JsonComment extends JsonPubOnPub
{
text:string; // comment text
nestinglevel:number; // 0 or 1
positivereviews:number; // number of users who added ♥ to this comment
comments:number; // number of nested replies available for this comment
assignedtoid?: string;
resolved?: string;
}
export interface JsonAbuseReport extends JsonPubOnPub
{
text:string; // report text
resolution:string;
publicationuserid:string;
}
export interface JsonChannel extends JsonPublication
{
name: string;
description:string;
pictureid : string;
comments : number;
positivereviews : number;
}
export interface JsonReview extends JsonPubOnPub
{
ispositive: boolean;
}
export interface JsonRelease extends JsonPublication
{
name: string;
releaseid:string;
labels:JsonReleaseLabel[];
buildnumber: number;
version: string;
commit: string;
branch: string;
}
export interface JsonReleaseLabel
{
name: string;
userid: string;
time: number;
releaseid: string;
}
export interface JsonEtag
{
id:string;
kind:string;
ETag:string;
}
export interface JsonList
{
items:JsonIdObject[];
etags:JsonEtag[];
continuation:string;
}
export interface JsonTag extends JsonIdObject
{
time:number;
name:string;
category:string;
description:string;
instances:number;
topscreenshotids:string[];
}
export interface JsonScreenShot extends JsonPubOnPub
{
pictureurl:string; // screenshot picture url
thumburl:string; // screenshot picture thumb url
}
export interface JsonVideoSource {
// poster to display the video
poster?: string;
// locale of this video source
srclang: string;
// video url
src: string;
// video mime type
type: string;
}
export interface JsonVideoTrack {
// local of this track
srclang: string;
// url of the track
src: string;
// kind, by default subtitles
kind?: string;
// label shown to user
label?: string;
}
// information to create a localized video with cc
export interface JsonVideo {
// poster to display the video
poster: string;
// closed caption tracks
tracks?: JsonVideoTrack[];
// localized video streams
sources: JsonVideoSource[];
}
export interface CanDeleteResponse {
publicationkind: string;
publicationname: string;
publicationuserid: string;
candelete:boolean;
candeletekind:boolean;
canmanage:boolean;
hasabusereports:boolean;
}
export interface SocialNetwork {
id: string;
name: string;
description: string;
parseIds: (text: string) => string[];
idToUrl: (id: string) => string;
idToHTMLAsync?: (id: string) => Promise; // HTMLElement;
}
var oembedCache: StringMap<HTML.OEmbed> = {};
var _socialNetworks: SocialNetwork[] = [
{
id: "art",
name: "TouchDevelop Art",
description: lf("Cover art ({0}/...)", Cloud.config.cdnUrl),
parseIds: text => {
var links = [];
if (text)
text.replace(/https?:\/\/.*\/pub\/([a-z]+)/gi,(m, id) => {
var ytid = id;
links.push(id)
});
return links;
},
idToUrl: id => Cloud.config.cdnUrl + "/pub/" + id,
// idToHTMLAsync: id => Promise.as(HTML.mkYouTubePlayer(id))
}, {
id: "youtube",
name: "YouTube",
description: lf("YouTube video (https://youtu.be/...)"),
parseIds: text => {
var links = [];
if (text)
text.replace(/https?:\/\/(youtu\.be\/([^\s]+))|(www\.youtube\.com\/watch\?v=([^\s]+))/gi,(m, m2, id1, m3, id2) => {
var ytid = id1 || id2;
links.push(ytid);
});
return links;
},
idToUrl: id => 'https://youtu.be/' + id,
idToHTMLAsync: id => Promise.as(HTML.mkYouTubePlayer(id))
}, {
id: "vimeo",
name: "Vimeo",
description: lf("vimeo video (https://vimeo.com/...)"),
parseIds: text => {
var links = [];
if (text)
text.replace(/https?:\/\/vimeo\.com\/\S*?(\d{6,})/gi,(m, id) => {
links.push(id);
});
return links;
},
idToUrl: id => "https://vimeo.com/" + id,
idToHTMLAsync: (id:string) : Promise => {
if (Cloud.lite)
return Promise.as(HTML.mkLazyVideoPlayer(
Util.fmt("{0}/thumbnail/512/vimeo/{1:uri}", Cloud.getServiceUrl(), id),
"https://player.vimeo.com/video/" + id))
var url = 'https://vimeo.com/' + id;
var p = oembedCache[url] ? Promise.as(oembedCache[url])
: Util.httpGetJsonAsync("https://vimeo.com/api/oembed.json?url=https%3A//vimeo.com/" + id)
.then(oembed => {
oembedCache[url] = oembed;
return oembed;
});
return p.then((oe: HTML.OEmbed) => HTML.mkLazyVideoPlayer(oe.thumbnail_url, "https://player.vimeo.com/video/" + id));
},
}, {
id: "instagram",
name: "Instagram",
description: lf("Instagram photo (https://instagram.com/p/...)"),
parseIds: text => {
var links = [];
if (text)
text.replace(/https?:\/\/instagram\.com\/p\/([a-z0-9]+)\/?/gi,(m, id) => {
links.push(id);
});
return links;
},
idToUrl: id => 'https://instagram.com/p/' + id + '/',
/* CORS issue
idToHTMLAsync: id => Util.httpGetJsonAsync('https://api.instagram.com/oembed?omit_script=true&url=https://instagram.com/p/' + id + '/')
.then(oembed => HTML.mkOEmbed('https://instagram.com/p/' + id + '/', oembed),
e => {
Util.log('oembed error:' + e);
return null;
})
*/
},
{
id: "vine",
name: "Vine",
description: lf("Vine animation (https://vine.co/v/...)"),
parseIds: text => {
var links = [];
if (text)
text.replace(/https?:\/\/vine\.co\/v\/([a-z0-9]+)\/?/gi,(m, id) => {
links.push(id);
});
return links;
},
idToUrl: id => 'https://vine.co/v/' + id,
/* CORS issue
idToHTMLAsync: id => Util.httpGetJsonAsync('https://vine.co/oembed.json?omit_script=true&url=https://vine.co/v/' + id)
.then(oembed => HTML.mkOEmbed('https://vine.co/v/' + id, oembed),
e => {
Util.log('oembed error:' + e);
return null;
})
*/
},
{
id: "twitter",
name: "Twitter",
description: lf("Twitter picture or tweet (https://twitter.com/.../status/...)"),
parseIds: text => {
var links = [];
if (text)
text.replace(/https:\/\/twitter\.com\/[^\/]+\/status\/[0-9]+\/?/gi,(m) => {
links.push(m);
});
return links;
},
idToUrl: id => id,
/* CORS issue
idToHTMLAsync: id => Util.httpGetJsonAsync('https://vine.co/oembed.json?omit_script=true&url=https://vine.co/v/' + id)
.then(oembed => HTML.mkOEmbed('https://vine.co/v/' + id, oembed),
e => {
Util.log('oembed error:' + e);
return null;
})
*/
},
];
export function socialNetworks(widgets : Cloud.EditorWidgets) : SocialNetwork[] {
if (!Cloud.hasPermission("post-script-meta"))
return [];
return _socialNetworks.filter(sn => !!widgets["socialNetwork" + sn.id]);
}
export class MdComments
{
public userid:string;
public scriptid: string;
public print = false;
public showCopy = true;
public useSVG = true;
public useExternalLinks = false;
public blockExternalLinks:boolean = undefined;
public pointerHelp = Cloud.lite;
public allowLinks = true;
public allowImages = true;
public allowVideos = true;
public designTime = false;
public forWeb = false;
public relativeLinks = false;
private currComment:AST.Comment;
constructor(public renderer:Renderer = null, private libName:string = null)
{
}
public serviceUrlOr(path:string, local:string)
{
if (this.useExternalLinks)
return this.relativeLinks ? path : Cloud.getServiceUrl() + path
else
return local
}
public topicLink(id:string)
{
return this.serviceUrlOr(Cloud.config.topicPath, Cloud.config.localTopicPath) + MdComments.shrink(id);
}
public appLink(id:string)
{
return this.serviceUrlOr("/app/", "") + id
}
static shrink(s:string)
{
return s ? s.replace(/[^A-Za-z0-9]/g, "").toLowerCase() : "";
}
static error(msg:string)
{
return "<span class='md-error'>" + Util.htmlEscape(msg) + " </span>";
}
static proxyVideos(v: JsonVideo) {
v.poster = HTML.proxyResource(v.poster);
v.sources.forEach(s => {
s.poster = HTML.proxyResource(s.poster);
s.src = HTML.proxyResource(s.src);
})
v.tracks.forEach(t => {
t.src = HTML.proxyResource(t.src);
})
}
static attachVideoHandlers(e: HTMLElement, autoPlay: boolean): void {
if (!Browser.directionAuto) {
Util.toArray(e.getElementsByClassName('md-tutorial')).forEach((v: HTMLElement) => dirAuto(v));
Util.toArray(e.getElementsByClassName('md-box-avatar-body')).forEach((v: HTMLElement) => dirAuto(v));
}
/*
var sns = socialNetworks(widgets).filter(sn => !!sn.idToHTMLAsync);
Util.toArray(e.getElementsByTagName('a')).forEach((v: HTMLAnchorElement) => {
sns.forEach(sn => sn.parseIds(v.href).forEach(id => {
v.style.display = 'none';
sn.idToHTMLAsync(id).done(pl => v.parentElement.insertBefore(pl, v));
}));
});
*/
Util.toArray(e.getElementsByClassName('md-video-link')).forEach((v: HTMLElement) => {
if (v.hasAttribute("data-playerurl")) v.withClick(() => v.innerHTML = HTML.mkVideoIframe(v.getAttribute("data-playerurl")));
else if (v.hasAttribute("data-video")) {
var lang = Util.getTranslationLanguage();
var jsvideo = <JsonVideo>JSON.parse(decodeURIComponent(v.getAttribute("data-video")));
if (!jsvideo) {
v.setChildren(div('', lf(":( invalid in video information")));
return;
}
// proxy all video resources
MdComments.proxyVideos(jsvideo);
var jssource = MdComments.findBestSource(jsvideo.sources);
if (!jssource) {
v.setChildren(div('', lf(":( could not find any video source")));
return;
}
var video = <HTMLVideoElement>createElement("video");
(<any>video).crossOrigin = "anonymous";
video.width = 300;
video.height = 150;
video.controls = true;
video.autoplay = autoPlay; // option?
video.preload = autoPlay ? "auto" : "none";
video.poster = jssource.poster || jsvideo.poster;
var source = <HTMLSourceElement>createElement("source");
source.src = jssource.src;
source.type = jssource.type;
video.appendChild(source);
if (jsvideo.tracks && jsvideo.tracks.length > 0) {
Util.log('loading tracks');
// dynamic tracks?
if (!Browser.videoTracks) {
var jstrack = MdComments.findBestTrack(jsvideo.tracks);
Util.log('best track: ' + jstrack.label);
Util.httpGetTextAsync(jstrack.src)
.done(wtt => {
var cues = HTML.parseWtt(wtt);
Util.log('found {0} cues', cues.length);
if (cues.length > 0) {
if (video.addTextTrack) {
var track = video.addTextTrack(jstrack.kind || "subtitles", jstrack.label || jstrack.srclang, jstrack.srclang);
cues.forEach(cue => track.addCue((<any>TextTrackCue)(cue.startTime, cue.endTime, cue.message)));
track.mode = track.SHOWING;
}
else {
var caption = <HTMLSpanElement>document.createElement('span');
caption.className = 'videoCaption';
var lastCue = undefined;
video.ontimeupdate = ev => {
var t = video.currentTime;
// shortcut
if (lastCue && lastCue.startTime <= t && t <= lastCue.endTime) return;
lastCue = undefined;
var i = 0;
while (i < cues.length) {
if (cues[i].startTime <= t && t <= cues[i].endTime) {
lastCue = cues[i];
break;
}
if (cues[i].endTime > t) break;
i++;
}
if (lastCue) {
caption.innerText = lastCue.message;
caption.style.opacity = "1.0";
}
else {
caption.style.opacity = "0.0";
}
};
v.classList.add('videoOuter');
v.setChildren([video, caption]);
}
}
}, e => {
// silently fail
Util.log('wtt: failed to load caption: ' + ((<any>e).message || ""));
});
} else {
video.appendChildren(jsvideo.tracks.map((jstrack, i) => {
var track = <HTMLTrackElement>createElement("track");
if (i == 0) track.default = true;
track.src = jstrack.src;
track.kind = jstrack.kind || "subtitles";
if (jstrack.srclang)
track.srclang = jstrack.srclang;
track.label = jstrack.label || jstrack.srclang;
return track
}));
}
}
v.setChildren(video);
} else if (v.hasAttribute("data-videosrc")) {
var vtid = v.getAttribute("data-videosrc");
var pid = v.getAttribute("data-videoposter");
if (autoPlay) {
var video = <HTMLVideoElement>createElement("video");
(<any>video).crossOrigin = "anonymous";
video.width = 300;
video.height = 150;
video.controls = true;
video.autoplay = true;
video.src = decodeURI(vtid);
video.poster = decodeURI(pid);
v.setChildren(video);
} else {
v.withClick(() => {
var video = <HTMLVideoElement>createElement("video");
(<any>video).crossOrigin = "anonymous";
video.width = 300;
video.height = 150;
video.controls = true;
video.autoplay = true;
video.src = decodeURI(vtid);
video.poster = decodeURI(pid);
v.setChildren(video);
v.withClick(() => { });
});
}
}
});
}
static findBestSource(sources: JsonVideoSource[]): JsonVideoSource {
if (!sources) return undefined;
var lang = Util.getTranslationLanguage() || "en";
var video: JsonVideoSource = sources.filter(t => t.srclang == lang)[0];
if (video) return video;
lang = lang.substr(0, 2);
video = sources.filter(t => t.srclang == lang)[0];
if (video) return video;
// bail out
return sources[0];
}
static findBestTrack(tracks: JsonVideoTrack[]): JsonVideoTrack {
if (!tracks) return undefined;
var lang = Util.getTranslationLanguage() || "en";
var track: JsonVideoTrack = tracks.filter(t => t.srclang == lang)[0];
if (track) return track;
lang = lang.substr(0, 2);
track = tracks.filter(t => t.srclang == lang)[0];
if (track) return track;
return tracks[0];
}
private sig(arg: string) {
var m = arg.split(/->/);
var property: IProperty = undefined;
if (m) {
// find type / property
var kindName = m[0];
var propertyName = m[1];
if (kindName && propertyName) {
if (Script) {
var lib = Script.librariesAndThis().filter(lib => lib.getName() == kindName)[0];
if (lib) {
var action = lib.getPublicActions().filter(action => action.getName() == propertyName)[0];
if (action) {
// bingo!
var r = "<div class=notranslate translate=no dir=ltr style='display:inline-block'><div class='md-snippet'>";
r += this.renderer.renderPropertySig(action, false, true, false);
r += "</div></div>";
return r;
}
}
}
}
}
return lf("could not find decl '{0}'", m);
}
private apiList(arg:string)
{
var prefix = ""
var m = /^([^:]*):(.*)/.exec(arg)
if (m) {
prefix = m[1]
arg = m[2]
}
var res = ""
arg.split(/,/).forEach((t) => {
if (/^\s*$/.test(t)) return;
var id = prefix + "_" + t;
var topic = HelpTopic.findById(id)
if (!topic)
res += MdComments.error("no such topic: " + id)
else if (topic.isPropertyHelp())
res += topic.renderLink(this, true);
else {
var st = topic.getSubTopics();
if (st.length > 0) {
res += st.map((t) => t.renderLink(this, true)).join("");
} else {
res += MdComments.error(id + " is neither property nor type");
}
}
})
return res;
}
static findArtStringResource(app : TDev.AST.App, id: string): string {
var artVar = app ? app.resources().filter((r) => MdComments.shrink(r.getName()) == MdComments.shrink(id))[0] : null;
if (artVar && artVar.url) {
return TDev.RT.String_.valueFromArtUrl(artVar.url);
}
return undefined;
}
static findArtId(id : string) : string {
var artVar = Script ? Script.resources().filter((r) => MdComments.shrink(r.getName()) == MdComments.shrink(id))[0] : null;
["https://az31353.vo.msecnd.net/pub/",
Cloud.config.cdnUrl + "/pub/"].forEach(artPref => {
if (artVar && artVar.url && artVar.url.slice(0, artPref.length) == artPref) {
var newId = artVar.url.slice(artPref.length)
if (/^\w+$/.test(newId)) id = newId;
}
})
return id;
}
private defaultRepl(macro:string, arg:string) : string
{
if (macro == "var") {
switch (arg) {
case "userid": return this.userid || Cloud.getUserId();
case "apihelp":
return MdComments.error("var:apihelp no longer supported");
default: return MdComments.error("unknown variable " + arg);
}
} else if (macro == "pic") {
var m = /^((https:\/\/[^:]+)|([\w ]+))(:(\d+)x(\d+))?(:.*)?/i.exec(arg);
if (m) {
var url = m[2];
var artId = m[3];
var width = parseFloat(m[5] || "12");
var height = parseFloat(m[6] || "12");
if (width > 30) {
height = 30 / width * height;
width = 30;
}
if (height > 20) {
width = 20 / height * width;
height = 20;
}
var caption = m[7];
if (artId && !url) {
artId = MdComments.findArtId(artId);
url = Cloud.artUrl(artId);
}
var urlsafe = HTML.proxyResource(url);
if (urlsafe == url) urlsafe = Util.fmt("{0:url}", url);
var r = "<div class='md-img'><div class='md-img-inner'>";
r += Util.fmt("<img src=\"{0}\" alt='picture' style='height:{1}em'/></div>", urlsafe, height);
if (caption) {
r += "<div class='md-caption'>" + this.formatText(caption.slice(1)) + "</div>";
}
r += "</div>";
return r;
} else {
m = /^(:(\d+)x(\d+))?(:.*)?/.exec(arg);
if (!m)
return MdComments.error(lf("invalid picture id"));
}
} else if (macro == "pici") {
var artId = MdComments.findArtId(arg);
var r = Util.fmt("<img class='md-img-inline' src='{0}' alt='picture' />", Cloud.artUrl(artId));
return r;
} else if (macro == "decl" || macro == "decl*") {
var decl = !Script ? null : !arg ? Script : Script.things.filter((t) => t.getName() == arg)[0];
if (this.currComment && this.currComment.mdDecl)
decl = this.currComment.mdDecl;
if (!decl) return MdComments.error(lf("no such decl {0}", arg));
return this.mkDeclSnippet(decl, macro == "decl*");
} else if (macro == "imports") {
if (!Script) return MdComments.error(lf("import can only be used from a script context"));
var r = "";
[
{ name: 'npm', url: 'https://www.npmjs.com/package/{0:q}', pkgs: Script.imports.npmModules },
{ name: 'cordova', url: 'http://plugins.cordova.io/#/package/{0:q}/', pkgs: Script.imports.cordovaPlugins },
{ name: 'bower', url: 'https://www.npmjs.com/package/{0:q}/', pkgs: Script.imports.bowerModules },
{ name: 'client', url: '{0}', pkgs: Script.imports.clientScripts },
{ name: 'pip', url: 'https://pypi.python.org/pypi/{0:q}/', pkgs: Script.imports.pipPackages },
{ name: 'touchdevelop', url: '#pub:{0:q}', pkgs: Script.imports.touchDevelopPlugins }
].forEach(imports => {
var keys = Object.keys(imports.pkgs);
if (keys.length > 0) {
keys.forEach(key => {
var url = Util.fmt(imports.url, key);
var ver = imports.pkgs[key];
r += Util.fmt("<li>{3}: <a target='blank' href='{1}'>{0:q}{2}</a></li>\n", key, url, ver ? " " + ver : "", imports.name);
});
}
});
if (!r) return this.designTime ? "{imports}" : "";
else return "<h3>" + lf("imports") + "</h3><ul>" + r + "</ul>";
} else if (macro == "stlaunch") {
if (!this.scriptid) return "";
return "<h2>" + lf("follow tutorial online") + "</h2><div class='md-box-print print-big'>" + lf("Follow this tutorial online at <b>{1}/{0:q}</b>", this.scriptid, Cloud.config.shareUrl) + ".</div>";
} else if (macro == "stcmd") {
var mrun = /^run(:(.*))?/.exec(arg)
if (mrun) return Util.fmt("<b>Run your program: {0:q}</b>", mrun[2] || "");
var mcomp = /^compile(:(.*))?/.exec(arg)
if (mcomp) return Util.fmt("<b>Compile your program: {0:q}</b>", mcomp[2] || "");
return Util.fmt("<b>tutorial command: {0:q}</b>", arg)
} else if (macro == "adddecl") {
return this.designTime ? Util.fmt("<b>Add the declaration:</b>") : ""
} else if (macro == "stcode") {
return "<b>(code of the step)</b>";
} else if (macro == "internalstepid") {
return Util.fmt("<h1 class='stepid'>{0:q}</h1>", arg)
} else if (macro == "follow") {
return Util.fmt("<!-- FOLLOW --><a href=\"{0:q}\">{1:q}</a>",
this.appLink("#hub:follow-tile:" + MdComments.shrink(arg)),
arg)
} else if (macro == "topictile") {
return Util.fmt("<!-- FOLLOW --><a href=\"{0:q}\">{1:q}</a>",
this.appLink("#topic-tile:" + MdComments.shrink(arg)),
arg)
} else if (macro == "pub") {
var args = arg.split(':');
if (args.length != 2)
return MdComments.error(lf("missing publication title: {pub:id:title}"));
return Util.fmt("<!-- FOLLOW --><a href=\"{0:q}\">{1:q}</a>",
this.appLink("#pub:" + MdComments.shrink(args[0])),
args[1])
} else if (macro == "section") {
var mm = /^([^:;]+)[;:](.*)/.exec(arg)
var sectName = mm ? mm[1] : arg
var output = Util.fmt("--------------------------- <b>{0:q}</b> ---------------------------", sectName)
if (mm) {
mm[2].split(';').forEach(a => {
output += Util.fmt("<div class='md-section'>;; {0:q}</div>", a)
})
}
return output;
} else if (macro == "hide" || macro == "priority" || macro == "template" || macro == "highlight" ||
macro == 'box' || macro == "code" || macro == "widgets" || macro == "templatename" ||
macro == "hints" || macro == "pichints" || macro == "enum" || macro == "language" || macro == "weight" || macro == "namespace" ||
macro == "parenttopic" || macro == "docflags" || macro == "stprecise" || macro == "flags" || macro == "action" ||
macro == "topic" ||
macro == "stvalidator" || macro == "stnoprofile" || macro == "stauto" || macro == "sthints" ||
macro == "stcode" || macro == "storder" || macro == "stdelete" || macro == "stcheckpoint" || macro == "sthashtags" ||
macro == "stnocheers" || macro == "steditormode" || macro == "stnexttutorials" || macro == "stmoretutorials" ||
macro == "translations" || macro == "stpixeltracking" || macro == "steventhubstracking" || macro == "icon" || macro == "help"
) {
if (this.designTime)
return "{" + macro + (arg ? ":" + Util.htmlEscape(arg) : "") + "}";
else return "";
} else if (macro == "api") {
return this.apiList(arg);
} else if (macro == "sig") {
return this.sig(arg);
} else if (macro == "youtube") {
if (!this.allowVideos) return "";
if (this.blockExternal()) return this.blockLink("")
if (!arg)
return MdComments.error("youtube: missing video id");
else {
return Util.fmt("<div class='md-video-link' data-playerurl='{0:q}'>{1}</div>",
Util.fmt("//www.youtube-nocookie.com/embed/{0:uri}?modestbranding=1&autoplay=1&autohide=1", arg),
SVG.getVideoPlay(Util.fmt('https://img.youtube.com/vi/{0:q}/hqdefault.jpg', arg))
);
}
} else if (Cloud.lite && macro == "vimeo") {
if (!this.allowVideos) return "";
if (this.blockExternal()) return this.blockLink("")
var args = arg.split(/:/);
if (!/^\d+$/.test(args[0]))
return MdComments.error("vimeo: video id should be a number");
else {
var url = Util.fmt("//player.vimeo.com/video/{0:uri}?autoplay=1&badge=0", args[0]);
if (/loop/.test(args[1])) url += "&loop=1";
return Util.fmt("<div class='md-video-link' data-playerurl='{0:q}'>{1}</div>",
url,
SVG.getVideoPlay(Util.fmt("{0}/thumbnail/512/vimeo/{1:uri}", this.relativeLinks ? "" : Cloud.getServiceUrl(), args[0]))
);
}
} else if (macro == "channel9") {
if (!this.allowVideos) return "";
if (this.blockExternal()) return this.blockLink("")
if (!arg)
return MdComments.error("channel9: missing MP4 url");
var video = arg.replace(/^http:\/\/video/, 'https://sec');
var poster = video.replace(/\.mp4$/, '_512.jpg');
return Util.fmt("<div class='md-video-link' data-videoposter='{0:url}' data-videosrc='{1:url}'>{2}</div>",
poster,
video,
SVG.getVideoPlay(poster))
} else if (macro == "video") {
if (!this.allowVideos) return "";
if (this.blockExternal()) return this.blockLink("")
if (!arg)
return MdComments.error("video: missing video preview and url");
var res = MdComments.findArtStringResource(Script, arg);
if (res) return MdComments.expandJsonVideo(res);
var urls = arg.split(',');
if (urls.length != 2)
return MdComments.error("video: must have <preview url>,<mp4 url> arguments");
else {
return Util.fmt("<div class='md-video-link' data-videoposter='{0:url}' data-videosrc='{1:url}'>{2}</div>",
urls[0],
urls[1],
SVG.getVideoPlay(urls[0]))
}
} else if (macro == "cap") {
if (!arg)
return MdComments.error("cap: requires a comma separated list of required capabilities");
var required = AST.App.fromCapabilityList(arg.split(/,/));
var current = PlatformCapabilityManager.current();
var missing = required & ~current;
if (!missing)
return "";
else
return "<div class='md-tutorial md-warning'>" +
"<strong>" + lf("This code might not work on your current device") + "</strong>: " + lf("missing {0} capabilities.", AST.App.capabilityName(missing)) +
"</div>";
} else if (macro == "webonly") {
if (this.designTime)
return MdComments.error("{webonly} macro doesn't do anything anymore")
else
return "";
} else if (macro == "bigbutton") {
if (!arg) return MdComments.error("bigbutton: requires <text>,<url> arguments");
var ms = arg.split(',');
if (ms.length != 2) return MdComments.error(lf("bigbutton: must have <text>,<url> arguments"));
return this.blockLink(ms[1]) || Util.fmt("<a class='md-bigbutton' target='_blank' rel='nofollow' href='{0:url}'>{1:q}</a>", ms[1], ms[0]);
} else if (macro == "shim") {
if (this.designTime) return "{" + macro + ":" + Util.htmlEscape(arg) + "}";
if (!arg) return "";
else return "<b>" + lf("<b>compiles to C++ function:</b> <span class='font-family: monospace'>{0}</span>", arg);
} else {
return null;
}
}
static expandJsonVideo(res: string) {
var video : JsonVideo;
try { video = <JsonVideo>JSON.parse(res); } catch (e) {}
if (!video) return MdComments.error("video: missing data");
var source = MdComments.findBestSource(video.sources);
if (!source) return MdComments.error('videoset: missing sources');
var poster = source.poster || video.poster;
if (!poster) return MdComments.error('video: missing poster');
// encode all stream for later use
return Util.fmt("<div class='md-video-link' data-video='{0:uri}'>{1}</div>",
JSON.stringify(video),
SVG.getVideoPlay(poster))
}
private blockExternal()
{
if (this.blockExternalLinks === undefined)
this.blockExternalLinks = !!(Cloud.isRestricted() && !Cloud.hasPermission("external-links"));
return this.blockExternalLinks
}
private blockLink(href:string)
{
if (!this.blockExternal()) return null
// TODO check if the link is external?
return MdComments.error(lf("sorry, external link not allowed"))
}
private expandInline(s:string, allowStyle = false, allowRepl = true):string
{
var inp = s;
var outp = "";
var applySpan = (rx:RegExp, repl:(m:RegExpExecArray)=>string) =>
{
var m = rx.exec(inp);
if (m) {
inp = inp.slice(m[0].length);
outp += repl(m);
return true;
}
return false;
}
var replace = (tag:string) => {
return (m:RegExpExecArray) => "<" + tag + ">" + this.expandInline(m[1]) + "</" + tag + ">";
}
var getReplCode = (m:RegExpExecArray) => {
var s = m[2]
if (m[1].length == 1) {
s = s.replace(/->/g, "\u2192");
s = s.replace(/(^|\s)(\w+)\u2192/, (m, pr, w) => {
switch (w) {
case "code": return pr + AST.codeSymbol;
case "data": return pr + AST.dataSymbol;
case "art": return pr + AST.artSymbol;
case "libs": return pr + AST.libSymbol;
case "records": return pr + AST.recordSymbol;
default: return m;
}
});
}
var inner = this.expandInline(s, false, false)
// ensure symbols render properly
inner = inner.replace(/[\u25b7\u25f3\u273f\u267B\u2339]/, '<span class="symbol">$&</span>');
return inner
}
var replCode = (m:RegExpExecArray) => {
var inner = getReplCode(m)
if (this.allowLinks && m[1].length == 1)
inner = inner.replace(/(^|[\s\(,])([\u2192\w ]+)($|[\s\(,])/g, (all, before, topic, after) => {
if (HelpTopic.findById(topic))
return before + "<a class='md-code-link' href='" + this.topicLink(topic) + "'>" + topic + "</a>" + after;
else
return all;
})
return "<code class=notranslate translate=no dir=ltr>" + inner + "</code>";
}
var replUI = (m:RegExpExecArray) => {
var inner = getReplCode(m)
return "<code class='md-ui' translate=no dir=ltr>" + inner + "</code>";
}
var quote = Util.htmlEscape;
while (inp) {
if (allowRepl &&
applySpan(/^\{([\w\*]+)(:([^{}]*))?\}/, (m) => {
if (!this.allowImages) return "";
var res = this.defaultRepl(m[1].toLowerCase(), m[3])
if (res == null) return MdComments.error("unknown macro '" + m[1] + "'")
else return res;
}))
continue;
if (applySpan(/^\&(\w+|#\d+);/, (m) => m[0]) ||
applySpan(/^([<>&])/, (m) => Util.htmlEscape(m[1])) ||
applySpan(/^\t/, (m) => MdComments.error("<tab>")) ||
false)
continue;
if (allowStyle && (
applySpan(/^(``)(.+?)``/, replCode) ||
applySpan(/^(``)\[(.+?)\]``/, replUI) ||
applySpan(/^(`)\[(.+?)\]`/, replUI) ||
applySpan(/^(`)([^\n`]+)`/, replCode) ||
applySpan(/^\*\*(.+?)\*\*/, replace("strong")) ||
applySpan(/^\*([^\n\*]+)\*/, replace("em")) ||
applySpan(/^__(.+?)__/, replace("strong")) ||
applySpan(/^_([^\n_]+)_/, replace("em")) ||
(this.allowLinks && applySpan(/^(http|https|ftp):\/\/[^\s]*/gi, (m) => {
var msg = this.blockLink(str)
if (msg) return msg
var str = m[0];
var suff = ""
var mm = /(.*?)([,\.;:\)]+)$/.exec(str);
if (mm) {
str = mm[1]
suff = mm[2];
}
str = this.expandInline(str);
str = str.replace(/"/g, "&quot;");
return "<a href=\"" + quote(str) + "\" target='_blank' rel='nofollow'>" + str + "</a>" + suff;
})) ||
false))
continue;
var tdev = this.serviceUrlOr("/", "")
if (allowRepl && (
applySpan(/^\{\#(\w+)\}/g, (m) => "<a name='" + quote(m[1]) + "'></a>") ||
applySpan(/^\[([^\[\]]*)\]\s*\(([^ \(\)\s]+)\)/, (m) => {
var name = m[1];
var href = m[2];
var acls = '';
var additions = ""
if (!name) {
name = href.replace(/^\//, "");
if (this.pointerHelp)
name = name.replace(/^[\w\/]+\//, "")
}
if (this.pointerHelp && /^\/[\w\-\/]+(#\w+)?$/.test(href))
href = href
else if (/^\/\w+(->\w+)?(#\w+)?$/.test(href))
href = this.topicLink(href.slice(1));
else if (/^\/script:\w+$/.test(href)) {
acls = 'md-link';
href = (this.useExternalLinks ? tdev + href.slice(8) : "#" + href.slice(1));
}
else if (/^#[\w\-]+:[\w,\-:]*$/.test(href))
href = (this.useExternalLinks ? tdev + "/app/#" : "#") + href.slice(1);
else if (/^#[\w\-]+$/.test(href))
href = (this.useExternalLinks ? "#" : "#goto:") + href.slice(1);
else if (/^(http|https|ftp):\/\//.test(href) || /^mailto:/.test(href)) {
var msg = this.blockLink(href)
if (msg) return msg
href = href; // OK
acls = "md-link md-external-link";
additions = " rel=\"nofollow\" target=\"_blank\"";
if (!this.useExternalLinks && this.allowLinks)
name += " \u2197";
}
else
return MdComments.error("invalid link '" + href + "'");
if (this.allowLinks)
return "<a class=\"" + acls + "\" href=\"" + quote(href) + "\"" + additions + ">" + quote(name) + "</a>";
else
return quote(name);
}) ||
false))
continue;
if (applySpan(/^(\s+)/, (m) => m[0]) ||
applySpan(/^(\w+\s*|.)/, (m) => m[0]))
continue;
}
return outp;
}
public formatInline(s: string)
{
var prev = this.allowLinks;
var prevI = this.allowImages;
var prevV = this.allowVideos;
this.allowLinks = false;
this.allowImages = false;
this.allowVideos = false;
try {
return this.expandInline(s, true, true);
} finally {
this.allowLinks = prev;
this.allowImages = prevI;
this.allowVideos = prevV;
}
}
public formatTextNoLinks(s: string)
{
var prev = this.allowLinks;
this.allowLinks = false;
try {
return this.formatText(s)
} finally {
this.allowLinks = prev;
}
}
public formatText(s: string, comment:AST.Comment = null)
{
if (!s) return s;
this.init();
var start = "";
var final = "";
var wrap = (tag:string, end = "") => {
if (!end) end = "/" + tag;
return (m, s) => {
if (start != "") return m;
start = "<" + tag + ">";
final = "<" + end + ">";
return s;
}
}
if (/^ /.test(s)) return "<pre>" + Util.htmlEscape(s.slice(4)) + "</pre>";
if (/^-{5,}\s*$/.test(s)) return this.designTime ? s : "<hr/>";
if (/^\*{5,}\s*$/.test(s)) return this.designTime ? s : "<div style='page-break-after:always'></div>";
s = s.replace(/^#\s+(.*)/, wrap("h1"));
s = s.replace(/^##\s+(.*)/, wrap("h2"));
s = s.replace(/^###\s+(.*)/, wrap("h3"));
s = s.replace(/^####\s+(.*)/, wrap("h4"));
s = s.replace(/^>\s+(.*)/, wrap("blockquote"));
s = s.replace(/^[-+*]\s+(.*)/, wrap("ul><li", "/li></ul"));
s = s.replace(/^\d+\.\s+(.*)/, wrap("ol><li", "/li></ol"));
wrap("div class='md-para'", "/div")("", "");
var prevComment = this.currComment;
try {
this.currComment = comment;
return start + this.expandInline(s, true, true) + final
} finally {
this.currComment = prevComment;
}
}
private mkCopyButton(tp:string, dt:string)
{
var r = Util.fmt("<button class='{0} copy-button' data-type='{1:q}' data-data='{2:q}'>",
this.useSVG ? "code-button" : "wall-button", tp, dt);
if (!this.useSVG)
r += "copy";
else
r += Util.fmt('<div class="code-button-frame">{0}</div>', SVG.getIconSVGCore("copy,black"));
r += "</button>";
return r;
}
private depthLimit = 0;
public mkDeclSnippet(decl:AST.Decl, skipComments = false, formatComments = false, cls = 'md-snippet', addDepth = 0)
{
if (this.depthLimit < 0 || !this.renderer) return "<div class='md-message'>[" + Util.htmlEscape(decl.getName()) + " goes here]</div>";
var r = "<div class=notranslate translate=no dir=ltr><div class='" + cls + "'>";
var prev = this.renderer.formatComments;
var prevSc = this.renderer.skipComments;
var prevMd = this.renderer.mdComments
try {
this.depthLimit += addDepth - 1;
this.renderer.mdComments = this;
this.renderer.formatComments = formatComments;
this.renderer.skipComments = skipComments;
r += this.renderer.renderDecl(decl);
} finally {
this.depthLimit -= addDepth - 1;
this.renderer.formatComments = prev;
this.renderer.skipComments = prevSc;
this.renderer.mdComments = prevMd;
}
if (this.showCopy && !formatComments)
r += this.mkCopyButton("decls", decl.serialize());
r += "</div></div>";
return r;
}
private mkSnippet(stmts:AST.Stmt[])
{
if (!this.renderer) return "<div class='md-message'>[inline snippet goes here]</div>";
var r = "<div class=notranslate translate=no dir=ltr><div class='md-snippet'>";
r += this.renderer.renderSnippet(stmts);
if (this.showCopy) {
var block = new AST.Block();
block.stmts = stmts; // don't use setChildren(), that would override the parent
var d = block.serialize()
// OK, this is a hack...
if (this.libName)
d = d.replace(/code\u2192/g, '@\\u267b ->' + AST.Lexer.quoteId(this.libName) + '->');
r += this.mkCopyButton("block", d);
}
r += "</div></div>";
return r;
}
private init()
{
if (!this.renderer) return;
if (this.libName) {
this.renderer.codeReplacement = '<span class="id symbol">' + AST.libSymbol + '</span>\u200A' + this.libName + '\u200A';
if (this.showCopy)
this.renderer.codeReplacement += '\u2192\u00A0';
} else {
this.renderer.codeReplacement = null;
}
}
public extract(a:AST.Action)
{
return this.extractStmts(a.body.stmts)
}
public extractStmts(stmts:AST.Stmt[])
{
function isEmptyComment(s:AST.Stmt)
{
return s.docText() == ""
}
this.init();
var output = "";
var currBox = null;
for (var i = 0; i < stmts.length; ) {
var cmt = stmts[i].docText()
if (cmt != null) {
var m = /^\s*\{hide(:[^{}]*)?\}\s*$/.exec(cmt);
if (m) {
if (m[1]) output += this.formatText(m[1]);
var j = i + 1;
while (j < stmts.length) {
if (/^\s*\{\/hide\}\s*$/.test(stmts[j].docText())) {
j++;
break;
}
j++;
}
i = j;
} else if (i == 0 && cmt == '{var:apihelp}') {
i++;
} else if ((m = /^\s*(\{code}|````)\s*$/.exec(cmt)) != null) {
var j = i + 1;
var seenStmt = false;
while (j < stmts.length) {
if (/^\s*(\{\/code}|````)\s*$/.test(stmts[j].docText()))
break;
j++;
}
output += this.mkSnippet(stmts.slice(i + 1, j));
i = j + 1;
} else if ((m = /^\s*\{section:(.*)\}\s*$/.exec(cmt)) != null) {
var mm = /^([^:;]+)[;:](.*)/.exec(m[1])
var sectName = mm ? mm[1] : m[1]
output += Util.fmt("<hr class='md-section' data-name='{0:uri}' data-arguments='{1:uri}' />", sectName, mm ? mm[2] : "")
i++;
} else if ((m = /^\s*\{box:([^{}]*)\}\s*$/.exec(cmt)) != null) {
if (currBox) output += "</div>";
var parts = m[1].split(':');
var boxClass = parts[0];
var boxDir = "";
var boxHd = "";
var boxFt = "";
var boxCss = "md-box";
switch (boxClass) {
case "card":
boxHd = "<div class='md-box-header'>" + Util.htmlEscape(parts[1]) + "</div>";
break;
case "hint":
boxHd = "<div class='md-box-header'>" + lf("hint") + "</div>";
break;
case "exercise":
boxHd = "<div class='md-box-header'>" + lf("exercise") + "</div>";
break;
case "example":
boxHd = "<div class='md-box-header'>" + lf("example") + "</div>";
break;
case "nointernet":
boxHd = "<div class='md-box-header'>" + lf("no internet?") + "</div>";
break;
case "portrait":
boxHd = "<div class='md-box-header-print'>" + lf("device in portrait") +"</div>";
boxCss = "";
break;
case "landscape":
boxHd = "<div class='md-box-header-print'>" + lf("device in landscape") + "</div>";
boxCss = "";
break;
case "print":
case "screen":
case "block":
boxHd = "";
boxCss = "";
break;
case "avatar":
var artId = MdComments.findArtId(parts[1]);
boxHd = Util.fmt("<img class='md-box-avatar-img' src='{0}' /><div class='md-box-avatar-body' dir='auto'>", Cloud.artUrl(artId));
boxFt = '</div>';
boxCss = '';
boxClass = 'avatar';
boxDir = "dir='ltr'";
break;
case "column":
boxHd = "";
boxCss = "col-xs-12 col-sm-6 col-md-4";
break;
default:
boxHd = MdComments.error("no such box type " + m[1] + ", use hint, exercise, example or nointernet")
boxClass = 'hint'
break;
}
currBox = boxClass;
output += Util.fmt("<div class='{0} md-box-{1}' {2}>{3}", boxCss, boxClass, boxDir, boxHd)
i++;
} else if (/^\s*\{\/box(:[^{}]*)?\}\s*$/.test(cmt)) {
if (currBox) {
output += boxFt + "</div>";
currBox = null;
} else {
output += MdComments.error("no box to close")
}
i++;
} else {
output += this.formatText(cmt);
i++;
}
} else {
var j = i;
while (j < stmts.length) {
if (stmts[j].docText() != null) break;
j++;
}
output += this.mkSnippet(stmts.slice(i, j));
i = j;
}
}
if (currBox) output += "</div>";
var fixMultiline = (s:string) => {
s = s.replace(/(<\/ul><ul>|<\/ol><ol>|<\/pre><pre>|<\/div><!-- HL --><div class='code-highlight'>)/g, "");
s = s.replace(/<ul><li><!-- FOLLOW -->/g, "<ul class='tutorial-list'><li>")
return s;
}
output = fixMultiline(output);
return "<div class='md-tutorial' dir='auto'>" + output + "</div>";
}
static splitDivs(tut:string)
{
var splits:string[] = []
var leftovers = tut.replace(/([^<]+|<[^<>]+>)/g, (m, g) => {
splits.push(g)
return ""
})
Util.assert(!leftovers, "should be nothing left: " + leftovers)
if (/<div.*md-tutorial.*>/.test(splits[0]) &&
/<\/div>/.test(splits.peek())) {
splits.shift()
splits.pop()
}
var stack:string[] = []
var output:string[] = []
var curr = ""
var norm = (s:string) => s.toLowerCase().replace(/\s*/g, "")
splits.forEach(s => {
curr += s
if (norm(s) == stack.peek())
stack.pop()
else {
var m = /<([a-z0-9\-]+)(\s|>)/i.exec(s)
if (m && /^(div|p|ul|ol|h[1-9]|blockquote)$/i.test(m[1]))
stack.push(norm("</" + m[1] + ">"))
}
if (stack.length == 0) {
output.push(curr)
curr = ""
}
})
if (curr) output.push(curr)
return output
}
}
export interface HelpTopicJson
{
name: string;
id: string;
rootid: string;
description: string;
icon:string;
iconbackground:string;
iconArtId?:string;
time?:number;
userid?:string;
text: string;
priority:number;
platforms?:string[];
parentTopic?:string;
screenshot?: string;
helpPath?: string;
}
export interface HelpTopicInfoJson {
title: string;
description: string;
body: string[];
translations?: StringMap<string>; // locale -> script id
manual?: boolean;
}
export class HelpTopic
{
private searchCache:string;
public app:AST.App;
private apiKind:Kind;
private apiProperty:IProperty;
private subTopics:StringMap<HelpTopic>;
private initPromise:Promise;
public id:string;
public fromJson:JsonScript;
public isBuiltIn = false;
public isTutorial(): boolean {
return this.hashTags()
&& /#(stepbystep|hourofcode)\b/i.test(this.allHashTags)
&& !/template|notes/i.test(this.json.name);
}
public isHourOfCode(): boolean {
return this.hashTags() && /#HourOfCode\b/i.test(this.allHashTags);
}
private translatedTopic: HelpTopicInfoJson;
public nestingLevel:number;
public parentTopic:HelpTopic = null;
public childTopics:HelpTopic[] = [];
static contextTopics:HelpTopic[] = [];
static shippedScripts:any;
static scriptTemplates:any[];
static getScriptAsync:(id:string)=>Promise;
static _topics:HelpTopic[] = [];
static _initalized = false;
constructor(public json:HelpTopicJson)
{
this.id = MdComments.shrink(this.json.name);
if (!json.text && HelpTopic.shippedScripts.hasOwnProperty(json.id))
json.text = HelpTopic.shippedScripts[json.id]
}
static fromJsonScript(e:JsonScript)
{
var t = new HelpTopic({
name: e.name,
id: e.id,
description: e.description,
icon: e.icon,
iconbackground: e.iconbackground,
iconArtId: e.iconArtId,
userid: e.userid,
time:e.time,
text: null,
rootid: e.rootid,
platforms: e.platforms,
priority: 20000,
});
t.id = e.id;
t.hashTags();
t.fromJson = e;
return t;
}
static fromScriptText(id:string, text:string)
{
var app = AST.Parser.parseScript(text);
var t = HelpTopic.fromScript(app, false);
if (id) {
t.id = id;
t.json.id = id;
}
return t;
}
static fromScript(app:AST.App, useApp = true)
{
var t = new HelpTopic({
name: app.getName(),
id: "none",
description: app.getDescription(),
icon: SVG.justName(app.iconPath()),
iconbackground: app.htmlColor(),
text: app.serialize(),
rootid: "none",
priority: 20000
})
if (useApp) {
t.app = app;
t.initPromise = Promise.as();
}
return t;
}
static forLibraryAction(act:AST.LibraryRefAction)
{
var t = new HelpTopic({
name: act.getName(),
id: "",
description: act.getDescription(),
icon: "Recycle",
iconbackground: "#008800",
text: "",
rootid: "none",
priority: 20000,
helpPath: act.getHelpPath()
})
t.apiProperty = act
t.apiKind = api.core.Nothing
t.app = act.parentLibrary().resolved
t.id = Util.tagify(t.app.getName() + " " + act.getName())
t.json.description += " #" + t.id
return t;
}
static getAllTutorials(): HelpTopic[]{
var tuts = HelpTopic.getAll().filter(ht => ht.isTutorial());
return tuts;
}
static getAll() : HelpTopic[]
{
if (!HelpTopic._initalized) {
HelpTopic._initalized = true;
var bestForTag:any = {}
HelpTopic._topics.forEach((topic) => {
bestForTag[topic.id] = topic;
var ht = topic.hashTags().map(MdComments.shrink);
if (ht.indexOf("docs") < 0) topic.hashTagsCache.push("#docs");
var tt = Util.toHashTag(topic.json.name)
if (ht.indexOf(MdComments.shrink(tt)) < 0) topic.hashTagsCache.push(tt);
})
api.getKinds().forEach((k:Kind) => {
if (k.isPrivate || k instanceof ThingSetKind || (k.isData && k.getContexts() == KindContext.None)) return;
var tagName = Util.toHashTag(k.getName());
var topic:HelpTopic = bestForTag[MdComments.shrink(tagName)]
if (!topic) {
topic = new HelpTopic({
name: k.getName(),
id: "",
description: "",
icon: SVG.justName(k.icon()) || "Document",
iconbackground: "#7d26cd",
priority: 11000,
rootid: "none",
text: ""
})
topic.hashTagsCache = ["#docs", tagName]
HelpTopic._topics.push(topic);
}
topic.id = MdComments.shrink(tagName.replace(/^#/, ""));
topic.json.name = k.getName();
topic.json.description = k.getHelp(false)
topic.json.parentTopic = "api"
topic.apiKind = k;
var outerTopic = topic;
outerTopic.subTopics = {};
k.listProperties().forEach((prop) => {
if (!prop.isBrowsable()) return;
var propname = prop.parentKind.getName() + prop.getArrow() + prop.getName();
var tagName = "#" + prop.helpTopic();
var topic:HelpTopic = bestForTag[MdComments.shrink(tagName)]
if (!topic) {
var j = outerTopic.json;
topic = new HelpTopic({
name: propname,
id: "",
rootid: "none",
description: "",
icon: j.icon,
iconbackground: j.iconbackground,
priority: 10000 + j.priority,
text: ""
});
topic.hashTagsCache = ["#docs", tagName]
HelpTopic._topics.push(topic);
}
topic.id = MdComments.shrink(tagName.replace(/^#/, ""));
topic.json.name = propname;
topic.json.description = prop.getDescription(true)
// they clash for "unknown -> :="
if (outerTopic.id != topic.id)
topic.json.parentTopic = outerTopic.id
outerTopic.subTopics[topic.id] = topic;
topic.apiKind = k;
topic.apiProperty = prop;
})
})
HelpTopic._topics.sort((a, b) => {
var d = a.json.priority - b.json.priority
if (d) return d;
return Util.stringCompare(a.json.name, b.json.name);
})
HelpTopic._topics.forEach((t) => t.isBuiltIn = true)
HelpTopic.topicCache = {}
HelpTopic.topicByScriptId = {}
HelpTopic._topics.forEach((t) => {
HelpTopic.topicByScriptId[t.json.id] = t
HelpTopic.topicCache[t.id] = t
})
var workSet:StringMap<number> = {}
var getNesting = (t:HelpTopic) => {
if (workSet.hasOwnProperty(t.id) && workSet[t.id]) workSet[t.id] = 2
if (t.nestingLevel !== undefined) return t.nestingLevel
t.nestingLevel = 0
if (t.json.parentTopic && HelpTopic.topicCache.hasOwnProperty(t.json.parentTopic)) {
var par = HelpTopic.topicCache[t.json.parentTopic]
workSet[t.id] = 1
var pn = getNesting(par)
if (workSet[t.id] == 1) {
t.parentTopic = par
t.nestingLevel = getNesting(par) + 1
t.parentTopic.childTopics.push(t)
} else {
if (dbg)
Util.log("DOCERR: cycle in help topics involving " + t.id)
}
workSet[t.id] = 0
} else {
if (dbg) {
if (t.json.parentTopic)
Util.log("DOCERR: parent topic " + t.json.parentTopic + " doesn't exists (in '" + t.json.name + "' /" + t.json.id + ")")
else
Util.log("DOCERR: no parent topic set for '" + t.json.name + "' /" + t.json.id)
}
}
return t.nestingLevel
}
HelpTopic._topics.forEach(getNesting)
HelpTopic._topics.forEach(t => {
if (t.childTopics.length > 0)
t.childTopics.sort((a, b) => Util.stringCompare(a.json.name, b.json.name))
})
}
return HelpTopic._topics;
}
public renderLink(mdcmt:MdComments, withKind = false)
{
var p = this.apiProperty;
var r = "";
r += "<a href='" + mdcmt.topicLink(this.id) + "' class='md-api-entry-link'><div class='md-api-entry'>";
if (this.json.text)
r += "<div class='md-more'>more info</div>"
if (mdcmt.renderer)
r += mdcmt.renderer.renderPropertySig(p, false, withKind);
else
r += Renderer.tdiv("signature", "function " + p.getName()) // we shouldn't really get here
r += "<div class='nopara'>" + mdcmt.formatTextNoLinks(p.getDescription()) + "</div>";
r += "</div></a>";
return r;
}
public isBetterThan(other:HelpTopic)
{
var d = this.hashTags().length - other.hashTags().length;
if (d != 0) return d < 0;
d = this.json.priority - other.json.priority;
if (d != 0) return d < 0;
return Util.stringCompare(this.id, other.id) < 0;
}
static addMany(scripts:any, topicsJson:HelpTopicJson[], templates:any[])
{
HelpTopic.shippedScripts = scripts;
var sc = (<any>TDev).ScriptCache;
if (sc) sc.shippedScripts = scripts;
HelpTopic.scriptTemplates = templates;
topicsJson.forEach((d) => {
HelpTopic._topics.push(new HelpTopic(d))
})
}
public updateKey()
{
var j = this.fromJson
if (j)
return j.rootid + ":" + j.userid + ":" + j.name
return this.json.rootid + ":jeiv:" + this.json.name
}
private allHashTags: string;
private hashTagsCache:string[];
public hashTags()
{
if (!this.hashTagsCache) {
var r = this.hashTagsCache = [];
var ht = "";
this.json.description = this.json.description.replace(/(#\w+)/g, (m, h) => { r.push(m); ht += " " + m; return "" })
this.allHashTags = ht;
}
return this.hashTagsCache;
}
public translations(): StringMap<string> {
var m = /\{translations:([^\}]+)\}/i.exec(this.json.text);
if (!m) return undefined;
var res = MdComments.findArtStringResource(this.app, m[1]);
if (!res) return undefined;
var r: StringMap<string> = {};
res.split('\n')
.map(p => p.split('='))
.forEach(parts => r[parts[0].toLowerCase()] = parts[1]);
return r;
}
public templateHashTags() : string[] {
var m = /\{sthashtags:([^\}]+)\}/i.exec(this.json.text);
if (m) return m[1].split(',');
else return [];
}
public templateEditorMode(): string {
var m = /\{steditormode:([^\}]+)\}/i.exec(this.json.text);
if (m) return m[1].trim().toLowerCase();
return "";
}
public eventHubsTracking(): { namespace: string; hub: string; token: string; } {
var m = /\{steventhubstracking:([^\:]+):([^\:]+):([^\}]+)\}/i.exec(this.json.text);
if (m) return { namespace: m[1], hub: m[2], token: m[3] };
return undefined;
}
public pixelTrackingUrl(): string {
var m = /\{stpixeltracking:([^\}]+)\}/i.exec(this.json.text);
if (m) return m[1];
return "";
}
public nextTutorials(): string[] {
var m = /\{stnexttutorials:([^\}]+)\}/i.exec(this.json.text);
if (m) return m[1].split(',');
return [];
}
public moreTutorials(): string {
var m = /\{stmoretutorials:([^\}]+)\}/i.exec(this.json.text);
if (m) return m[1];
return undefined;
}
private translateAsync(to: string): Promise { // of HelpTopicInfoJson
if (this.translatedTopic) return Promise.as(this.translatedTopic);
if (!to || /^en/i.test(to) || Cloud.isOffline()) return Promise.as(undefined);
tick(Ticks.translateDocTopic, to);
return this.translateToScriptAsync(to, this.json.id);
}
private translateToScriptAsync(to: string, tutorialId: string): Promise { // translated topic
// unpublished tutorial
if (!tutorialId ||
// cloud config not set
!Cloud.config.translateCdnUrl || !Cloud.config.translateApiUrl)
return Promise.as(this.translatedTopic = <HelpTopicInfoJson>{ body: undefined });
return ProgressOverlay.lockAndShowAsync(lf("translating topic...") + (dbg ? tutorialId : ""))
.then(() => {
var blobUrl = HTML.proxyResource(Cloud.config.translateCdnUrl + "/docs/" + to + "/" + tutorialId);
return Util.httpGetJsonAsync(blobUrl).then((blob) => {
this.translatedTopic = blob;
return this.translatedTopic;
}, e => {
// requestion translation
Util.log('requesting topic translation');
var url = Cloud.config.translateApiUrl + '/translate_doc?scriptId=' + tutorialId + '&to=' + to;
return Util.httpGetJsonAsync(url).then((js) => {
this.translatedTopic = js.info;
return this.translatedTopic;
}, e => {
Util.log('tutorial topic failed, ' + e);
return this.translatedTopic = <HelpTopicInfoJson>{ body : undefined };
});
});
}).then(() => ProgressOverlay.hide(), e => () => ProgressOverlay.hide());
}
public forSearch()
{
if (!this.searchCache) {
var j = this.json;
var c = j.name + " " + j.description;
if (this.apiProperty) {
c += " " + this.apiProperty.parentKind.toString();
this.apiProperty.getParameters().forEach((p) => {
c += " " + p.getName() + " " + p.getKind().toString();
})
c += " " + this.apiProperty.getResult().getKind().toString();
c += " " + this.apiProperty.getDescription();
} else if (this.apiKind) {
c += " " + this.apiKind.toString();
c += " " + this.apiKind.getDescription();
}
if (j.text) c += " " + j.text;
this.searchCache = c.toLowerCase();
}
return this.searchCache;
}
public reloadAppAsync()
{
var j = this.json;
var loadScript = (id) => {
if (id == "") {
if (j.text == null && j.id)
return HelpTopic.getScriptAsync(j.id).then((text) => {
j.text = text;
return text;
})
else
return Promise.as(j.text);
} else {
return HelpTopic.getScriptAsync(id);
}
}
return TDev.AST.loadScriptAsync(loadScript).then((tcRes:AST.LoadScriptResult) => {
var s = Script;
setGlobalScript(tcRes.prevScript);
return s;
})
}
public initAsync()
{
if (!this.json.text && !this.json.id)
return Promise.as(this.app);
if (this.initPromise) return this.initPromise;
this.initPromise = this.reloadAppAsync()
.then(s => { this.app = s; return s });
return this.initPromise
}
public isApiHelp()
{
return !!this.apiProperty
}
private renderCore(mdcmt : MdComments) : string
{
if (!mdcmt) {
var rend = new Renderer();
rend.stringLimit = 90;
mdcmt = new MdComments(rend, null);
}
var ch = ""
if (this.apiProperty) {
ch += "<div class='md-api-header md-tutorial'>" +
(new Renderer()).renderPropertySig(this.apiProperty, true) +
"<div class='md-prop-desc'>" +
mdcmt.formatText(this.apiProperty.getDescription()) +
"</div>" +
"</div>";
var cap = this.apiProperty.getCapability();
ch += "<div class='md-tutorial'>" +
"<ul>" +
(cap == PlatformCapability.None ? "" :
"<li>" + lf("<strong>required platform:</strong>") + " " + Util.htmlEscape(AST.App.capabilityName(cap)) +
" <a title='" + lf("read more about platforms") + "' href='/help/platforms'>" + lf("Learn more...") + "</a></li>") +
(!this.apiProperty.isBeta() ? "" :
"<li>" + lf("<strong>feature in beta testing:</strong> the syntax and semantics is subject to change") +
" <a title='" + lf("read more about the beta") + "' href='#topic:beta'>" + lf("Learn more...") + "</a></li>") +
(this.apiProperty.isImplementedAnywhere() ? "" :
"<li><strong>" + lf("API not implemented") + "</strong>, sorry " +
"<a title='" + lf("read more about unimplemented features") + "' href='#topic:notImplemented'>" + lf("Learn more...") + "</a></li>") +
"</ul>" +
"</div>";
if (mdcmt.useExternalLinks)
ch = ch.replace(/#topic(:|%3a)/g, Cloud.config.rootUrl + Cloud.config.topicPath);
}
if (this.app) {
var acts:AST.Action[] = <AST.Action[]> this.app.orderedThings().filter((a) => a instanceof AST.Action);
var noTutorial = acts.some(a => a.getName() == "this is no tutorial")
if (this.apiProperty instanceof AST.LibraryRefAction) {
acts = acts.filter(a => a.getName() == "docs " + this.apiProperty.getName())
} else {
if (this.isTutorial())
acts = acts.filter(a => /^main$/.test(a.getName()));
else {
acts = acts.filter((a) => a.isNormalAction() && /^example/.test(a.getName()))
if (acts.length == 0 && this.app.mainAction()) acts = [this.app.mainAction()];
}
}
acts.forEach((a) => {
ch += mdcmt.extract(a);
})
var tutorialSteps = noTutorial ? [] : AST.Step.splitActions(this.app);
if (tutorialSteps.length > 0 && mdcmt.forWeb)
ch = ch.replace(/<\/div>$/, "<hr class='md-section' data-name='startTutorial' data-arguments='' /></div>")
if (mdcmt.print) {
if (tutorialSteps.length > 0) {
tutorialSteps.forEach((s) => {
if (s.printOut) {
ch += mdcmt.mkDeclSnippet(s.printOut, false, true, "tutorial-step", 1);
}
})
var finalAct = this.app.actions().filter(a => a.getName() == "final")[0];
if (finalAct)
ch += mdcmt.extract(finalAct)
}
}
}
if (this.apiKind && !this.apiProperty) {
var r = this.getSubTopics().map((st) => st.renderLink(mdcmt))
ch += "<div class='md-tutorial'>" + r.join('') + "</div>";
}
if (this.id == "api") {
var r: string[] = [];
var kindTopics = HelpTopic.getAll().filter(st => st.apiKind && !st.apiProperty);
kindTopics.sort((a, b) => Util.stringCompare(a.id, b.id))
r.push('<h3>' + lf("services") + '</h3>');
kindTopics.filter(st => !st.apiKind.isData && !st.apiKind.isObsolete).forEach((st) => HelpTopic.renderKindDecl(r,st, mdcmt));
r.push('<h3>' + lf("types") + '</h3>');
kindTopics.filter(st => st.apiKind.isData && !st.apiKind.isAction && !st.apiKind.isObsolete).forEach((st) => HelpTopic.renderKindDecl(r, st, mdcmt));
r.push('<h3>' + lf("function types") + '</h3>');
kindTopics.filter(st => st.apiKind.isData && st.apiKind.isAction && !st.apiKind.isObsolete).forEach((st) => HelpTopic.renderKindDecl(r, st, mdcmt));
ch += "<div class='md-tutorial'>" + r.join('') + "</div>";
}
return ch;
}
public isPropertyHelp() { return !!this.apiProperty; }
public getSubTopics():HelpTopic[]
{
var names = Object.keys(this.subTopics);
names.sort(Util.stringCompare);
return names.map((k) => this.subTopics[k])
}
private renderTranslated() {
if (this.translatedTopic && this.translatedTopic.body) {
var ch = "<div class='md-tutorial' dir='auto'>" +
this.translatedTopic.body.join('\n') +
"</div>";
return ch;
}
return undefined;
}
public renderAsync(mdcmt : MdComments = null) : Promise // string
{
return this.initAsync().then(() => {
var prevScript = Script;
try {
setGlobalScript(this.app);
return this.renderCore(mdcmt);
} finally {
setGlobalScript(prevScript);
}
})
}
public docInfoAsync() : Promise // of HelpTopicInfoJson
{
var md = new TDev.MdComments(new TDev.CopyRenderer());
md.useSVG = false;
md.showCopy = false;
// md.useExternalLinks = true;
var ht = this
return this.renderAsync(md)
.then(text => {
var r = <HelpTopicInfoJson>{
title: "<h1>" + TDev.Util.htmlEscape(ht.json.name) + "</h1>",
description: ht.isApiHelp() ? "" : "<p>" + TDev.Util.htmlEscape(ht.json.description) + "</p>",
body: MdComments.splitDivs(text),
}
var translations = ht.translations();
if (translations) r.translations = translations;
return r;
})
}
public renderHeader()
{
var r = div(null);
this.initAsync().done(() => {
var appName = this.app.getName().replace(/ tutorial$/, "");
// remove any text before ':'
var i = appName.indexOf(':');
if (i > 0) appName = appName.substr(i+1);
else appName = lf("tutorial: ") + appName;
r.setChildren(appName)
})
return r;
}
public render(whenDone:(e:HTMLElement)=>void)
{
var d = div(null);
d.style.marginRight = "0.2em";
this.translateAsync(Util.getTranslationLanguage())
.then(() => this.renderAsync())
.done((text) => {
var translatedDocs = this.renderTranslated();
if (!translatedDocs)
Browser.setInnerHTML(d, text);
else if (this.translatedTopic && this.translatedTopic.manual) {
Browser.setInnerHTML(d, translatedDocs);
} else {
var elementDiv = <HTMLDivElement>div('');
Browser.setInnerHTML(elementDiv, text);
var trElementDiv = <HTMLDivElement>div('');
Browser.setInnerHTML(trElementDiv, translatedDocs);
var trNotice = div('translate-notice', lf("Translations by Microsoft® Translator, tap to see original..."))
.withClick(() => {
trElementDiv.style.display = 'none';
elementDiv.style.display = 'block';
Util.seeTranslatedText(false);
});
trElementDiv.insertBefore(trNotice, trElementDiv.firstElementChild);
var elNotice = div('translate-notice', lf("tap to translate with Microsoft® Translator..."))
.withClick(() => {
elementDiv.style.display = 'none';
trElementDiv.style.display = 'block';
Util.seeTranslatedText(true);
})
elementDiv.insertBefore(elNotice, elementDiv.firstElementChild);
if (Util.seeTranslatedText())
elementDiv.style.display = 'none';
else
trElementDiv.style.display = 'none';
d.setChildren([elementDiv, trElementDiv]);
}
HTML.fixWp8Links(d);
whenDone(d);
})
return d;
}
static printManyAsync(topics:HelpTopic[])
{
return Promise.join(topics.map(t => t.printedAsync())).then(arr =>
HelpTopic.printText(arr.join("<div style='page-break-after:always'></div>\n"), "Help"))
}
public printedAsync(newsletter = false)
{
var r = new CopyRenderer();
var md = new MdComments(r);
md.scriptid = this.json.id;
md.useSVG = false;
md.useExternalLinks = true;
md.showCopy = false;
md.print = true;
return this.renderAsync(md).then((text) => {
if (newsletter)
return CopyRenderer.inlineStyles(text);
else
return (
"<h1>" + Util.htmlEscape(this.json.name) + "</h1>" +
"<p>" + Util.htmlEscape(this.json.description) + "</p>" +
text)
})
}
static printText(text:string, title:string)
{
try {
var w = window.open("about:blank", "tdTopic" + Util.guidGen());
w.document.write("<!DOCTYPE html><html><head>" + CopyRenderer.css
+ "<title>" + Util.htmlEscape(title) + "</title>"
+ "<meta name='microsoft' content='notranslateclasses stmt keyword'/>"
+ "</head><body onload='try { window.print(); } catch(ex) {}'>"
+ (Cloud.config.printHeaderHtml || ("<div><img src='" + HTML.proxyResource("https://az31353.vo.msecnd.net/c04/uxoj.png") + "' alt='" + lf("TouchDevelop by Microsoft Research") + "'></div>"))
+ text
+ "</body></html>");
w.document.close();
} catch(e) {
ModalDialog.info(":( can't print from here", "Your browser might have blocked the print page or try to print from another device...");
}
}
public print()
{
this.printedAsync().done(text => HelpTopic.printText(text, this.json.name))
}
static renderKindDecl(r : string[], st : HelpTopic, mdcmt:MdComments) {
var j = st.json;
var n = j.name;
if (!st.apiKind.isData) n = n.toLowerCase();
r.push("<div class='api-kind'><a href='" + mdcmt.topicLink(st.id) + "'>");
r.push("<div class='api-kind-inner'>");
// without SVG we don't do any icons at the moment; in future see below
if (mdcmt.useSVG)
r.push("<span class='api-icon' style='background:" + j.iconbackground + "'>" +
(mdcmt.useSVG ? SVG.getIconSVGCore(j.icon + ",white") :
"<img src='/replaceicons/" + j.icon + ".png' alt='" + j.icon + "'>") + "</span>");
r.push("<div class='api-names'><div class='api-name'>" + Util.htmlEscape(n) + "</div>");
r.push("<div class='api-desc'>" + Util.htmlEscape(j.description) + "</div>");
r.push("</div></div></a></div>");
}
static topicCache:any;
static topicByScriptId:any;
static findById(id:string):HelpTopic
{
// make sure things are initialized
HelpTopic.getAll();
if (id)
id = id.replace(/^t:/, "")
id = MdComments.shrink(id)
if (HelpTopic.topicCache.hasOwnProperty(id))
return HelpTopic.topicCache[id];
return null;
}
static testSplit(id:string)
{
var h = HelpTopic.findById(id)
if (h)
h.renderAsync().done(text => {
console.log(MdComments.splitDivs(text))
})
}
static findByScriptId(id:string):HelpTopic
{
if (!HelpTopic.topicByScriptId) HelpTopic.findById("anything");
if (HelpTopic.topicByScriptId.hasOwnProperty(id))
return HelpTopic.topicByScriptId[id]
}
}
TDev.api.addHelpTopics = HelpTopic.addMany;
}