Skip to content

Instantly share code, notes, and snippets.

@p3g4asus
Last active February 11, 2022 08:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save p3g4asus/f7f00052a928686553257a655b657dad to your computer and use it in GitHub Desktop.
Save p3g4asus/f7f00052a928686553257a655b657dad to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name ZWO from description
// @namespace https://github.com/p3g4asus
// @version 2.0
// @description Show zwo xml file in whatsonzwift.com
// @author p3g4asus
// @match https://whatsonzwift.com/workouts/*
// @icon https://www.google.com/s2/favicons?domain=whatsonzwift.com
// @homepageURL https://gist.github.com/p3g4asus/f7f00052a928686553257a655b657dad
// @updateURL https://gist.github.com/p3g4asus/f7f00052a928686553257a655b657dad/raw/show-zwo.user.js
// @downloadURL https://gist.github.com/p3g4asus/f7f00052a928686553257a655b657dad/raw/show-zwo.user.js
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
//https://stackoverflow.com/a/51464792
//new XMLSerializer().serializeToString(xmlObject.documentElement);
function escapeXml(unsafe) {
return unsafe.replace(/[<>&'"]/g, function (c) {
switch (c) {
case '<': return '&lt;';
case '>': return '&gt;';
case '&': return '&amp;';
case '\'': return '&apos;';
case '"': return '&quot;';
}
});
}
function prettyPrintXml(xmlDoc) {
// return new XMLSerializer().serializeToString(xmlDoc);
var xsltDoc = new DOMParser().parseFromString([
// describes how we want to modify the XML - indent everything
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">',
' <xsl:strip-space elements="*"/>',
' <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes
' <xsl:value-of select="normalize-space(.)"/>',
' </xsl:template>',
' <xsl:template match="node()|@*">',
' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
' </xsl:template>',
' <xsl:output indent="yes"/>',
'</xsl:stylesheet>',
].join('\n'), 'application/xml');
var xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsltDoc);
var resultDoc = xsltProcessor.transformToDocument(xmlDoc);
var resultXml = new XMLSerializer().serializeToString(resultDoc);
return resultXml;
}
function parsePace(s) {
let pace = 2;
if (s=='5k') pace = 1;
else if (s == 'HM') pace = 3;
else if (s == 'MM') pace = 4;
else if (s == '1mi') pace = 0;
return pace;
}
function parseDuration(txt) {
let re;
let objout = {
dur: 60,
repeat: 0,
durationType: null,
txt: txt
};
let dur = 60;
let repeat = 0;
if ((txt.indexOf('min') > 0 || txt.indexOf('sec') > 0 || txt.indexOf('hr') > 0) && (re = /([0-9]+x\s+)?([0-9]+hr)?\s*([0-9]+min)?\s*([0-9]+sec)?\s+/.exec(txt))) {
let dd = 0;
objout.durationType = 'time';
for (let i = 1; i<re.length; i++) {
let trm1 = re[i];
if (!trm1) continue;
else if ((trm1 = trm1.trim()).endsWith('x')) {
objout.repeat = parseInt(re[i].substring(0, re[i].length - 1));
}
else if (trm1.endsWith('sec')) {
dd += parseInt(re[i].substring(0, re[i].length - 3));
}
else if (trm1.endsWith('min')) {
dd += parseInt(re[i].substring(0, re[i].length - 3)) * 60;
}
else if (trm1.endsWith('hr')) {
dd += parseInt(re[i].substring(0, re[i].length - 2)) * 3600;
}
}
if (dd) objout.dur = dd;
objout.txt = txt.substring(re[0].length);
}
else if (re = /(?:([0-9]+)x\s+)?([0-9]+)\s+m\s+/.exec(txt)) {
objout.durationType = 'distance';
if (re[1]) {
objout.repeat = parseInt(re[1]);
}
objout.dur = parseInt(re[2]);
objout.txt = txt.substring(re[0].length);
}
return objout;
}
let $sect = $('article.workout').nextAll('section').first();
let $divr = $('<div>').addClass('row');
let $div = $('<div>').addClass('one-whole').addClass('column');
let $ta = $('<textarea>').css('border','solid 1px').css('width','100%').css('height', 'auto').prop('readonly', true).attr('rows',10);
let $descp = $('div.overview').nextAll('p').first();
let $gli = $('h4.glyph-icon');
$div.prepend($ta);
$divr.prepend($div);
let $divr2 = $('<div>').addClass('row');
$div = $('<div>').addClass('one-whole').addClass('column');
let $down = $('<a>');
$div.prepend($down);
$divr2.prepend($div);
$sect.prepend($divr2);
$sect.prepend($divr);
let doc = document.implementation.createDocument("", "", null);
let work = doc.createElement("workout_file");
let elem = doc.createElement("author")
let totDur = 0;
let namew = null;
let meanPower = 0.0;
let sport = null;
let errorLines = 0;
elem.innerHTML = "whatsonzwift.com";
work.appendChild(elem);
elem = doc.createElement("name")
elem.innerHTML = escapeXml(namew = $gli.length?$gli.text().trim():'');
work.appendChild(elem);
elem = doc.createElement("description")
elem.innerHTML = escapeXml($descp.text());
work.appendChild(elem);
elem = doc.createElement("sportType")
elem.innerHTML = (sport = $gli.hasClass('flaticon-run')?'run':'bike');
work.appendChild(elem);
elem = doc.createElement("tags")
work.appendChild(elem);
doc.appendChild(work);
let workout = doc.createElement("workout");
let all = '';
let durationType = null;
let pace = null;
let $tbs = $('.workoutlist').children('.textbar');
let rexp = /(?:from\s+([0-9]+)\s+to\s+|@\s+||([0-9]+)%\s+Incline\s+@\s+)(?:([0-9]+)rpm,\s+)?(?:([0-9]+)%\s+(?:of\s+(5k|10k|HM|MM|1mi)\s+pace|FTP)|No Incline Walk|([0-9]+)% Incline Walk)/;
let idx_from = 1;
let idx_incline0 = 2;
let idx_rpm = 3;
let idx_to = 4;
let idx_pace = 5;
let idx_incline1 = 6;
$tbs.each(function(idx, el) {
let txt = $(el).text();
let o = parseDuration(txt);
if (o.durationType) {
let OffDuration = -1;
let OffPower = -1;
let o2;
let re, re2;
elem = null;
txt = o.txt;
if (re = rexp.exec(txt)) {
let ln = re[0].length;
if (txt.length > ln && txt.charAt(ln) == ',' && (o2 = parseDuration(txt.substring(ln + 1))) && o2.durationType && (re2 = rexp.exec(o2.txt))) {
OffPower = parseInt(re2[idx_to]);
OffDuration = o2.dur;
}
if (re[idx_from]) {
if (idx == 0) {
elem = doc.createElement("Warmup");
}
else if (idx == $tbs.length - 1) {
elem = doc.createElement("Cooldown");
}
else {
elem = doc.createElement("Ramp");
}
elem.setAttribute('Duration', o.dur);
totDur += o.dur;
let a1, a2;
elem.setAttribute('PowerLow', a1 = parseInt(re[idx_from]) / 100.0);
elem.setAttribute('PowerHigh', a2 = parseInt(re[idx_to]) / 100.0);
meanPower += (a1 + a2) / 2 * o.dur;
}
else if (OffPower >= 0 && OffDuration >= 0) {
elem = doc.createElement("IntervalsT");
if (o.repeat) elem.setAttribute('Repeat', o.repeat);
let a1, a2;
elem.setAttribute('OnDuration', o.dur);
elem.setAttribute('OnPower', a1 = parseInt(re[idx_to]) / 100.0);
elem.setAttribute('OffDuration', OffDuration);
elem.setAttribute('OffPower', a2 = OffPower / 100.0);
totDur += o.dur + OffDuration;
meanPower += a1 * o.dur + a2 * OffDuration;
}
else if (re[idx_incline1]) {
elem = doc.createElement("FreeRide");
elem.setAttribute('Duration', o.dur);
elem.setAttribute('Incline', parseInt(re[idx_incline1]) / 100.0);
totDur += o.dur;
}
else {
let a1;
elem = doc.createElement("SteadyState");
if (o.repeat) elem.setAttribute('Repeat', o.repeat);
elem.setAttribute('Duration', o.dur);
elem.setAttribute('Power', a1 = re[idx_to]?parseInt(re[idx_to]) / 100.0:0.5);
totDur += o.dur;
meanPower += a1 * o.dur;
}
if (re[idx_incline0]) elem.setAttribute('Incline', parseInt(re[idx_incline0]) / 100.0);
if (re[idx_rpm]) elem.setAttribute('RPM', parseInt(re[idx_rpm]));
if (re[idx_pace]) pace = parsePace(re[idx_pace]);
if (pace !== null) elem.setAttribute('pace', pace);
}
else if ((re = /\s*(:?free ride|free run)(:?\s+@\s*([0-9]+)rpm)?/.exec(txt))) {
elem = doc.createElement("FreeRide");
elem.setAttribute('Duration', o.dur);
if (re[1]) elem.setAttribute('RPM', parseInt(re[1]));
totDur += o.dur;
}
else if ((re = /\s*(?:([0-9]+(?:\.[0-9]+)?)\/([0-9]+)\s+)?(Jog At Easy Pace|Med Pace|Run|Warmup|Relaxed Cool Down|Cooldown|Jog|Walk|Fast Pace)/.exec(txt))) {
let a1 = 0.5;
elem = doc.createElement("SteadyState");
if (o.repeat) elem.setAttribute('Repeat', o.repeat);
elem.setAttribute('Duration', o.dur);
if (re[1] && re[2])
a1 = parseFloat(re[1]) / parseInt(re[2]);
else if (re[3] == "Jog" || re[3] == "Jog At Easy Pace")
a1 = 0.65;
else if (re[3] == "Med Pace" || re[3] == "Relaxed Cool Down")
a1 = 0.8;
else if (re[3] == "Fast Pace")
a1 = 0.95;
elem.setAttribute('Power', a1);
totDur += o.dur;
meanPower += a1 * o.dur;
}
else if (txt == "Walk Easy Pace") {
if (idx == 0) {
elem = doc.createElement("Warmup");
}
else if (idx == $tbs.length - 1) {
elem = doc.createElement("Cooldown");
}
else {
elem = doc.createElement("Ramp");
}
elem.setAttribute('Duration', o.dur);
totDur += o.dur;
let a1 = 0.3, a2 = 0.5;
elem.setAttribute('PowerLow', a1);
elem.setAttribute('PowerHigh', a2);
meanPower += (a1 + a2) / 2 * o.dur;
}
if (elem) workout.appendChild(elem);
else errorLines++;
if (durationType === null) {
elem = doc.createElement("durationType")
elem.innerHTML = o.durationType;
work.appendChild(elem);
durationType = o.durationType;
}
}
all += txt + '\n';
});
work.appendChild(workout);
let pp = prettyPrintXml(doc);
$ta.text(pp);
let blob = new Blob([pp], {type: 'text/xml'});
let fn = $gli.text().trim() + '.zwo';
$down.attr('download',fn).attr('href', URL.createObjectURL(blob)).text('Download ' + fn);
// $ta.text(prettyPrintXml(doc) + '\n\n\n' + all);
let url = new URL(location.href).pathname;
let idxend = url.lastIndexOf('/workouts/');
if (idxend >= 0) {
idxend += '/workouts/'.length;
namew = url.substring(idxend).replace('/', '_');
console.log(JSON.stringify({
durationType: durationType,
name: namew,
pace: pace,
duration: totDur,
power: meanPower,
sport: sport,
errors: errorLines,
xml: pp
}));
}
else
console.error("Cannot parse any workout here");
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment