diff --git a/docs/_static/js/readthedocs-doc-diff.js b/docs/_static/js/readthedocs-doc-diff.js index 0fceba340d4..63f7d69c1e5 100644 --- a/docs/_static/js/readthedocs-doc-diff.js +++ b/docs/_static/js/readthedocs-doc-diff.js @@ -1 +1 @@ -(()=>{var e={416:(e,t,n)=>{"use strict";var r,i=n(224),o=n(52),a={addedClass:"doc-diff-added",modifiedClass:"doc-diff-modified",removedClass:"doc-diff-removed",skipModified:!0};function s(){var e={base_host:"",base_version:"latest",base_language:"en",base_page:"index.html",root_selector:"div.document[role='main']",inject_styles:!0};return new Promise((function(t,n){var r=document.querySelector("script#READTHEDOCS_DATA");if(r)try{var i=JSON.parse(r.innerText);e.base_host="https://"+i.project+".readthedocs.io",e.base_language=i.language,e.base_page=i.page+".html"}catch(e){console.debug("Error parsing configuration data",e)}var o=document.querySelector("script#doc-diff-config");if(o)try{var a=JSON.parse(o.innerText);Object.assign(e,a)}catch(e){console.debug("Error parsing configuration data",e)}return void 0===e.base_url&&(e.base_url=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"en",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"latest",r=arguments.length>3?arguments[3]:void 0,i=[];e&&(e="https://cors.writethedocs.workers.dev/corsproxy/?apiurl="+e);i=null==n&&null==t?[e,r]:null==t?[e,n,r]:[e,t,n,r];return i.join("/")}(e.base_host,e.base_language,e.base_version,e.base_page)),t(e)}))}e=n.hmd(e),n.c[n.s]!==e&&(r=new Promise((function(e){if("interactive"===document.readyState||"complete"===document.readyState)return e();document.addEventListener("DOMContentLoaded",(function(){e()}),{capture:!0,once:!0,passive:!0})})),new Promise((function(e){r.then((function(){return s()})).then((function(e){return function(e){return new Promise((function(t,n){fetch(e.base_url).then((function(e){return e.text()})).then((function(r){var s=(new DOMParser).parseFromString(r,"text/html").documentElement.querySelector(e.root_selector),f=document.querySelector(e.root_selector);null!=s&&null!=f||n(new Error("Element not found in both documents.")),e.inject_styles&&(document.adoptedStyleSheets=[o.Z]);var l=(0,i.visualDomDiff)(s,f,a);f.replaceWith(l.firstElementChild),t(!0)}))}))}(e)})).then((function(){e()})).catch((function(e){console.error(e.message)}))})))},52:(e,t,n)=>{"use strict";n.d(t,{Z:()=>f});var r=n(81),i=n.n(r),o=n(645),a=n.n(o)()(i());a.push([e.id,".doc-diff-added {\n background-color: rgb(171, 242, 188);\n text-decoration: none;\n}\n\n.doc-diff-modified {\n}\n\n.doc-diff-removed {\n background-color: rgba(255, 129, 130, 0.4);\n text-decoration: none;\n}\n",""]);var s=new CSSStyleSheet;s.replaceSync(a.toString());const f=s},645:e=>{"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n="",r=void 0!==t[5];return t[4]&&(n+="@supports (".concat(t[4],") {")),t[2]&&(n+="@media ".concat(t[2]," {")),r&&(n+="@layer".concat(t[5].length>0?" ".concat(t[5]):""," {")),n+=e(t),r&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n})).join("")},t.i=function(e,n,r,i,o){"string"==typeof e&&(e=[[null,e,void 0]]);var a={};if(r)for(var s=0;s0?" ".concat(h[5]):""," {").concat(h[1],"}")),h[5]=o),n&&(h[2]?(h[1]="@media ".concat(h[2]," {").concat(h[1],"}"),h[2]=n):h[2]=n),i&&(h[4]?(h[1]="@supports (".concat(h[4],") {").concat(h[1],"}"),h[4]=i):h[4]="".concat(i)),t.push(h))}},t}},81:e=>{"use strict";e.exports=function(e){return e[1]}},27:e=>{var t=function(){this.Diff_Timeout=1,this.Diff_EditCost=4,this.Match_Threshold=.5,this.Match_Distance=1e3,this.Patch_DeleteThreshold=.5,this.Patch_Margin=4,this.Match_MaxBits=32},n=-1;t.Diff=function(e,t){return[e,t]},t.prototype.diff_main=function(e,n,r,i){void 0===i&&(i=this.Diff_Timeout<=0?Number.MAX_VALUE:(new Date).getTime()+1e3*this.Diff_Timeout);var o=i;if(null==e||null==n)throw new Error("Null input. (diff_main)");if(e==n)return e?[new t.Diff(0,e)]:[];void 0===r&&(r=!0);var a=r,s=this.diff_commonPrefix(e,n),f=e.substring(0,s);e=e.substring(s),n=n.substring(s),s=this.diff_commonSuffix(e,n);var l=e.substring(e.length-s);e=e.substring(0,e.length-s),n=n.substring(0,n.length-s);var h=this.diff_compute_(e,n,a,o);return f&&h.unshift(new t.Diff(0,f)),l&&h.push(new t.Diff(0,l)),this.diff_cleanupMerge(h),h},t.prototype.diff_compute_=function(e,r,i,o){var a;if(!e)return[new t.Diff(1,r)];if(!r)return[new t.Diff(n,e)];var s=e.length>r.length?e:r,f=e.length>r.length?r:e,l=s.indexOf(f);if(-1!=l)return a=[new t.Diff(1,s.substring(0,l)),new t.Diff(0,f),new t.Diff(1,s.substring(l+f.length))],e.length>r.length&&(a[0][0]=a[2][0]=n),a;if(1==f.length)return[new t.Diff(n,e),new t.Diff(1,r)];var h=this.diff_halfMatch_(e,r);if(h){var u=h[0],c=h[1],d=h[2],g=h[3],p=h[4],v=this.diff_main(u,d,i,o),_=this.diff_main(c,g,i,o);return v.concat([new t.Diff(0,p)],_)}return i&&e.length>100&&r.length>100?this.diff_lineMode_(e,r,o):this.diff_bisect_(e,r,o)},t.prototype.diff_lineMode_=function(e,r,i){var o=this.diff_linesToChars_(e,r);e=o.chars1,r=o.chars2;var a=o.lineArray,s=this.diff_main(e,r,!1,i);this.diff_charsToLines_(s,a),this.diff_cleanupSemantic(s),s.push(new t.Diff(0,""));for(var f=0,l=0,h=0,u="",c="";f=1&&h>=1){s.splice(f-l-h,l+h),f=f-l-h;for(var d=this.diff_main(u,c,!1,i),g=d.length-1;g>=0;g--)s.splice(f,0,d[g]);f+=d.length}h=0,l=0,u="",c=""}f++}return s.pop(),s},t.prototype.diff_bisect_=function(e,r,i){for(var o=e.length,a=r.length,s=Math.ceil((o+a)/2),f=s,l=2*s,h=new Array(l),u=new Array(l),c=0;ci);m++){for(var y=-m+p;y<=m-v;y+=2){for(var w=f+y,x=(T=y==-m||y!=m&&h[w-1]o)v+=2;else if(x>a)p+=2;else if(g){if((M=f+d-y)>=0&&M=(N=o-u[M]))return this.diff_bisectSplit_(e,r,T,x,i)}}for(var D=-m+_;D<=m-b;D+=2){for(var N,M=f+D,E=(N=D==-m||D!=m&&u[M-1]o)b+=2;else if(E>a)_+=2;else if(!g){if((w=f+d-D)>=0&&w=(N=o-N))return this.diff_bisectSplit_(e,r,T,x,i)}}}}return[new t.Diff(n,e),new t.Diff(1,r)]},t.prototype.diff_bisectSplit_=function(e,t,n,r,i){var o=e.substring(0,n),a=t.substring(0,r),s=e.substring(n),f=t.substring(r),l=this.diff_main(o,a,!1,i),h=this.diff_main(s,f,!1,i);return l.concat(h)},t.prototype.diff_linesToChars_=function(e,t){var n=[],r={};function i(e){for(var t="",i=0,a=-1,s=n.length;ar?e=e.substring(n-r):nt.length?e:t,r=e.length>t.length?t:e;if(n.length<4||2*r.length=e.length?[r,o,a,s,h]:null}var a,s,f,l,h,u=o(n,r,Math.ceil(n.length/4)),c=o(n,r,Math.ceil(n.length/2));return u||c?(a=c?u&&u[4].length>c[4].length?u:c:u,e.length>t.length?(s=a[0],f=a[1],l=a[2],h=a[3]):(l=a[0],h=a[1],s=a[2],f=a[3]),[s,f,l,h,a[4]]):null},t.prototype.diff_cleanupSemantic=function(e){for(var r=!1,i=[],o=0,a=null,s=0,f=0,l=0,h=0,u=0;s0?i[o-1]:-1,f=0,l=0,h=0,u=0,a=null,r=!0)),s++;for(r&&this.diff_cleanupMerge(e),this.diff_cleanupSemanticLossless(e),s=1;s=p?(g>=c.length/2||g>=d.length/2)&&(e.splice(s,0,new t.Diff(0,d.substring(0,g))),e[s-1][1]=c.substring(0,c.length-g),e[s+1][1]=d.substring(g),s++):(p>=c.length/2||p>=d.length/2)&&(e.splice(s,0,new t.Diff(0,c.substring(0,p))),e[s-1][0]=1,e[s-1][1]=d.substring(0,d.length-p),e[s+1][0]=n,e[s+1][1]=c.substring(p),s++),s++}s++}},t.prototype.diff_cleanupSemanticLossless=function(e){function n(e,n){if(!e||!n)return 6;var r=e.charAt(e.length-1),i=n.charAt(0),o=r.match(t.nonAlphaNumericRegex_),a=i.match(t.nonAlphaNumericRegex_),s=o&&r.match(t.whitespaceRegex_),f=a&&i.match(t.whitespaceRegex_),l=s&&r.match(t.linebreakRegex_),h=f&&i.match(t.linebreakRegex_),u=l&&e.match(t.blanklineEndRegex_),c=h&&n.match(t.blanklineStartRegex_);return u||c?5:l||h?4:o&&!s&&f?3:s||f?2:o||a?1:0}for(var r=1;r=c&&(c=d,l=i,h=o,u=a)}e[r-1][1]!=l&&(l?e[r-1][1]=l:(e.splice(r-1,1),r--),e[r][1]=h,u?e[r+1][1]=u:(e.splice(r+1,1),r--))}r++}},t.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/,t.whitespaceRegex_=/\s/,t.linebreakRegex_=/[\r\n]/,t.blanklineEndRegex_=/\n\r?\n$/,t.blanklineStartRegex_=/^\r?\n\r?\n/,t.prototype.diff_cleanupEfficiency=function(e){for(var r=!1,i=[],o=0,a=null,s=0,f=!1,l=!1,h=!1,u=!1;s0?i[o-1]:-1,h=u=!1),r=!0)),s++;r&&this.diff_cleanupMerge(e)},t.prototype.diff_cleanupMerge=function(e){e.push(new t.Diff(0,""));for(var r,i=0,o=0,a=0,s="",f="";i1?(0!==o&&0!==a&&(0!==(r=this.diff_commonPrefix(f,s))&&(i-o-a>0&&0==e[i-o-a-1][0]?e[i-o-a-1][1]+=f.substring(0,r):(e.splice(0,0,new t.Diff(0,f.substring(0,r))),i++),f=f.substring(r),s=s.substring(r)),0!==(r=this.diff_commonSuffix(f,s))&&(e[i][1]=f.substring(f.length-r)+e[i][1],f=f.substring(0,f.length-r),s=s.substring(0,s.length-r))),i-=o+a,e.splice(i,o+a),s.length&&(e.splice(i,0,new t.Diff(n,s)),i++),f.length&&(e.splice(i,0,new t.Diff(1,f)),i++),i++):0!==i&&0==e[i-1][0]?(e[i-1][1]+=e[i][1],e.splice(i,1)):i++,a=0,o=0,s="",f=""}""===e[e.length-1][1]&&e.pop();var l=!1;for(i=1;it));r++)a=i,s=o;return e.length!=r&&e[r][0]===n?s:s+(t-a)},t.prototype.diff_prettyHtml=function(e){for(var t=[],r=/&/g,i=//g,a=/\n/g,s=0;s");switch(f){case 1:t[s]=''+l+"";break;case n:t[s]=''+l+"";break;case 0:t[s]=""+l+""}}return t.join("")},t.prototype.diff_text1=function(e){for(var t=[],n=0;nthis.Match_MaxBits)throw new Error("Pattern too long for this browser.");var r=this.match_alphabet_(t),i=this;function o(e,r){var o=e/t.length,a=Math.abs(n-r);return i.Match_Distance?o+a/i.Match_Distance:a?1:o}var a=this.Match_Threshold,s=e.indexOf(t,n);-1!=s&&(a=Math.min(o(0,s),a),-1!=(s=e.lastIndexOf(t,n+t.length))&&(a=Math.min(o(0,s),a)));var f,l,h=1<=g;_--){var b=r[e.charAt(_-1)];if(v[_]=0===d?(v[_+1]<<1|1)&b:(v[_+1]<<1|1)&b|(u[_+1]|u[_])<<1|1|u[_+1],v[_]&h){var m=o(d,_-1);if(m<=a){if(a=m,!((s=_-1)>n))break;g=Math.max(1,2*n-s)}}}if(o(d+1,n)>a)break;u=v}return s},t.prototype.match_alphabet_=function(e){for(var t={},n=0;n2&&(this.diff_cleanupSemantic(a),this.diff_cleanupEfficiency(a));else if(e&&"object"==typeof e&&void 0===r&&void 0===i)a=e,o=this.diff_text1(a);else if("string"==typeof e&&r&&"object"==typeof r&&void 0===i)o=e,a=r;else{if("string"!=typeof e||"string"!=typeof r||!i||"object"!=typeof i)throw new Error("Unknown call format to patch_make.");o=e,a=i}if(0===a.length)return[];for(var s=[],f=new t.patch_obj,l=0,h=0,u=0,c=o,d=o,g=0;g=2*this.Patch_Margin&&l&&(this.patch_addContext_(f,c),s.push(f),f=new t.patch_obj,l=0,c=d,h=u)}1!==p&&(h+=v.length),p!==n&&(u+=v.length)}return l&&(this.patch_addContext_(f,c),s.push(f)),s},t.prototype.patch_deepCopy=function(e){for(var n=[],r=0;rthis.Match_MaxBits?-1!=(s=this.match_main(t,h.substring(0,this.Match_MaxBits),l))&&(-1==(u=this.match_main(t,h.substring(h.length-this.Match_MaxBits),l+h.length-this.Match_MaxBits))||s>=u)&&(s=-1):s=this.match_main(t,h,l),-1==s)o[a]=!1,i-=e[a].length2-e[a].length1;else if(o[a]=!0,i=s-l,h==(f=-1==u?t.substring(s,s+h.length):t.substring(s,u+this.Match_MaxBits)))t=t.substring(0,s)+this.diff_text2(e[a].diffs)+t.substring(s+h.length);else{var c=this.diff_main(h,f,!1);if(h.length>this.Match_MaxBits&&this.diff_levenshtein(c)/h.length>this.Patch_DeleteThreshold)o[a]=!1;else{this.diff_cleanupSemanticLossless(c);for(var d,g=0,p=0;pa[0][1].length){var s=n-a[0][1].length;a[0][1]=r.substring(a[0][1].length)+a[0][1],o.start1-=s,o.start2-=s,o.length1+=s,o.length2+=s}if(0==(a=(o=e[e.length-1]).diffs).length||0!=a[a.length-1][0])a.push(new t.Diff(0,r)),o.length1+=n,o.length2+=n;else if(n>a[a.length-1][1].length){s=n-a[a.length-1][1].length;a[a.length-1][1]+=r.substring(0,s),o.length1+=s,o.length2+=s}return r},t.prototype.patch_splitMax=function(e){for(var r=this.Match_MaxBits,i=0;i2*r?(l.length1+=c.length,a+=c.length,h=!1,l.diffs.push(new t.Diff(u,c)),o.diffs.shift()):(c=c.substring(0,r-l.length1-this.Patch_Margin),l.length1+=c.length,a+=c.length,0===u?(l.length2+=c.length,s+=c.length):h=!1,l.diffs.push(new t.Diff(u,c)),c==o.diffs[0][1]?o.diffs.shift():o.diffs[0][1]=o.diffs[0][1].substring(c.length))}f=(f=this.diff_text2(l.diffs)).substring(f.length-this.Patch_Margin);var d=this.diff_text1(o.diffs).substring(0,this.Patch_Margin);""!==d&&(l.length1+=d.length,l.length2+=d.length,0!==l.diffs.length&&0===l.diffs[l.diffs.length-1][0]?l.diffs[l.diffs.length-1][1]+=d:l.diffs.push(new t.Diff(0,d))),h||e.splice(++i,0,l)}}},t.prototype.patch_toText=function(e){for(var t=[],n=0;n{"use strict";n.r(t),n.d(t,{__assign:()=>o,__asyncDelegator:()=>w,__asyncGenerator:()=>y,__asyncValues:()=>x,__await:()=>m,__awaiter:()=>h,__classPrivateFieldGet:()=>T,__classPrivateFieldIn:()=>O,__classPrivateFieldSet:()=>S,__createBinding:()=>c,__decorate:()=>s,__exportStar:()=>d,__extends:()=>i,__generator:()=>u,__importDefault:()=>E,__importStar:()=>M,__makeTemplateObject:()=>D,__metadata:()=>l,__param:()=>f,__read:()=>p,__rest:()=>a,__spread:()=>v,__spreadArray:()=>b,__spreadArrays:()=>_,__values:()=>g});var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},r(e,t)};function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var o=function(){return o=Object.assign||function(e){for(var t,n=1,r=arguments.length;n=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a}function f(e,t){return function(n,r){t(n,r,e)}}function l(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}function h(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{f(r.next(e))}catch(e){o(e)}}function s(e){try{f(r.throw(e))}catch(e){o(e)}}function f(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}f((r=r.apply(e,t||[])).next())}))}function u(e,t){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function s(o){return function(s){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=a.trys,(i=i.length>0&&i[i.length-1])||6!==o[0]&&2!==o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function p(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||t-- >0)&&!(r=o.next()).done;)a.push(r.value)}catch(e){i={error:e}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return a}function v(){for(var e=[],t=0;t1||s(e,t)}))})}function s(e,t){try{(n=i[e](t)).value instanceof m?Promise.resolve(n.value.v).then(f,l):h(o[0][2],n)}catch(e){h(o[0][3],e)}var n}function f(e){s("next",e)}function l(e){s("throw",e)}function h(e,t){e(t),o.shift(),o.length&&s(o[0][0],o[0][1])}}function w(e){var t,n;return t={},r("next"),r("throw",(function(e){throw e})),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,i){t[r]=e[r]?function(t){return(n=!n)?{value:m(e[r](t)),done:"return"===r}:i?i(t):t}:i}}function x(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e=g(e),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise((function(r,i){(function(e,t,n,r){Promise.resolve(r).then((function(t){e({value:t,done:n})}),t)})(r,i,(t=e[n](t)).done,t.value)}))}}}function D(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e}var N=Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t};function M(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&c(t,e,n);return N(t,e),t}function E(e){return e&&e.__esModule?e:{default:e}}function T(e,t,n,r){if("a"===n&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!r:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?r:"a"===n?r.call(e):r?r.value:t.get(e)}function S(e,t,n,r,i){if("m"===r)throw new TypeError("Private method is not writable");if("a"===r&&!i)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===r?i.call(e,n):i?i.value=n:t.set(e,n),n}function O(e,t){if(null===t||"object"!=typeof t&&"function"!=typeof t)throw new TypeError("Cannot use 'in' operator on non-object");return"function"==typeof e?t===e:e.has(t)}},967:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(402),i=new Set;i.add("IMG"),i.add("VIDEO"),i.add("IFRAME"),i.add("OBJECT"),i.add("SVG");var o=new Set;o.add("BDO"),o.add("BDI"),o.add("Q"),o.add("CITE"),o.add("CODE"),o.add("DATA"),o.add("TIME"),o.add("VAR"),o.add("DFN"),o.add("ABBR"),o.add("STRONG"),o.add("EM"),o.add("BIG"),o.add("SMALL"),o.add("MARK"),o.add("SUB"),o.add("SUP"),o.add("SAMP"),o.add("KBD"),o.add("B"),o.add("I"),o.add("S"),o.add("U"),o.add("SPAN"),t.optionsToConfig=function(e){var t=void 0===e?{}:e,n=t.addedClass,a=void 0===n?"vdd-added":n,s=t.modifiedClass,f=void 0===s?"vdd-modified":s,l=t.removedClass,h=void 0===l?"vdd-removed":l,u=t.skipModified,c=void 0!==u&&u,d=t.skipChildren,g=t.skipSelf,p=t.diffText;return{addedClass:a,diffText:void 0===p?r.diffText:p,modifiedClass:f,removedClass:h,skipModified:c,skipChildren:function(e){if(!r.isElement(e)&&!r.isDocumentFragment(e)&&!r.isDocument(e))return!0;if(d){var t=d(e);if("boolean"==typeof t)return t}return i.has(e.nodeName)},skipSelf:function(e){if(!r.isText(e)&&!r.isElement(e))return!0;if(g){var t=g(e);if("boolean"==typeof t)return t}return o.has(e.nodeName)}}}},145:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(27),i=n(967),o=n(844),a=n(402),s=function(e){return"TH"===e?"TD":e},f=function(e,t){return new o.DomIterator(e,t).reduce((function(e,t){return e+(a.isText(t)?t.data:a.charForNodeName(s(t.nodeName)))}),"")},l=function(e){return a.isText(e)?e.length:1},h=function(e){return"TR"===e.nodeName},u={skipChildren:h,skipSelf:function(e){return!h(e)}};t.visualDomDiff=function e(t,n,h){var c,d;void 0===h&&(h={});var g,p,v,_,b,m=n.ownerDocument||n,y=i.optionsToConfig(h),w=y.addedClass,x=y.diffText,D=y.modifiedClass,N=y.removedClass,M=y.skipSelf,E=y.skipChildren,T=function(e){return!M(e)},S=function(e,t){return a.getAncestors(e,t).filter(T).length},O=function(e){return a.isElement(e)&&M(e)},A=function(e,t){return a.getAncestors(e,t).filter(O).reverse()},C=function(e){return Z.has(e)?1:J.has(e)?-1:0},P=x(f(t,y),f(n,y)),k=0,I=new o.DomIterator(t,y),j=new o.DomIterator(n,y),F=0,R=0,B=0;v=P[k++],c=I.next(),g=c.done,_=c.value,d=j.next(),p=d.done,b=d.value;var L=m.createDocumentFragment(),U=L,q=0,Q=L,V=0,G=null,H=null,J=new Set,Z=new Set,$=new Set,z=new Map,K=new Array,X=new Map;function Y(){for(var e=S(_,t);q>e;){if(!U.parentNode)return a.never();U===G&&(G=null),U=U.parentNode,q--}if(q!==e)return a.never()}function W(){for(var e=S(b,n);V>e;){if(!Q.parentNode)return a.never();Q===H&&(H=null),Q=Q.parentNode,V--}if(V!==e)return a.never()}function ee(e){if(U!==Q||H||G)return a.never();if(a.isText(e)){var r=A(_,t),i=A(b,n);z.set(e,i);var o=r.length;if(o!==i.length)$.add(e);else for(var s=0;st)return a.never()}function ie(e){var t,n=l(_);if((R+=e)===n)t=I.next(),g=t.done,_=t.value,R=0;else if(R>n)return a.never()}function oe(e){var t,n=l(b);if((B+=e)===n)t=j.next(),p=t.done,b=t.value,B=0;else if(B>n)return a.never()}for(;v;)if(v[0]===r.DIFF_DELETE){if(g)return a.never();Y();var ae=Math.min(v[1].length-F,l(_)-R),se=v[1].substring(F,F+ae);te(a.isText(_)?m.createTextNode(se):_.cloneNode(!1)),re(ae),ie(ae)}else if(v[0]===r.DIFF_INSERT){if(p)return a.never();W();var fe=Math.min(v[1].length-F,l(b)-B);se=v[1].substring(F,F+fe);ne(a.isText(b)?m.createTextNode(se):b.cloneNode(!1)),re(fe),oe(fe)}else{if(g||p)return a.never();Y(),W();var le=Math.min(v[1].length-F,l(_)-R,l(b)-B);se=v[1].substring(F,F+le);U===Q&&(a.isText(_)&&a.isText(b)||s(_.nodeName)===s(b.nodeName)&&!E(_)&&!E(b)||a.areNodesEqual(_,b))?ee(a.isText(b)?m.createTextNode(se):b.cloneNode(!1)):(te(a.isText(_)?m.createTextNode(se):_.cloneNode(!1)),ne(a.isText(b)?m.createTextNode(se):b.cloneNode(!1))),re(le),ie(le),oe(le)}return J.forEach((function(e){for(var t=e.parentNode,n=e.previousSibling;n&&Z.has(n);)t.insertBefore(e,n),n=e.previousSibling})),K.forEach((function(t){var n=t.newTable,r=t.oldTable,i=t.outputTable;if(!a.isTableValid(r,!0)||!a.isTableValid(n,!0)||!a.isTableValid(i,!1)){new o.DomIterator(i).forEach((function(e){Z.delete(e),J.delete(e),$.delete(e),z.delete(e)}));var s=i.parentNode,f=r.cloneNode(!0),l=n.cloneNode(!0);return s.insertBefore(f,i),s.insertBefore(l,i),s.removeChild(i),J.add(f),void Z.add(l)}var c=[];new o.DomIterator(i,u).some((function(e){var t=X.get(e);if(!t)return!1;var n=t.oldRow,r=t.newRow,i=n.childNodes.length,o=r.childNodes.length,a=Math.max(i,o),s=Math.min(i,o);if(e.childNodes.length===a)for(var f=e.childNodes,l=0,h=f.length;l{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){function e(e,t){this.rootNode=e,this.config=t,this.descend=!0,this.nextNode=this.rootNode,this.skipSelf(this.nextNode)&&this.next()}return e.prototype.toArray=function(){for(var e,t=[],n=this.next(),r=n.done,i=n.value;!r;)t.push(i),r=(e=this.next()).done,i=e.value;return t},e.prototype.forEach=function(e){for(var t,n=this.next(),r=n.done,i=n.value;!r;)e(i),r=(t=this.next()).done,i=t.value},e.prototype.reduce=function(e,t){for(var n,r=t,i=this.next(),o=i.done,a=i.value;!o;)r=e(r,a),o=(n=this.next()).done,a=n.value;return r},e.prototype.some=function(e){for(var t,n=this.next(),r=n.done,i=n.value;!r;){if(e(i))return!0;r=(t=this.next()).done,i=t.value}return!1},e.prototype.next=function(){if(!this.nextNode)return{done:!0,value:this.rootNode};var e=this.nextNode;return this.descend&&this.nextNode.firstChild&&!this.skipChildren(this.nextNode)?this.nextNode=this.nextNode.firstChild:this.nextNode===this.rootNode?this.nextNode=null:this.nextNode.nextSibling?(this.nextNode=this.nextNode.nextSibling,this.descend=!0):(this.nextNode=this.nextNode.parentNode,this.descend=!1,this.next()),this.nextNode&&this.skipSelf(this.nextNode)&&this.next(),{done:!1,value:e}},e.prototype.skipSelf=function(e){return!(!this.config||!this.config.skipSelf)&&this.config.skipSelf(e)},e.prototype.skipChildren=function(e){return!(!this.config||!this.config.skipChildren)&&this.config.skipChildren(e)},e}();t.DomIterator=n},224:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(655).__exportStar(n(145),t)},402:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(27);function i(e){return e.nodeType===e.ELEMENT_NODE}function o(e){return e.nodeType===e.TEXT_NODE}function a(e){return e.nodeType===e.COMMENT_NODE}function s(e,t){return e===t}function f(e,t,n){if(void 0===n&&(n=s),e.length!==t.length)return!1;for(var r=0,i=e.length;r="豈"?t++:(n[1]=a.substring(0,a.length-1),i[1]=l+s.substring(0,s.length-1),o[1]=l+f,0===n[1].length&&e.splice(t,1))}else t++}}t.isElement=i,t.isText=o,t.isDocument=function(e){return e.nodeType===e.DOCUMENT_NODE},t.isDocumentFragment=function(e){return e.nodeType===e.DOCUMENT_FRAGMENT_NODE},t.isComment=a,t.strictEqual=s,t.areArraysEqual=f,t.areNodesEqual=function e(t,n,r){if(void 0===r&&(r=!1),t===n)return!0;if(t.nodeType!==n.nodeType||t.nodeName!==n.nodeName)return!1;if(o(t)||a(t)){if(t.data!==n.data)return!1}else if(i(t)){var s=l(t).sort();if(!f(s,l(n).sort()))return!1;for(var h=0,u=s.length;h0&&o.push([r.DIFF_EQUAL,l.substring(0,g)]),c.diff_cleanupSemantic(o),d(i,o),o.length=0,i.push([r.DIFF_EQUAL,l.substring(g,h-p)]),p>0&&o.push([r.DIFF_EQUAL,l.substring(h-p)])}else o.push(f)}else o.push(f)}return c.diff_cleanupSemantic(o),d(i,o),o.length=0,c.diff_cleanupMerge(i),u(i),i},t.markUpNode=function(e,t,n){var r=e.ownerDocument,o=e.parentNode,a=e.previousSibling;if(i(e))e.classList.add(n);else if(a&&a.nodeName===t&&a.classList.contains(n))a.appendChild(e);else{var s=r.createElement(t);s.classList.add(n),o.insertBefore(s,e),s.appendChild(e)}},t.isTableValid=function(e,t){var n;return function(e){var t=e.childNodes,n=t.length,i=0;i{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.hmd=e=>((e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:()=>{throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e),n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};n(n.s=416)})(); \ No newline at end of file +(()=>{var e={416:(e,t,n)=>{"use strict";var r,i=n(224),o=n(52),a={addedClass:"doc-diff-added",modifiedClass:"doc-diff-modified",removedClass:"doc-diff-removed",skipModified:!0};function s(){var e={base_host:"",base_version:"latest",base_language:"en",base_page:"index.html",root_selector:"div.document[role='main']",inject_styles:!0};return new Promise((function(t,n){var r=document.querySelector("script#READTHEDOCS_DATA");if(r)try{var i=JSON.parse(r.innerText);e.base_host="https://"+i.project+".readthedocs.io",e.base_language=i.language,e.base_page=i.page+".html"}catch(e){console.debug("Error parsing configuration data",e)}var o=document.querySelector("script#doc-diff-config");if(o)try{var a=JSON.parse(o.innerText);Object.assign(e,a)}catch(e){console.debug("Error parsing configuration data",e)}return void 0===e.base_url&&(e.base_url=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"en",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"latest",r=arguments.length>3?arguments[3]:void 0,i=[];e&&(e="https://cors.writethedocs.workers.dev/corsproxy/?apiurl="+e);i=null==n&&null==t?[e,r]:null==t?[e,n,r]:[e,t,n,r];return i.join("/")}(e.base_host,e.base_language,e.base_version,e.base_page)),t(e)}))}e=n.hmd(e),n.c[n.s]!==e&&(r=new Promise((function(e){if("interactive"===document.readyState||"complete"===document.readyState)return e();document.addEventListener("DOMContentLoaded",(function(){e()}),{capture:!0,once:!0,passive:!0})})),new Promise((function(e){r.then((function(){return s()})).then((function(e){return function(e){return new Promise((function(t,n){fetch(e.base_url).then((function(e){return e.text()})).then((function(r){var s=(new DOMParser).parseFromString(r,"text/html").documentElement.querySelector(e.root_selector),f=document.querySelector(e.root_selector);null!=s&&null!=f||n(new Error("Element not found in both documents.")),e.inject_styles&&(document.adoptedStyleSheets=[o.Z]);var l=(0,i.visualDomDiff)(s,f,a);f.replaceWith(l.firstElementChild),t(!0)}))}))}(e)})).then((function(){e()})).catch((function(e){console.error(e.message)}))})))},52:(e,t,n)=>{"use strict";n.d(t,{Z:()=>f});var r=n(81),i=n.n(r),o=n(645),a=n.n(o)()(i());a.push([e.id,".doc-diff-added {\n background-color: rgb(171, 242, 188);\n text-decoration: none;\n}\n\n.doc-diff-modified {\n}\n\n.doc-diff-removed {\n background-color: rgba(255, 129, 130, 0.4);\n text-decoration: none;\n}\n",""]);var s=new CSSStyleSheet;s.replaceSync(a.toString());const f=s},645:e=>{"use strict";e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n="",r=void 0!==t[5];return t[4]&&(n+="@supports (".concat(t[4],") {")),t[2]&&(n+="@media ".concat(t[2]," {")),r&&(n+="@layer".concat(t[5].length>0?" ".concat(t[5]):""," {")),n+=e(t),r&&(n+="}"),t[2]&&(n+="}"),t[4]&&(n+="}"),n})).join("")},t.i=function(e,n,r,i,o){"string"==typeof e&&(e=[[null,e,void 0]]);var a={};if(r)for(var s=0;s0?" ".concat(h[5]):""," {").concat(h[1],"}")),h[5]=o),n&&(h[2]?(h[1]="@media ".concat(h[2]," {").concat(h[1],"}"),h[2]=n):h[2]=n),i&&(h[4]?(h[1]="@supports (".concat(h[4],") {").concat(h[1],"}"),h[4]=i):h[4]="".concat(i)),t.push(h))}},t}},81:e=>{"use strict";e.exports=function(e){return e[1]}},27:e=>{var t=function(){this.Diff_Timeout=1,this.Diff_EditCost=4,this.Match_Threshold=.5,this.Match_Distance=1e3,this.Patch_DeleteThreshold=.5,this.Patch_Margin=4,this.Match_MaxBits=32},n=-1;t.Diff=function(e,t){return[e,t]},t.prototype.diff_main=function(e,n,r,i){void 0===i&&(i=this.Diff_Timeout<=0?Number.MAX_VALUE:(new Date).getTime()+1e3*this.Diff_Timeout);var o=i;if(null==e||null==n)throw new Error("Null input. (diff_main)");if(e==n)return e?[new t.Diff(0,e)]:[];void 0===r&&(r=!0);var a=r,s=this.diff_commonPrefix(e,n),f=e.substring(0,s);e=e.substring(s),n=n.substring(s),s=this.diff_commonSuffix(e,n);var l=e.substring(e.length-s);e=e.substring(0,e.length-s),n=n.substring(0,n.length-s);var h=this.diff_compute_(e,n,a,o);return f&&h.unshift(new t.Diff(0,f)),l&&h.push(new t.Diff(0,l)),this.diff_cleanupMerge(h),h},t.prototype.diff_compute_=function(e,r,i,o){var a;if(!e)return[new t.Diff(1,r)];if(!r)return[new t.Diff(n,e)];var s=e.length>r.length?e:r,f=e.length>r.length?r:e,l=s.indexOf(f);if(-1!=l)return a=[new t.Diff(1,s.substring(0,l)),new t.Diff(0,f),new t.Diff(1,s.substring(l+f.length))],e.length>r.length&&(a[0][0]=a[2][0]=n),a;if(1==f.length)return[new t.Diff(n,e),new t.Diff(1,r)];var h=this.diff_halfMatch_(e,r);if(h){var u=h[0],c=h[1],d=h[2],g=h[3],p=h[4],v=this.diff_main(u,d,i,o),_=this.diff_main(c,g,i,o);return v.concat([new t.Diff(0,p)],_)}return i&&e.length>100&&r.length>100?this.diff_lineMode_(e,r,o):this.diff_bisect_(e,r,o)},t.prototype.diff_lineMode_=function(e,r,i){var o=this.diff_linesToChars_(e,r);e=o.chars1,r=o.chars2;var a=o.lineArray,s=this.diff_main(e,r,!1,i);this.diff_charsToLines_(s,a),this.diff_cleanupSemantic(s),s.push(new t.Diff(0,""));for(var f=0,l=0,h=0,u="",c="";f=1&&h>=1){s.splice(f-l-h,l+h),f=f-l-h;for(var d=this.diff_main(u,c,!1,i),g=d.length-1;g>=0;g--)s.splice(f,0,d[g]);f+=d.length}h=0,l=0,u="",c=""}f++}return s.pop(),s},t.prototype.diff_bisect_=function(e,r,i){for(var o=e.length,a=r.length,s=Math.ceil((o+a)/2),f=s,l=2*s,h=new Array(l),u=new Array(l),c=0;ci);m++){for(var y=-m+p;y<=m-v;y+=2){for(var w=f+y,x=(T=y==-m||y!=m&&h[w-1]o)v+=2;else if(x>a)p+=2;else if(g){if((M=f+d-y)>=0&&M=(N=o-u[M]))return this.diff_bisectSplit_(e,r,T,x,i)}}for(var D=-m+_;D<=m-b;D+=2){for(var N,M=f+D,E=(N=D==-m||D!=m&&u[M-1]o)b+=2;else if(E>a)_+=2;else if(!g){if((w=f+d-D)>=0&&w=(N=o-N))return this.diff_bisectSplit_(e,r,T,x,i)}}}}return[new t.Diff(n,e),new t.Diff(1,r)]},t.prototype.diff_bisectSplit_=function(e,t,n,r,i){var o=e.substring(0,n),a=t.substring(0,r),s=e.substring(n),f=t.substring(r),l=this.diff_main(o,a,!1,i),h=this.diff_main(s,f,!1,i);return l.concat(h)},t.prototype.diff_linesToChars_=function(e,t){var n=[],r={};function i(e){for(var t="",i=0,a=-1,s=n.length;ar?e=e.substring(n-r):nt.length?e:t,r=e.length>t.length?t:e;if(n.length<4||2*r.length=e.length?[r,o,a,s,h]:null}var a,s,f,l,h,u=o(n,r,Math.ceil(n.length/4)),c=o(n,r,Math.ceil(n.length/2));return u||c?(a=c?u&&u[4].length>c[4].length?u:c:u,e.length>t.length?(s=a[0],f=a[1],l=a[2],h=a[3]):(l=a[0],h=a[1],s=a[2],f=a[3]),[s,f,l,h,a[4]]):null},t.prototype.diff_cleanupSemantic=function(e){for(var r=!1,i=[],o=0,a=null,s=0,f=0,l=0,h=0,u=0;s0?i[o-1]:-1,f=0,l=0,h=0,u=0,a=null,r=!0)),s++;for(r&&this.diff_cleanupMerge(e),this.diff_cleanupSemanticLossless(e),s=1;s=p?(g>=c.length/2||g>=d.length/2)&&(e.splice(s,0,new t.Diff(0,d.substring(0,g))),e[s-1][1]=c.substring(0,c.length-g),e[s+1][1]=d.substring(g),s++):(p>=c.length/2||p>=d.length/2)&&(e.splice(s,0,new t.Diff(0,c.substring(0,p))),e[s-1][0]=1,e[s-1][1]=d.substring(0,d.length-p),e[s+1][0]=n,e[s+1][1]=c.substring(p),s++),s++}s++}},t.prototype.diff_cleanupSemanticLossless=function(e){function n(e,n){if(!e||!n)return 6;var r=e.charAt(e.length-1),i=n.charAt(0),o=r.match(t.nonAlphaNumericRegex_),a=i.match(t.nonAlphaNumericRegex_),s=o&&r.match(t.whitespaceRegex_),f=a&&i.match(t.whitespaceRegex_),l=s&&r.match(t.linebreakRegex_),h=f&&i.match(t.linebreakRegex_),u=l&&e.match(t.blanklineEndRegex_),c=h&&n.match(t.blanklineStartRegex_);return u||c?5:l||h?4:o&&!s&&f?3:s||f?2:o||a?1:0}for(var r=1;r=c&&(c=d,l=i,h=o,u=a)}e[r-1][1]!=l&&(l?e[r-1][1]=l:(e.splice(r-1,1),r--),e[r][1]=h,u?e[r+1][1]=u:(e.splice(r+1,1),r--))}r++}},t.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/,t.whitespaceRegex_=/\s/,t.linebreakRegex_=/[\r\n]/,t.blanklineEndRegex_=/\n\r?\n$/,t.blanklineStartRegex_=/^\r?\n\r?\n/,t.prototype.diff_cleanupEfficiency=function(e){for(var r=!1,i=[],o=0,a=null,s=0,f=!1,l=!1,h=!1,u=!1;s0?i[o-1]:-1,h=u=!1),r=!0)),s++;r&&this.diff_cleanupMerge(e)},t.prototype.diff_cleanupMerge=function(e){e.push(new t.Diff(0,""));for(var r,i=0,o=0,a=0,s="",f="";i1?(0!==o&&0!==a&&(0!==(r=this.diff_commonPrefix(f,s))&&(i-o-a>0&&0==e[i-o-a-1][0]?e[i-o-a-1][1]+=f.substring(0,r):(e.splice(0,0,new t.Diff(0,f.substring(0,r))),i++),f=f.substring(r),s=s.substring(r)),0!==(r=this.diff_commonSuffix(f,s))&&(e[i][1]=f.substring(f.length-r)+e[i][1],f=f.substring(0,f.length-r),s=s.substring(0,s.length-r))),i-=o+a,e.splice(i,o+a),s.length&&(e.splice(i,0,new t.Diff(n,s)),i++),f.length&&(e.splice(i,0,new t.Diff(1,f)),i++),i++):0!==i&&0==e[i-1][0]?(e[i-1][1]+=e[i][1],e.splice(i,1)):i++,a=0,o=0,s="",f=""}""===e[e.length-1][1]&&e.pop();var l=!1;for(i=1;it));r++)a=i,s=o;return e.length!=r&&e[r][0]===n?s:s+(t-a)},t.prototype.diff_prettyHtml=function(e){for(var t=[],r=/&/g,i=//g,a=/\n/g,s=0;s");switch(f){case 1:t[s]=''+l+"";break;case n:t[s]=''+l+"";break;case 0:t[s]=""+l+""}}return t.join("")},t.prototype.diff_text1=function(e){for(var t=[],n=0;nthis.Match_MaxBits)throw new Error("Pattern too long for this browser.");var r=this.match_alphabet_(t),i=this;function o(e,r){var o=e/t.length,a=Math.abs(n-r);return i.Match_Distance?o+a/i.Match_Distance:a?1:o}var a=this.Match_Threshold,s=e.indexOf(t,n);-1!=s&&(a=Math.min(o(0,s),a),-1!=(s=e.lastIndexOf(t,n+t.length))&&(a=Math.min(o(0,s),a)));var f,l,h=1<=g;_--){var b=r[e.charAt(_-1)];if(v[_]=0===d?(v[_+1]<<1|1)&b:(v[_+1]<<1|1)&b|(u[_+1]|u[_])<<1|1|u[_+1],v[_]&h){var m=o(d,_-1);if(m<=a){if(a=m,!((s=_-1)>n))break;g=Math.max(1,2*n-s)}}}if(o(d+1,n)>a)break;u=v}return s},t.prototype.match_alphabet_=function(e){for(var t={},n=0;n2&&(this.diff_cleanupSemantic(a),this.diff_cleanupEfficiency(a));else if(e&&"object"==typeof e&&void 0===r&&void 0===i)a=e,o=this.diff_text1(a);else if("string"==typeof e&&r&&"object"==typeof r&&void 0===i)o=e,a=r;else{if("string"!=typeof e||"string"!=typeof r||!i||"object"!=typeof i)throw new Error("Unknown call format to patch_make.");o=e,a=i}if(0===a.length)return[];for(var s=[],f=new t.patch_obj,l=0,h=0,u=0,c=o,d=o,g=0;g=2*this.Patch_Margin&&l&&(this.patch_addContext_(f,c),s.push(f),f=new t.patch_obj,l=0,c=d,h=u)}1!==p&&(h+=v.length),p!==n&&(u+=v.length)}return l&&(this.patch_addContext_(f,c),s.push(f)),s},t.prototype.patch_deepCopy=function(e){for(var n=[],r=0;rthis.Match_MaxBits?-1!=(s=this.match_main(t,h.substring(0,this.Match_MaxBits),l))&&(-1==(u=this.match_main(t,h.substring(h.length-this.Match_MaxBits),l+h.length-this.Match_MaxBits))||s>=u)&&(s=-1):s=this.match_main(t,h,l),-1==s)o[a]=!1,i-=e[a].length2-e[a].length1;else if(o[a]=!0,i=s-l,h==(f=-1==u?t.substring(s,s+h.length):t.substring(s,u+this.Match_MaxBits)))t=t.substring(0,s)+this.diff_text2(e[a].diffs)+t.substring(s+h.length);else{var c=this.diff_main(h,f,!1);if(h.length>this.Match_MaxBits&&this.diff_levenshtein(c)/h.length>this.Patch_DeleteThreshold)o[a]=!1;else{this.diff_cleanupSemanticLossless(c);for(var d,g=0,p=0;pa[0][1].length){var s=n-a[0][1].length;a[0][1]=r.substring(a[0][1].length)+a[0][1],o.start1-=s,o.start2-=s,o.length1+=s,o.length2+=s}if(0==(a=(o=e[e.length-1]).diffs).length||0!=a[a.length-1][0])a.push(new t.Diff(0,r)),o.length1+=n,o.length2+=n;else if(n>a[a.length-1][1].length){s=n-a[a.length-1][1].length;a[a.length-1][1]+=r.substring(0,s),o.length1+=s,o.length2+=s}return r},t.prototype.patch_splitMax=function(e){for(var r=this.Match_MaxBits,i=0;i2*r?(l.length1+=c.length,a+=c.length,h=!1,l.diffs.push(new t.Diff(u,c)),o.diffs.shift()):(c=c.substring(0,r-l.length1-this.Patch_Margin),l.length1+=c.length,a+=c.length,0===u?(l.length2+=c.length,s+=c.length):h=!1,l.diffs.push(new t.Diff(u,c)),c==o.diffs[0][1]?o.diffs.shift():o.diffs[0][1]=o.diffs[0][1].substring(c.length))}f=(f=this.diff_text2(l.diffs)).substring(f.length-this.Patch_Margin);var d=this.diff_text1(o.diffs).substring(0,this.Patch_Margin);""!==d&&(l.length1+=d.length,l.length2+=d.length,0!==l.diffs.length&&0===l.diffs[l.diffs.length-1][0]?l.diffs[l.diffs.length-1][1]+=d:l.diffs.push(new t.Diff(0,d))),h||e.splice(++i,0,l)}}},t.prototype.patch_toText=function(e){for(var t=[],n=0;n{"use strict";n.r(t),n.d(t,{__assign:()=>o,__asyncDelegator:()=>w,__asyncGenerator:()=>y,__asyncValues:()=>x,__await:()=>m,__awaiter:()=>h,__classPrivateFieldGet:()=>T,__classPrivateFieldIn:()=>O,__classPrivateFieldSet:()=>S,__createBinding:()=>c,__decorate:()=>s,__exportStar:()=>d,__extends:()=>i,__generator:()=>u,__importDefault:()=>E,__importStar:()=>M,__makeTemplateObject:()=>D,__metadata:()=>l,__param:()=>f,__read:()=>p,__rest:()=>a,__spread:()=>v,__spreadArray:()=>b,__spreadArrays:()=>_,__values:()=>g});var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},r(e,t)};function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var o=function(){return o=Object.assign||function(e){for(var t,n=1,r=arguments.length;n=0;s--)(i=e[s])&&(a=(o<3?i(a):o>3?i(t,n,a):i(t,n))||a);return o>3&&a&&Object.defineProperty(t,n,a),a}function f(e,t){return function(n,r){t(n,r,e)}}function l(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}function h(e,t,n,r){return new(n||(n=Promise))((function(i,o){function a(e){try{f(r.next(e))}catch(e){o(e)}}function s(e){try{f(r.throw(e))}catch(e){o(e)}}function f(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}f((r=r.apply(e,t||[])).next())}))}function u(e,t){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function s(o){return function(s){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=a.trys,(i=i.length>0&&i[i.length-1])||6!==o[0]&&2!==o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=e.length&&(e=void 0),{value:e&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function p(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||t-- >0)&&!(r=o.next()).done;)a.push(r.value)}catch(e){i={error:e}}finally{try{r&&!r.done&&(n=o.return)&&n.call(o)}finally{if(i)throw i.error}}return a}function v(){for(var e=[],t=0;t1||s(e,t)}))})}function s(e,t){try{(n=i[e](t)).value instanceof m?Promise.resolve(n.value.v).then(f,l):h(o[0][2],n)}catch(e){h(o[0][3],e)}var n}function f(e){s("next",e)}function l(e){s("throw",e)}function h(e,t){e(t),o.shift(),o.length&&s(o[0][0],o[0][1])}}function w(e){var t,n;return t={},r("next"),r("throw",(function(e){throw e})),r("return"),t[Symbol.iterator]=function(){return this},t;function r(r,i){t[r]=e[r]?function(t){return(n=!n)?{value:m(e[r](t)),done:"return"===r}:i?i(t):t}:i}}function x(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t,n=e[Symbol.asyncIterator];return n?n.call(e):(e=g(e),t={},r("next"),r("throw"),r("return"),t[Symbol.asyncIterator]=function(){return this},t);function r(n){t[n]=e[n]&&function(t){return new Promise((function(r,i){(function(e,t,n,r){Promise.resolve(r).then((function(t){e({value:t,done:n})}),t)})(r,i,(t=e[n](t)).done,t.value)}))}}}function D(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e}var N=Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t};function M(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&c(t,e,n);return N(t,e),t}function E(e){return e&&e.__esModule?e:{default:e}}function T(e,t,n,r){if("a"===n&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!r:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?r:"a"===n?r.call(e):r?r.value:t.get(e)}function S(e,t,n,r,i){if("m"===r)throw new TypeError("Private method is not writable");if("a"===r&&!i)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return"a"===r?i.call(e,n):i?i.value=n:t.set(e,n),n}function O(e,t){if(null===t||"object"!=typeof t&&"function"!=typeof t)throw new TypeError("Cannot use 'in' operator on non-object");return"function"==typeof e?t===e:e.has(t)}},967:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(402),i=new Set;i.add("IMG"),i.add("VIDEO"),i.add("IFRAME"),i.add("OBJECT"),i.add("SVG");var o=new Set;o.add("BDO"),o.add("BDI"),o.add("Q"),o.add("CITE"),o.add("CODE"),o.add("DATA"),o.add("TIME"),o.add("VAR"),o.add("DFN"),o.add("ABBR"),o.add("STRONG"),o.add("EM"),o.add("BIG"),o.add("SMALL"),o.add("MARK"),o.add("SUB"),o.add("SUP"),o.add("SAMP"),o.add("KBD"),o.add("B"),o.add("I"),o.add("S"),o.add("U"),o.add("SPAN"),t.optionsToConfig=function(e){var t=void 0===e?{}:e,n=t.addedClass,a=void 0===n?"vdd-added":n,s=t.modifiedClass,f=void 0===s?"vdd-modified":s,l=t.removedClass,h=void 0===l?"vdd-removed":l,u=t.skipModified,c=void 0!==u&&u,d=t.skipChildren,g=t.skipSelf,p=t.diffText;return{addedClass:a,diffText:void 0===p?r.diffText:p,modifiedClass:f,removedClass:h,skipModified:c,skipChildren:function(e){if(!r.isElement(e)&&!r.isDocumentFragment(e)&&!r.isDocument(e))return!0;if(d){var t=d(e);if("boolean"==typeof t)return t}return i.has(e.nodeName)},skipSelf:function(e){if(!r.isText(e)&&!r.isElement(e))return!0;if(g){var t=g(e);if("boolean"==typeof t)return t}return o.has(e.nodeName)}}}},145:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(27),i=n(967),o=n(844),a=n(402),s=function(e){return"TH"===e?"TD":e},f=function(e,t){return new o.DomIterator(e,t).reduce((function(e,t){return e+(a.isText(t)?t.data:a.charForNodeName(s(t.nodeName)))}),"")},l=function(e){return a.isText(e)?e.length:1},h=function(e){return"TR"===e.nodeName},u={skipChildren:h,skipSelf:function(e){return!h(e)}};t.visualDomDiff=function e(t,n,h){var c,d;void 0===h&&(h={});var g,p,v,_,b,m=n.ownerDocument||n,y=i.optionsToConfig(h),w=y.addedClass,x=y.diffText,D=y.modifiedClass,N=y.removedClass,M=y.skipSelf,E=y.skipChildren,T=function(e){return!M(e)},S=function(e,t){return a.getAncestors(e,t).filter(T).length},O=function(e){return a.isElement(e)&&M(e)},A=function(e,t){return a.getAncestors(e,t).filter(O).reverse()},C=function(e){return Z.has(e)?1:J.has(e)?-1:0},P=x(f(t,y),f(n,y)),k=0,I=new o.DomIterator(t,y),j=new o.DomIterator(n,y),F=0,R=0,B=0;v=P[k++],c=I.next(),g=c.done,_=c.value,d=j.next(),p=d.done,b=d.value;var L=m.createDocumentFragment(),U=L,q=0,Q=L,V=0,G=null,H=null,J=new Set,Z=new Set,$=new Set,z=new Map,K=new Array,X=new Map;function Y(){for(var e=S(_,t);q>e;){if(!U.parentNode)return a.never();U===G&&(G=null),U=U.parentNode,q--}if(q!==e)return a.never()}function W(){for(var e=S(b,n);V>e;){if(!Q.parentNode)return a.never();Q===H&&(H=null),Q=Q.parentNode,V--}if(V!==e)return a.never()}function ee(e){if(U!==Q||H||G)return a.never();if(a.isText(e)){var r=A(_,t),i=A(b,n);z.set(e,i);var o=r.length;if(o!==i.length)$.add(e);else for(var s=0;st)return a.never()}function ie(e){var t,n=l(_);if((R+=e)===n)t=I.next(),g=t.done,_=t.value,R=0;else if(R>n)return a.never()}function oe(e){var t,n=l(b);if((B+=e)===n)t=j.next(),p=t.done,b=t.value,B=0;else if(B>n)return a.never()}for(;v;)if(v[0]===r.DIFF_DELETE){if(g)return a.never();Y();var ae=Math.min(v[1].length-F,l(_)-R),se=v[1].substring(F,F+ae);te(a.isText(_)?m.createTextNode(se):_.cloneNode(!1)),re(ae),ie(ae)}else if(v[0]===r.DIFF_INSERT){if(p)return a.never();W();var fe=Math.min(v[1].length-F,l(b)-B);se=v[1].substring(F,F+fe);ne(a.isText(b)?m.createTextNode(se):b.cloneNode(!1)),re(fe),oe(fe)}else{if(g||p)return a.never();Y(),W();var le=Math.min(v[1].length-F,l(_)-R,l(b)-B);se=v[1].substring(F,F+le);U===Q&&(a.isText(_)&&a.isText(b)||s(_.nodeName)===s(b.nodeName)&&!E(_)&&!E(b)||a.areNodesEqual(_,b))?ee(a.isText(b)?m.createTextNode(se):b.cloneNode(!1)):(te(a.isText(_)?m.createTextNode(se):_.cloneNode(!1)),ne(a.isText(b)?m.createTextNode(se):b.cloneNode(!1))),re(le),ie(le),oe(le)}return J.forEach((function(e){for(var t=e.parentNode,n=e.previousSibling;n&&Z.has(n);)t.insertBefore(e,n),n=e.previousSibling})),K.forEach((function(t){var n=t.newTable,r=t.oldTable,i=t.outputTable;if(!a.isTableValid(r,!0)||!a.isTableValid(n,!0)||!a.isTableValid(i,!1)){new o.DomIterator(i).forEach((function(e){Z.delete(e),J.delete(e),$.delete(e),z.delete(e)}));var s=i.parentNode,f=r.cloneNode(!0),l=n.cloneNode(!0);return s.insertBefore(f,i),s.insertBefore(l,i),s.removeChild(i),J.add(f),void Z.add(l)}var c=[];new o.DomIterator(i,u).some((function(e){var t=X.get(e);if(!t)return!1;var n=t.oldRow,r=t.newRow,i=n.childNodes.length,o=r.childNodes.length,a=Math.max(i,o),s=Math.min(i,o);if(e.childNodes.length===a)for(var f=e.childNodes,l=0,h=f.length;l{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){function e(e,t){this.rootNode=e,this.config=t,this.descend=!0,this.nextNode=this.rootNode,this.skipSelf(this.nextNode)&&this.next()}return e.prototype.toArray=function(){for(var e,t=[],n=this.next(),r=n.done,i=n.value;!r;)t.push(i),r=(e=this.next()).done,i=e.value;return t},e.prototype.forEach=function(e){for(var t,n=this.next(),r=n.done,i=n.value;!r;)e(i),r=(t=this.next()).done,i=t.value},e.prototype.reduce=function(e,t){for(var n,r=t,i=this.next(),o=i.done,a=i.value;!o;)r=e(r,a),o=(n=this.next()).done,a=n.value;return r},e.prototype.some=function(e){for(var t,n=this.next(),r=n.done,i=n.value;!r;){if(e(i))return!0;r=(t=this.next()).done,i=t.value}return!1},e.prototype.next=function(){if(!this.nextNode)return{done:!0,value:this.rootNode};var e=this.nextNode;return this.descend&&this.nextNode.firstChild&&!this.skipChildren(this.nextNode)?this.nextNode=this.nextNode.firstChild:this.nextNode===this.rootNode?this.nextNode=null:this.nextNode.nextSibling?(this.nextNode=this.nextNode.nextSibling,this.descend=!0):(this.nextNode=this.nextNode.parentNode,this.descend=!1,this.next()),this.nextNode&&this.skipSelf(this.nextNode)&&this.next(),{done:!1,value:e}},e.prototype.skipSelf=function(e){return!(!this.config||!this.config.skipSelf)&&this.config.skipSelf(e)},e.prototype.skipChildren=function(e){return!(!this.config||!this.config.skipChildren)&&this.config.skipChildren(e)},e}();t.DomIterator=n},224:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(655).__exportStar(n(145),t)},402:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(27);function i(e){return e.nodeType===e.ELEMENT_NODE}function o(e){return e.nodeType===e.TEXT_NODE}function a(e){return e.nodeType===e.COMMENT_NODE}function s(e,t){return e===t}function f(e,t,n){if(void 0===n&&(n=s),e.length!==t.length)return!1;for(var r=0,i=e.length;r="豈"?t++:(n[1]=a.substring(0,a.length-1),i[1]=l+s.substring(0,s.length-1),o[1]=l+f,0===n[1].length&&e.splice(t,1))}else t++}}t.isElement=i,t.isText=o,t.isDocument=function(e){return e.nodeType===e.DOCUMENT_NODE},t.isDocumentFragment=function(e){return e.nodeType===e.DOCUMENT_FRAGMENT_NODE},t.isComment=a,t.strictEqual=s,t.areArraysEqual=f,t.areNodesEqual=function e(t,n,r){if(void 0===r&&(r=!1),t===n)return!0;if(t.nodeType!==n.nodeType||t.nodeName!==n.nodeName)return!1;if(o(t)||a(t)){if(t.data!==n.data)return!1}else if(i(t)){var s=l(t).sort();if(!f(s,l(n).sort()))return!1;for(var h=0,u=s.length;h0&&o.push([r.DIFF_EQUAL,l.substring(0,g)]),c.diff_cleanupSemantic(o),d(i,o),o.length=0,i.push([r.DIFF_EQUAL,l.substring(g,h-p)]),p>0&&o.push([r.DIFF_EQUAL,l.substring(h-p)])}else o.push(f)}else o.push(f)}return c.diff_cleanupSemantic(o),d(i,o),o.length=0,c.diff_cleanupMerge(i),u(i),i},t.markUpNode=function(e,t,n){var r=e.ownerDocument,o=e.parentNode,a=e.previousSibling;if(i(e))e.classList.add(n);else if(a&&a.nodeName===t&&a.classList.contains(n))a.appendChild(e);else{var s=r.createElement(t);s.classList.add(n),o.insertBefore(s,e),s.appendChild(e)}},t.isTableValid=function(e,t){var n;return function(e){var t=e.childNodes,n=t.length,i=0;i{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.hmd=e=>((e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:()=>{throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e),n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};n(n.s=416)})(); diff --git a/docs/user/guides/setup/index.rst b/docs/user/guides/setup/index.rst index cd1f25a21ca..c2ac3388654 100644 --- a/docs/user/guides/setup/index.rst +++ b/docs/user/guides/setup/index.rst @@ -24,6 +24,10 @@ The following how-to guides help you solve common tasks and challenges in the se Need several projects under the same umbrella? Start using subprojects, which is a way to host multiple projects under a "main project". +⏩️ :doc:`Using a .readthedocs.yaml file in a sub-folder ` + This guide shows how to configure a Read the Docs project to use a custom path for the ``.readthedocs.yaml`` build configuration. + *Monorepos* that have multiple documentation projects in the same Git repository can benefit from this feature. + ⏩️ :doc:`Hiding a version ` Is your version (flyout) menu overwhelmed and hard to navigate? Here's how to make it shorter. @@ -44,4 +48,5 @@ The following how-to guides help you solve common tasks and challenges in the se Managing custom domains Managing subprojects Hiding a version + Using a .readthedocs.yaml file in a sub-folder Using custom URL redirects in documentation projects diff --git a/docs/user/guides/setup/monorepo.rst b/docs/user/guides/setup/monorepo.rst new file mode 100644 index 00000000000..fb54c53b7d9 --- /dev/null +++ b/docs/user/guides/setup/monorepo.rst @@ -0,0 +1,97 @@ +.. Next steps: Show an example pattern for a monorepo layout or link to an example project + +How to use a .readthedocs.yaml file in a sub-folder +=================================================== + +This guide shows how to configure a Read the Docs project to use a custom path for the ``.readthedocs.yaml`` build configuration. +`Monorepos `__ that have multiple documentation projects in the same Git repository can benefit from this feature. + +By default, +Read the Docs will use the ``.readthedocs.yaml`` at the top level of your Git repository. +But if a Git repository contains multiple documentation projects that need different build configurations, +you will need to have a ``.readthedocs.yaml`` file in multiple sub-folders. + +.. seealso:: + + `sphinx-multiproject `__ + If you are only using Sphinx projects and want to share the same build configuration, + you can also use the ``sphinx-multiproject`` extension. + + :doc:`/guides/environment-variables` + You might also be able to reuse the same configuration file across multiple projects, + using only environment variables. + This is possible if the configuration pattern is very similar and the documentation tool is the same. + +Implementation considerations +----------------------------- + +This feature is currently *project-wide*. +A custom build configuration file path is applied to all versions of your documentation. + +.. warning:: + + Changing the configuration path will apply to all versions. + Different versions of the project may not be able to build again if this path is changed. + +Adding an additional project from the same repository +----------------------------------------------------- + +Once you have added the first project from the :ref:`Import Wizard `, +it will show as if it has already been imported and cannot be imported again. +In order to add another project with the same repository, +you will need to use the :ref:`Manual Import `. + +Setting the custom build configuration file +------------------------------------------- + +Once you have added a Git repository to a project that needs a custom configuration file path, +navigate to :menuselection:`Admin --> Advanced Settings` and add the path to the :guilabel:`Build configuration file` field. + +.. image:: /img/screenshot-howto-build-configuration-file.png + :alt: Screenshot of where to find the :guilabel:`Build configuration file` setting. + +After pressing :guilabel:`Save`, +you need to ensure that relevant versions of your documentation are built again. + +.. tip:: + + Having multiple different build configuration files can be complex. + We recommend setting up 1-2 projects in your Monorepo and getting them to build and publish successfully before adding additional projects to the equation. + +Next steps +---------- + +Once you have your monorepo pattern implemented and tested and it's ready to roll out to all your projects, +you should also consider the Read the Docs project setup for these individual projects. + +Having individual projects gives you the full flexibility of the Read the Docs platform to make individual setups for each project. + +For each project, it's now possible to configure: + +* Sets of maintainers (or :doc:`organizations ` on |com_brand|) +* :doc:`Custom redirect rules ` +* :doc:`Custom domains ` +* :doc:`Automation rules ` +* :doc:`Traffic and search analytics ` +* Additional documentation tools with individual :doc:`build processes `: + One project might use :doc:`Sphinx `, + while another project setup might use `Asciidoctor `__. + +...and much more. *All* settings for a Read the Docs project is available for each individual project. + +.. seealso:: + + :doc:`/guides/subprojects` + More information on nesting one project inside another project. + In this setup, it is still possible to use the same monorepo for each subproject. + +Other tips +---------- + +For a monorepo, +it's not desirable to have changes in unrelated sub-folders trigger new builds. + +Therefore, +you should consider setting up :ref:`conditional build cancellation rules `. +The configuration is added in each ``.readthedocs.yaml``, +making it possible to write one conditional build rules per documentation project in the Monorepo 💯️ diff --git a/docs/user/img/screenshot-howto-build-configuration-file.png b/docs/user/img/screenshot-howto-build-configuration-file.png new file mode 100644 index 00000000000..f182d0ccaf2 Binary files /dev/null and b/docs/user/img/screenshot-howto-build-configuration-file.png differ diff --git a/readthedocs/api/v2/serializers.py b/readthedocs/api/v2/serializers.py index c0f1855ed11..a897cdc1403 100644 --- a/readthedocs/api/v2/serializers.py +++ b/readthedocs/api/v2/serializers.py @@ -73,26 +73,27 @@ def get_skip(self, obj): class Meta(ProjectSerializer.Meta): fields = ProjectSerializer.Meta.fields + ( - 'enable_epub_build', - 'enable_pdf_build', - 'conf_py_file', - 'analytics_code', - 'analytics_disabled', - 'cdn_enabled', - 'container_image', - 'container_mem_limit', - 'container_time_limit', - 'install_project', - 'use_system_packages', - 'skip', - 'requirements_file', - 'python_interpreter', - 'features', - 'has_valid_clone', - 'has_valid_webhook', - 'show_advertising', - 'environment_variables', - 'max_concurrent_builds', + "enable_epub_build", + "enable_pdf_build", + "conf_py_file", + "analytics_code", + "analytics_disabled", + "cdn_enabled", + "container_image", + "container_mem_limit", + "container_time_limit", + "install_project", + "use_system_packages", + "skip", + "requirements_file", + "python_interpreter", + "features", + "has_valid_clone", + "has_valid_webhook", + "show_advertising", + "environment_variables", + "max_concurrent_builds", + "readthedocs_yaml_path", ) diff --git a/readthedocs/builds/admin.py b/readthedocs/builds/admin.py index 83602b49a2d..c3c3d6b23d6 100644 --- a/readthedocs/builds/admin.py +++ b/readthedocs/builds/admin.py @@ -33,6 +33,7 @@ class BuildAdmin(admin.ModelAdmin): "date", "builder", "length", + "readthedocs_yaml_path", "pretty_config", ) readonly_fields = ( diff --git a/readthedocs/builds/migrations/0050_build_readthedocs_yaml_path.py b/readthedocs/builds/migrations/0050_build_readthedocs_yaml_path.py new file mode 100644 index 00000000000..233af10ded9 --- /dev/null +++ b/readthedocs/builds/migrations/0050_build_readthedocs_yaml_path.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.18 on 2023-04-04 13:03 + +from django.db import migrations, models + +import readthedocs.projects.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ("builds", "0049_automation_rule_copy"), + ] + + operations = [ + migrations.AddField( + model_name="build", + name="readthedocs_yaml_path", + field=models.CharField( + blank=True, + default=None, + max_length=1024, + null=True, + validators=[readthedocs.projects.validators.validate_build_config_file], + verbose_name="Custom build configuration file path used in this build", + ), + ), + ] diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index b49ad8dc6b6..c0c4ab8af83 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -81,6 +81,7 @@ SPHINX_SINGLEHTML, ) from readthedocs.projects.models import APIProject, Project +from readthedocs.projects.validators import validate_build_config_file from readthedocs.projects.version_handling import determine_stable_version log = structlog.get_logger(__name__) @@ -707,6 +708,14 @@ class Build(models.Model): null=True, blank=True, ) + readthedocs_yaml_path = models.CharField( + _("Custom build configuration file path used in this build"), + max_length=1024, + default=None, + blank=True, + null=True, + validators=[validate_build_config_file], + ) length = models.IntegerField(_('Build Length'), null=True, blank=True) diff --git a/readthedocs/config/config.py b/readthedocs/config/config.py index 3c755bdd612..c59196bfd4d 100644 --- a/readthedocs/config/config.py +++ b/readthedocs/config/config.py @@ -43,17 +43,18 @@ ) __all__ = ( - 'ALL', - 'load', - 'BuildConfigV1', - 'BuildConfigV2', - 'ConfigError', - 'ConfigOptionNotSupportedError', - 'ConfigFileNotFound', - 'InvalidConfig', - 'PIP', - 'SETUPTOOLS', - 'LATEST_CONFIGURATION_VERSION', + "ALL", + "load", + "BuildConfigV1", + "BuildConfigV2", + "ConfigError", + "ConfigOptionNotSupportedError", + "ConfigFileNotFound", + "DefaultConfigFileNotFound", + "InvalidConfig", + "PIP", + "SETUPTOOLS", + "LATEST_CONFIGURATION_VERSION", ) ALL = 'all' @@ -96,6 +97,17 @@ def __init__(self, directory): ) +class DefaultConfigFileNotFound(ConfigError): + + """Error when we can't find a configuration file.""" + + def __init__(self, directory): + super().__init__( + f"No default configuration file in: {directory}", + CONFIG_FILE_REQUIRED, + ) + + class ConfigOptionNotSupportedError(ConfigError): """Error for unsupported configuration options in a version.""" @@ -1369,7 +1381,7 @@ def search(self): return Search(**self._config['search']) -def load(path, env_config): +def load(path, env_config, readthedocs_yaml_path=None): """ Load a project configuration and the top-most build config for a given path. @@ -1377,10 +1389,20 @@ def load(path, env_config): the version of the configuration a build object would be load and validated, ``BuildConfigV1`` is the default. """ - filename = find_one(path, CONFIG_FILENAME_REGEX) - - if not filename: - raise ConfigFileNotFound(path) + # Custom non-default config file location + if readthedocs_yaml_path: + filename = os.path.join(path, readthedocs_yaml_path) + # When a config file is specified and not found, we raise ConfigError + # because ConfigFileNotFound + if not os.path.exists(filename): + raise ConfigFileNotFound(os.path.relpath(filename, path)) + # Default behavior + else: + filename = find_one(path, CONFIG_FILENAME_REGEX) + if not filename: + # This exception is current caught higher up and will result in an attempt + # to load the v1 config schema. + raise DefaultConfigFileNotFound(path) # Allow symlinks, but only the ones that resolve inside the base directory. with safe_open( diff --git a/readthedocs/config/tests/test_config.py b/readthedocs/config/tests/test_config.py index f7a7f0023dd..25712bef288 100644 --- a/readthedocs/config/tests/test_config.py +++ b/readthedocs/config/tests/test_config.py @@ -16,8 +16,8 @@ BuildConfigV1, BuildConfigV2, ConfigError, - ConfigFileNotFound, ConfigOptionNotSupportedError, + DefaultConfigFileNotFound, InvalidConfig, load, ) @@ -80,7 +80,7 @@ def get_build_config(config, env_config=None, source_file='readthedocs.yml'): def test_load_no_config_file(tmpdir, files): apply_fs(tmpdir, files) base = str(tmpdir) - with raises(ConfigFileNotFound) as e: + with raises(DefaultConfigFileNotFound) as e: with override_settings(DOCROOT=tmpdir): load(base, {}) assert e.value.code == CONFIG_FILE_REQUIRED @@ -196,6 +196,67 @@ def test_build_config_has_source_file(tmpdir): assert build.source_file == os.path.join(base, 'readthedocs.yml') +def test_load_non_default_filename(tmpdir): + """ + Load a config file name with a non-default name. + + Verifies that we can load a custom config path and that an existing default config file is + correctly ignored. + + Note: Our CharField validator for readthedocs_yaml_path currently ONLY allows a file to be + called .readthedocs.yaml. + This test just verifies that the loader doesn't care since we support different file names + in the backend. + """ + non_default_filename = "myconfig.yaml" + apply_fs( + tmpdir, + { + non_default_filename: textwrap.dedent( + """ + version: 2 + """ + ), + ".readthedocs.yaml": "illegal syntax but should not load", + }, + ) + base = str(tmpdir) + with override_settings(DOCROOT=tmpdir): + build = load(base, {}, readthedocs_yaml_path="myconfig.yaml") + assert isinstance(build, BuildConfigV2) + assert build.source_file == os.path.join(base, non_default_filename) + + +def test_load_non_yaml_extension(tmpdir): + """ + Load a config file name from non-default path. + + In this version, we verify that we can handle non-yaml extensions + because we allow the user to do that. + + See docstring of test_load_non_default_filename. + """ + non_default_filename = ".readthedocs.skrammel" + apply_fs( + tmpdir, + { + "subdir": { + non_default_filename: textwrap.dedent( + """ + version: 2 + """ + ), + }, + ".readthedocs.yaml": "illegal syntax but should not load", + }, + ) + base = str(tmpdir) + with override_settings(DOCROOT=tmpdir): + build = load(base, {}, readthedocs_yaml_path="subdir/.readthedocs.skrammel") + assert isinstance(build, BuildConfigV2) + assert build.source_file == os.path.join(base, "subdir/.readthedocs.skrammel") + + def test_build_config_has_list_with_single_empty_value(tmpdir): base = str(apply_fs( tmpdir, { diff --git a/readthedocs/doc_builder/backends/mkdocs.py b/readthedocs/doc_builder/backends/mkdocs.py index 0d0174176d0..2831d456ee5 100644 --- a/readthedocs/doc_builder/backends/mkdocs.py +++ b/readthedocs/doc_builder/backends/mkdocs.py @@ -45,6 +45,8 @@ class BaseMkdocs(BaseBuilder): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + + # This is the *MkDocs* yaml file self.yaml_file = self.get_yaml_config() # README: historically, the default theme was ``readthedocs`` but in diff --git a/readthedocs/doc_builder/config.py b/readthedocs/doc_builder/config.py index 34b29c589c4..3b22c04c2e4 100644 --- a/readthedocs/doc_builder/config.py +++ b/readthedocs/doc_builder/config.py @@ -2,19 +2,23 @@ from os import path -from readthedocs.config import BuildConfigV1, ConfigFileNotFound +from readthedocs.config import BuildConfigV1 from readthedocs.config import load as load_config from readthedocs.projects.models import ProjectConfigurationError +from ..config.config import DefaultConfigFileNotFound from .constants import DOCKER_IMAGE, DOCKER_IMAGE_SETTINGS -def load_yaml_config(version): +def load_yaml_config(version, readthedocs_yaml_path=None): """ - Load a configuration from `readthedocs.yml` file. + Load a build configuration file (`.readthedocs.yaml`). This uses the configuration logic from `readthedocs-build`, which will keep parsing consistent between projects. + + :param readthedocs_yaml_path: Optionally, we are told which readthedocs_yaml_path to + load instead of using defaults. """ checkout_path = version.project.checkout_path(version.slug) project = version.project @@ -56,8 +60,9 @@ def load_yaml_config(version): config = load_config( path=checkout_path, env_config=env_config, + readthedocs_yaml_path=readthedocs_yaml_path, ) - except ConfigFileNotFound: + except DefaultConfigFileNotFound: # Default to use v1 with some defaults from the web interface # if we don't find a configuration file. config = BuildConfigV1( diff --git a/readthedocs/doc_builder/director.py b/readthedocs/doc_builder/director.py index 3a7ba1ce7bf..662ff2b1c4d 100644 --- a/readthedocs/doc_builder/director.py +++ b/readthedocs/doc_builder/director.py @@ -1,3 +1,12 @@ +""" +The ``director`` module can be seen as the entrypoint of the build process. + +It "directs" all of the high-level build jobs: + +* checking out the repo +* setting up the environment +* fetching instructions etc. +""" import os import tarfile @@ -197,17 +206,34 @@ def build(self): # VCS checkout def checkout(self): - log.info( - "Clonning repository.", - ) + """Checkout Git repo and load build config file.""" + + log.info("Cloning repository.") self.vcs_repository.update() identifier = self.data.build_commit or self.data.version.identifier log.info("Checking out.", identifier=identifier) self.vcs_repository.checkout(identifier) - self.data.config = load_yaml_config(version=self.data.version) + # The director is responsible for understanding which config file to use for a build. + # In order to reproduce a build 1:1, we may use readthedocs_yaml_path defined by the build + # instead of per-version or per-project. + # Use the below line to fetch the readthedocs_yaml_path defined per-build. + # custom_config_file = self.data.build.get("readthedocs_yaml_path", None) + custom_config_file = None + + # This logic can be extended with version-specific config files + if not custom_config_file and self.data.version.project.readthedocs_yaml_path: + custom_config_file = self.data.version.project.readthedocs_yaml_path + + if custom_config_file: + log.info("Using a custom .readthedocs.yaml file.", path=custom_config_file) + self.data.config = load_yaml_config( + version=self.data.version, + readthedocs_yaml_path=custom_config_file, + ) self.data.build["config"] = self.data.config.as_dict() + self.data.build["readthedocs_yaml_path"] = custom_config_file if self.vcs_repository.supports_submodules: self.vcs_repository.update_submodules(self.data.config) @@ -360,6 +386,7 @@ def check_old_output_directory(self): raise BuildUserError(BuildUserError.BUILD_OUTPUT_OLD_DIRECTORY_USED) def run_build_commands(self): + """Runs each build command in the build environment.""" reshim_commands = ( {"pip", "install"}, {"conda", "create"}, diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 5a197efa0fd..a962cb5e90e 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -198,15 +198,16 @@ class ProjectAdvancedForm(ProjectTriggerBuildMixin, ProjectForm): class Meta: model = Project per_project_settings = ( - 'default_version', - 'default_branch', - 'privacy_level', - 'analytics_code', - 'analytics_disabled', - 'show_version_warning', - 'single_version', - 'external_builds_enabled', - 'external_builds_privacy_level', + "default_version", + "default_branch", + "privacy_level", + "analytics_code", + "analytics_disabled", + "show_version_warning", + "single_version", + "external_builds_enabled", + "external_builds_privacy_level", + "readthedocs_yaml_path", ) # These that can be set per-version using a config file. per_version_settings = ( @@ -347,8 +348,8 @@ def can_build_external_versions(self, integrations): return False def clean_conf_py_file(self): - filename = self.cleaned_data.get('conf_py_file', '').strip() - if filename and 'conf.py' not in filename: + filename = self.cleaned_data.get("conf_py_file", "").strip() + if filename and "conf.py" not in filename: raise forms.ValidationError( _( 'Your configuration file is invalid, make sure it contains ' @@ -357,6 +358,17 @@ def clean_conf_py_file(self): ) # yapf: disable return filename + def clean_readthedocs_yaml_path(self): + """ + Validate user input to help user. + + We also validate this path during the build process, so this validation step is + only considered as helpful to a user, not a security measure. + """ + filename = self.cleaned_data.get("readthedocs_yaml_path") + filename = (filename or "").strip() + return filename + def get_all_active_versions(self): """ Returns all active versions. @@ -379,7 +391,9 @@ class UpdateProjectForm( ProjectExtraForm, ): - class Meta: + """Basic project settings form for Admin.""" + + class Meta: # noqa model = Project fields = ( # Basics @@ -395,6 +409,7 @@ class Meta: ) def clean_language(self): + """Ensure that language isn't already active.""" language = self.cleaned_data['language'] project = self.instance if project: @@ -528,6 +543,8 @@ def save(self): class WebHookForm(forms.ModelForm): + """Webhook form.""" + project = forms.CharField(widget=forms.HiddenInput(), required=False) class Meta: @@ -595,6 +612,8 @@ def get_choices(self): ) for project in self.get_translation_queryset().all()] def clean_project(self): + """Ensures that selected project is valid as a translation.""" + translation_project_slug = self.cleaned_data['project'] # Ensure parent project isn't already itself a translation @@ -719,6 +738,7 @@ def clean_project(self): return self.project def clean_domain(self): + """Validates domain.""" domain = self.cleaned_data['domain'].lower() parsed = urlparse(domain) @@ -853,6 +873,7 @@ def clean_project(self): return self.project def clean_name(self): + """Validate environment variable name chosen.""" name = self.cleaned_data['name'] if name.startswith('__'): raise forms.ValidationError( diff --git a/readthedocs/projects/migrations/0100_project_readthedocs_yaml_path.py b/readthedocs/projects/migrations/0100_project_readthedocs_yaml_path.py new file mode 100644 index 00000000000..958b5d9cce0 --- /dev/null +++ b/readthedocs/projects/migrations/0100_project_readthedocs_yaml_path.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.18 on 2023-04-04 15:10 + +from django.db import migrations, models + +import readthedocs.projects.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0099_alter_domain_https"), + ] + + operations = [ + migrations.AddField( + model_name="historicalproject", + name="readthedocs_yaml_path", + field=models.CharField( + blank=True, + default=None, + help_text="Warning: experimental feature. Custom path from repository top-level to your .readthedocs.yaml, ex. subpath/docs/.readthedocs.yaml. Leave blank for default value: .readthedocs.yaml.", + max_length=1024, + null=True, + validators=[readthedocs.projects.validators.validate_build_config_file], + verbose_name="Path for .readthedocs.yaml", + ), + ), + migrations.AddField( + model_name="project", + name="readthedocs_yaml_path", + field=models.CharField( + blank=True, + default=None, + help_text="Warning: experimental feature. Custom path from repository top-level to your .readthedocs.yaml, ex. subpath/docs/.readthedocs.yaml. Leave blank for default value: .readthedocs.yaml.", + max_length=1024, + null=True, + validators=[readthedocs.projects.validators.validate_build_config_file], + verbose_name="Path for .readthedocs.yaml", + ), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index d4697c69fcd..c18a5376df5 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -44,6 +44,7 @@ ) from readthedocs.projects.templatetags.projects_tags import sort_version_aware from readthedocs.projects.validators import ( + validate_build_config_file, validate_domain_name, validate_no_ip, validate_repository_url, @@ -353,15 +354,30 @@ class Project(models.Model): conf_py_file = models.CharField( _('Python configuration file'), max_length=255, - default='', + default="", blank=True, help_text=_( - 'Path from project root to conf.py file ' - '(ex. docs/conf.py). ' - 'Leave blank if you want us to find it for you.', + "Path from project root to conf.py file " + "(ex. docs/conf.py). " + "Leave blank if you want us to find it for you.", ), ) + readthedocs_yaml_path = models.CharField( + _("Path for .readthedocs.yaml"), + max_length=1024, + default=None, + blank=True, + null=True, + help_text=_( + "Warning: experimental feature. " + "Custom path from repository top-level to your .readthedocs.yaml, " + "ex. subpath/docs/.readthedocs.yaml. " + "Leave blank for default value: .readthedocs.yaml.", + ), + validators=[validate_build_config_file], + ) + featured = models.BooleanField(_('Featured'), default=False) skip = models.BooleanField(_("Skip (disable) building this project"), default=False) @@ -857,7 +873,7 @@ def artifact_path(self, type_, version=LATEST): return os.path.join(self.checkout_path(version=version), "_readthedocs", type_) def conf_file(self, version=LATEST): - """Find a ``conf.py`` file in the project checkout.""" + """Find a Sphinx ``conf.py`` file in the project checkout.""" if self.conf_py_file: conf_path = os.path.join( self.checkout_path(version), diff --git a/readthedocs/projects/tasks/builds.py b/readthedocs/projects/tasks/builds.py index d1b4f31a6d1..38b357be785 100644 --- a/readthedocs/projects/tasks/builds.py +++ b/readthedocs/projects/tasks/builds.py @@ -103,10 +103,8 @@ class TaskData: build_commit: str = None start_time: timezone.datetime = None - # pylint: disable=unsubscriptable-object environment_class: type[DockerBuildEnvironment] | type[LocalBuildEnvironment] = None - build_director: BuildDirector = None config: BuildConfigV1 | BuildConfigV2 = None project: APIProject = None diff --git a/readthedocs/projects/tests/mockers.py b/readthedocs/projects/tests/mockers.py index 471fc00008a..c86d27b6c8c 100644 --- a/readthedocs/projects/tests/mockers.py +++ b/readthedocs/projects/tests/mockers.py @@ -36,6 +36,16 @@ def stop(self): for k, m in self.patches.items(): m.stop() + def add_file_in_repo_checkout(self, path, content): + """ + A quick way to emulate that a file is in the repo. + + Does not change git data. + """ + destination = os.path.join(self.project_repository_path, path) + open(destination, "w").write(content) + return destination + def _mock_artifact_builders(self): # TODO: save the mock instances to be able to check them later # self.patches['builder.localmedia.move'] = mock.patch( @@ -116,6 +126,11 @@ def _mock_git_repository(self): # TODO: improve this self._counter = 0 + + # The tmp project repository should be at a unique location, but we need + # to hook into test setup and teardown such that we can clean up nicely. + # This probably means that the tmp dir should be handed to the mocker from + # outside. self.project_repository_path = '/tmp/readthedocs-tests/git-repository' shutil.rmtree(self.project_repository_path, ignore_errors=True) os.makedirs(self.project_repository_path) diff --git a/readthedocs/projects/tests/test_build_tasks.py b/readthedocs/projects/tests/test_build_tasks.py index 7c385a837a6..f64e5acfba1 100644 --- a/readthedocs/projects/tests/test_build_tasks.py +++ b/readthedocs/projects/tests/test_build_tasks.py @@ -33,12 +33,7 @@ def setup(self, requests_mock): # Save the reference to query it from inside the test self.requests_mock = requests_mock - self.project = fixture.get( - Project, - slug="project", - enable_epub_build=True, - enable_pdf_build=True, - ) + self.project = self._get_project() self.version = self.project.versions.get(slug="latest") self.build = fixture.get( Build, @@ -59,6 +54,14 @@ def setup(self, requests_mock): # tearDown self.mocker.stop() + def _get_project(self): + return fixture.get( + Project, + slug="project", + enable_epub_build=True, + enable_pdf_build=True, + ) + def _trigger_update_docs_task(self): # NOTE: is it possible to replace calling this directly by `trigger_build` instead? :) return update_docs_task.delay( @@ -77,6 +80,86 @@ def _config_file(self, config): return config +class TestCustomConfigFile(BuildEnvironmentBase): + + # Relative path to where a custom config file is assumed to exist in repo + config_file_name = "unique.yaml" + + def _get_project(self): + return fixture.get( + Project, + slug="project", + enable_epub_build=False, + enable_pdf_build=False, + readthedocs_yaml_path=self.config_file_name, + ) + + def _config_file(self, config): + config = BuildConfigV2( + {}, + config, + source_file=self.config_file_name, + ) + config.validate() + return config + + @mock.patch("readthedocs.doc_builder.director.load_yaml_config") + @mock.patch("readthedocs.doc_builder.director.BuildDirector.build_docs_class") + def test_config_is_stored(self, build_docs_class, load_yaml_config): + """Test that a custom config file is stored""" + + # We add the PDF format to this config so we can check that the + # config file is in use + config = self._config_file( + { + "version": 2, + "formats": ["pdf"], + "sphinx": { + "configuration": "docs/conf.py", + }, + } + ) + load_yaml_config.return_value = config + build_docs_class.return_value = True # success + + assert not BuildData.objects.all().exists() + + self._trigger_update_docs_task() + + # Assert that the director tries to load the custom config file + load_yaml_config.assert_called_once_with( + version=mock.ANY, readthedocs_yaml_path=self.config_file_name + ) + + # Assert that we are building a PDF, since that is what our custom config file says + build_docs_class.assert_called_with("sphinx_pdf") + + @mock.patch("readthedocs.core.utils.filesystem._assert_path_is_inside_docroot") + @mock.patch("readthedocs.doc_builder.director.BuildDirector.build_docs_class") + def test_config_file_is_loaded( + self, build_docs_class, _assert_path_is_inside_docroot + ): + """Test that a custom config file is loaded + + The readthedocs_yaml_path field on Project should be loading the file that we add + to the repo.""" + + # While testing, we are unsure if temporary test files exist in the docroot + _assert_path_is_inside_docroot.return_value = True + + self.mocker.add_file_in_repo_checkout( + self.config_file_name, + "version: 2\n" + "formats: [pdf]\n" + "sphinx:\n" + " configuration: docs/conf.py", + ) + + self._trigger_update_docs_task() + + # Assert that we are building a PDF, since that is what our custom config file says + build_docs_class.assert_called_with("sphinx_pdf") + class TestBuildTask(BuildEnvironmentBase): @pytest.mark.parametrize( "formats,builders", @@ -390,7 +473,7 @@ def test_successful_build( # TODO: assert the verb and the path for each API call as well - # Update build state: clonning + # Update build state: cloning assert self.requests_mock.request_history[3].json() == { "id": 1, "state": "cloning", @@ -405,6 +488,7 @@ def test_successful_build( "state": "installing", "commit": "a1b2c3", "builder": mock.ANY, + "readthedocs_yaml_path": None, "error": "", # We update the `config` field at the same time we send the # `installing` state, to reduce one API call @@ -449,6 +533,7 @@ def test_successful_build( "id": 1, "state": "building", "commit": "a1b2c3", + "readthedocs_yaml_path": None, "config": mock.ANY, "builder": mock.ANY, "error": "", @@ -458,6 +543,7 @@ def test_successful_build( "id": 1, "state": "uploading", "commit": "a1b2c3", + "readthedocs_yaml_path": None, "config": mock.ANY, "builder": mock.ANY, "error": "", @@ -482,6 +568,7 @@ def test_successful_build( "id": 1, "state": "finished", "commit": "a1b2c3", + "readthedocs_yaml_path": None, "config": mock.ANY, "builder": mock.ANY, "length": mock.ANY, diff --git a/readthedocs/projects/tests/test_validators.py b/readthedocs/projects/tests/test_validators.py new file mode 100644 index 00000000000..87436bd5da1 --- /dev/null +++ b/readthedocs/projects/tests/test_validators.py @@ -0,0 +1,42 @@ +import pytest +from django.core.exceptions import ValidationError + +from readthedocs.projects import validators + + +def test_repository_path_validator(): + # Invalid stuff + with pytest.raises(ValidationError): + validators.validate_build_config_file("/absolute_path") + + with pytest.raises(ValidationError): + validators.validate_build_config_file("directory/") + + with pytest.raises(ValidationError): + validators.validate_build_config_file("../not_that") + + with pytest.raises(ValidationError): + validators.validate_build_config_file("../../../and_not_that/../") + + with pytest.raises(ValidationError): + validators.validate_build_config_file("'none_of_this'") + + with pytest.raises(ValidationError): + validators.validate_build_config_file('"and_none_of_this"') + + with pytest.raises(ValidationError): + validators.validate_build_config_file("nor_this.") + + with pytest.raises(ValidationError): + validators.validate_build_config_file(",you_probably_meant_to_use_a_dot") + + with pytest.raises(ValidationError): + validators.validate_build_config_file(".readthedocs.uml") + + # Valid stuff + validators.validate_build_config_file("this/is/okay/.readthedocs.yaml") + validators.validate_build_config_file("thiS/Is/oKay/.readthedocs.yaml") + validators.validate_build_config_file("this is okay/.readthedocs.yaml") + validators.validate_build_config_file("this_is_okay/.readthedocs.yaml") + validators.validate_build_config_file("this-is-okay/.readthedocs.yaml") + validators.validate_build_config_file(".readthedocs.yaml") diff --git a/readthedocs/projects/validators.py b/readthedocs/projects/validators.py index 42c1abf8364..fdeb0d6aa66 100644 --- a/readthedocs/projects/validators.py +++ b/readthedocs/projects/validators.py @@ -7,6 +7,7 @@ from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.utils.deconstruct import deconstructible +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -98,3 +99,67 @@ class SubmoduleURLValidator(RepositoryURLValidator): validate_repository_url = RepositoryURLValidator() validate_submodule_url = SubmoduleURLValidator() + + +def validate_build_config_file(path): + """ + Validate that user input is a good relative repository path. + + By 'good', we mean that it's a valid unix path, + but not all valid unix paths are good repository paths. + + This validator checks for common mistakes. + """ + invalid_characters = "[]{}()`'\"\\%&<>|," + valid_filenames = [".readthedocs.yaml"] + + if path.startswith("/"): + raise ValidationError( + _( + "Use a relative path. It should not begin with '/'. " + "The path is relative to the root of your repository." + ), + code="path_invalid", + ) + if path.endswith("/"): + raise ValidationError( + _("The path cannot end with '/', as it cannot be a directory."), + code="path_invalid", + ) + if ".." in path: + raise ValidationError( + _("Found invalid sequence in path: '..'"), + code="path_invalid", + ) + if any(ch in path for ch in invalid_characters): + raise ValidationError( + mark_safe( + _( + "Found invalid character. Avoid these characters: " + "{invalid_characters}" + ).format(invalid_characters=invalid_characters), + ), + code="path_invalid", + ) + + is_valid = any(fn == path for fn in valid_filenames) or any( + path.endswith(f"/{fn}") for fn in valid_filenames + ) + if not is_valid and len(valid_filenames) == 1: + raise ValidationError( + mark_safe( + _("The only allowed filename is {filename}.").format( + filename=valid_filenames[0] + ), + ), + code="path_invalid", + ) + if not is_valid: + raise ValidationError( + mark_safe( + _("The only allowed filenames are {filenames}.").format( + filenames=", ".join(valid_filenames) + ), + ), + code="path_invalid", + ) diff --git a/readthedocs/rtd_tests/tests/test_api.py b/readthedocs/rtd_tests/tests/test_api.py index 294a70f496f..9719903b9b7 100644 --- a/readthedocs/rtd_tests/tests/test_api.py +++ b/readthedocs/rtd_tests/tests/test_api.py @@ -705,19 +705,27 @@ class APITests(TestCase): def test_user_doesnt_get_full_api_return(self): user_normal = get(User, is_staff=False) user_admin = get(User, is_staff=True) - project = get(Project, main_language_project=None, conf_py_file='foo') + project = get( + Project, + main_language_project=None, + conf_py_file="foo", + readthedocs_yaml_path="bar", + ) client = APIClient() client.force_authenticate(user=user_normal) resp = client.get('/api/v2/project/%s/' % (project.pk)) self.assertEqual(resp.status_code, 200) - self.assertNotIn('conf_py_file', resp.data) + self.assertNotIn("conf_py_file", resp.data) + self.assertNotIn("readthedocs_yaml_path", resp.data) client.force_authenticate(user=user_admin) resp = client.get('/api/v2/project/%s/' % (project.pk)) self.assertEqual(resp.status_code, 200) - self.assertIn('conf_py_file', resp.data) - self.assertEqual(resp.data['conf_py_file'], 'foo') + self.assertIn("conf_py_file", resp.data) + self.assertEqual(resp.data["conf_py_file"], "foo") + self.assertIn("readthedocs_yaml_path", resp.data) + self.assertEqual(resp.data["readthedocs_yaml_path"], "bar") def test_project_features(self): user = get(User, is_staff=True) @@ -2478,6 +2486,7 @@ def test_get_version_by_id(self): "repo": "https://github.com/pypa/pip", "repo_type": "git", "requirements_file": None, + "readthedocs_yaml_path": None, "show_advertising": True, "skip": False, "slug": "pip", diff --git a/readthedocs/rtd_tests/tests/test_config_integration.py b/readthedocs/rtd_tests/tests/test_config_integration.py index 9ffd8db958a..faedbef54ac 100644 --- a/readthedocs/rtd_tests/tests/test_config_integration.py +++ b/readthedocs/rtd_tests/tests/test_config_integration.py @@ -23,7 +23,7 @@ def create_load(config=None): if config is None: config = {} - def inner(path=None, env_config=None): + def inner(path=None, env_config=None, readthedocs_yaml_path=None): env_config_defaults = { 'output_base': '', 'name': '1', @@ -97,8 +97,9 @@ def test_python_supported_versions_default_image_1_0(self, load_config): expected_env_config.update(img_settings) load_config.assert_called_once_with( - path=mock.ANY, - env_config=expected_env_config, + path=mock.ANY, + env_config=expected_env_config, + readthedocs_yaml_path=None, ) self.assertEqual(config.python.version, '3') diff --git a/readthedocs/rtd_tests/tests/test_project_forms.py b/readthedocs/rtd_tests/tests/test_project_forms.py index efc2c9b87bf..b9413763540 100644 --- a/readthedocs/rtd_tests/tests/test_project_forms.py +++ b/readthedocs/rtd_tests/tests/test_project_forms.py @@ -258,6 +258,24 @@ def test_can_update_privacy_level(self): self.assertTrue(form.is_valid()) self.assertEqual(self.project.privacy_level, PRIVATE) + @override_settings(ALLOW_PRIVATE_REPOS=False) + def test_custom_readthedocs_yaml(self): + custom_readthedocs_yaml_path = "folder/.readthedocs.yaml" + form = ProjectAdvancedForm( + { + "default_version": LATEST, + "documentation_type": SPHINX, + "python_interpreter": "python3", + "privacy_level": PRIVATE, + "readthedocs_yaml_path": custom_readthedocs_yaml_path, + }, + instance=self.project, + ) + # The form is valid, but the field is ignored + self.assertTrue(form.is_valid()) + self.assertEqual(self.project.privacy_level, PUBLIC) + project = form.save() + self.assertEqual(project.readthedocs_yaml_path, custom_readthedocs_yaml_path) class TestProjectAdvancedFormDefaultBranch(TestCase):