diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..1b693ca --- /dev/null +++ b/404.html @@ -0,0 +1,282 @@ + + + + + + +Nick's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + +
+

404

+ +

Page not found :(

+

The requested page could not be found.

+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..e392ae4 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,60 @@ +.tab { + display: flex; + flex-wrap: wrap; + /* margin-left: -20px; */ + padding: 0; + list-style: none; + position: relative; + /* border: 1px solid #a0a0a0; */ + /* border-radius: 5px; */ + border-bottom: 1px solid #a0a0a0; + border-top: 1px solid #a0a0a0; +} + +.tab > * { + flex: none; + padding-left: 20px; + position: relative; +} + +.tab > * > a { + display: block; + text-align: center; + padding: 9px 20px; + color: #a0a0a0; + border-bottom: 2px solid transparent; + border-bottom-color: transparent; + font-size: 18px; + /* text-transform: uppercase; */ + transition: color 0.25s ease-in-out; + line-height: 20px; +} + +.tab > :hover > a { + color: #eaeaea; + border-color: #eaeaea; +} + +.tab > .active > a { + color: #00adb5; + border-color: #00adb5; +} + +.tab li a { + text-decoration: none; + cursor: pointer; +} + +.tab-content { + padding: 0; + /* border: 1px solid #a0a0a0; */ + border-radius: 5px; +} + +.tab-content li { + display: none; +} + +.tab-content li.active { + display: initial; +} diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..8324d93 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,5 @@ +.author__urls.social-icons i,.author__urls.social-icons .svg-inline--fa,.page__footer-follow .social-icons i,.page__footer-follow .social-icons .svg-inline--fa{color:inherit}.ais-search-box .ais-search-box--input{background-color:#1f242c}/*! + * Minimal Mistakes Jekyll Theme 4.24.0 by Michael Rose + * Copyright 2013-2020 Michael Rose - mademistakes.com | @mmistakes + * Licensed under MIT (https://github.com/mmistakes/minimal-mistakes/blob/master/LICENSE) +*/.mfp-counter{font-family:Georgia,Times,serif}.mfp-bg{top:0;left:0;width:100%;height:100%;z-index:1042;overflow:hidden;position:fixed;background:#000;opacity:.8;filter:alpha(opacity=80)}.mfp-wrap{top:0;left:0;width:100%;height:100%;z-index:1043;position:fixed;outline:none !important;-webkit-backface-visibility:hidden}.mfp-container{text-align:center;position:absolute;width:100%;height:100%;left:0;top:0;padding:0 8px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.mfp-container:before{content:'';display:inline-block;height:100%;vertical-align:middle}.mfp-align-top .mfp-container:before{display:none}.mfp-content{position:relative;display:inline-block;vertical-align:middle;margin:0 auto;text-align:left;z-index:1045}.mfp-inline-holder .mfp-content,.mfp-ajax-holder .mfp-content{width:100%;cursor:auto}.mfp-ajax-cur{cursor:progress}.mfp-zoom-out-cur,.mfp-zoom-out-cur .mfp-image-holder .mfp-close{cursor:-moz-zoom-out;cursor:-webkit-zoom-out;cursor:zoom-out}.mfp-zoom{cursor:pointer;cursor:-webkit-zoom-in;cursor:-moz-zoom-in;cursor:zoom-in}.mfp-auto-cursor .mfp-content{cursor:auto}.mfp-close,.mfp-arrow,.mfp-preloader,.mfp-counter{-webkit-user-select:none;-moz-user-select:none;user-select:none}.mfp-loading.mfp-figure{display:none}.mfp-hide{display:none !important}.mfp-preloader{color:#ccc;position:absolute;top:50%;width:auto;text-align:center;margin-top:-0.8em;left:8px;right:8px;z-index:1044}.mfp-preloader a{color:#ccc}.mfp-preloader a:hover{color:#fff}.mfp-s-ready .mfp-preloader{display:none}.mfp-s-error .mfp-content{display:none}button.mfp-close,button.mfp-arrow{overflow:visible;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;display:block;outline:none;padding:0;z-index:1046;-webkit-box-shadow:none;box-shadow:none}button::-moz-focus-inner{padding:0;border:0}.mfp-close{width:44px;height:44px;line-height:44px;position:absolute;right:0;top:0;text-decoration:none;text-align:center;opacity:1;filter:alpha(opacity=100);padding:0 0 18px 10px;color:#fff;font-style:normal;font-size:28px;font-family:Georgia,Times,serif}.mfp-close:hover,.mfp-close:focus{opacity:1;filter:alpha(opacity=100)}.mfp-close:active{top:1px}.mfp-close-btn-in .mfp-close{color:#fff}.mfp-image-holder .mfp-close,.mfp-iframe-holder .mfp-close{color:#fff;right:-6px;text-align:right;padding-right:6px;width:100%}.mfp-counter{position:absolute;top:0;right:0;color:#ccc;font-size:12px;line-height:18px}.mfp-arrow{position:absolute;opacity:1;filter:alpha(opacity=100);margin:0;top:50%;margin-top:-55px;padding:0;width:90px;height:110px;-webkit-tap-highlight-color:transparent}.mfp-arrow:active{margin-top:-54px}.mfp-arrow:hover,.mfp-arrow:focus{opacity:1;filter:alpha(opacity=100)}.mfp-arrow:before,.mfp-arrow:after,.mfp-arrow .mfp-b,.mfp-arrow .mfp-a{content:'';display:block;width:0;height:0;position:absolute;left:0;top:0;margin-top:35px;margin-left:35px;border:medium inset transparent}.mfp-arrow:after,.mfp-arrow .mfp-a{border-top-width:13px;border-bottom-width:13px;top:8px}.mfp-arrow:before,.mfp-arrow .mfp-b{border-top-width:21px;border-bottom-width:21px;opacity:0.7}.mfp-arrow-left{left:0}.mfp-arrow-left:after,.mfp-arrow-left .mfp-a{border-right:17px solid #fff;margin-left:31px}.mfp-arrow-left:before,.mfp-arrow-left .mfp-b{margin-left:25px;border-right:27px solid #fff}.mfp-arrow-right{right:0}.mfp-arrow-right:after,.mfp-arrow-right .mfp-a{border-left:17px solid #fff;margin-left:39px}.mfp-arrow-right:before,.mfp-arrow-right .mfp-b{border-left:27px solid #fff}.mfp-iframe-holder{padding-top:40px;padding-bottom:40px}.mfp-iframe-holder .mfp-content{line-height:0;width:100%;max-width:900px}.mfp-iframe-holder .mfp-close{top:-40px}.mfp-iframe-scaler{width:100%;height:0;overflow:hidden;padding-top:56.25%}.mfp-iframe-scaler iframe{position:absolute;display:block;top:0;left:0;width:100%;height:100%;box-shadow:0 0 8px rgba(0,0,0,0.6);background:#000}img.mfp-img{width:auto;max-width:100%;height:auto;display:block;line-height:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:40px 0 40px;margin:0 auto}.mfp-figure{line-height:0}.mfp-figure:after{content:'';position:absolute;left:0;top:40px;bottom:40px;display:block;right:0;width:auto;height:auto;z-index:-1;box-shadow:0 0 8px rgba(0,0,0,0.6);background:#444}.mfp-figure small{color:#bdbdbd;display:block;font-size:12px;line-height:14px}.mfp-figure figure{margin:0}.mfp-figure figcaption{margin-top:0;margin-bottom:0}.mfp-bottom-bar{margin-top:-36px;position:absolute;top:100%;left:0;width:100%;cursor:auto}.mfp-title{text-align:left;line-height:18px;color:#f3f3f3;word-wrap:break-word;padding-right:36px}.mfp-image-holder .mfp-content{max-width:100%}.mfp-gallery .mfp-image-holder .mfp-figure{cursor:pointer}@media screen and (max-width: 800px) and (orientation: landscape), screen and (max-height: 300px){.mfp-img-mobile .mfp-image-holder{padding-left:0;padding-right:0}.mfp-img-mobile img.mfp-img{padding:0}.mfp-img-mobile .mfp-figure:after{top:0;bottom:0}.mfp-img-mobile .mfp-figure small{display:inline;margin-left:5px}.mfp-img-mobile .mfp-bottom-bar{background:rgba(0,0,0,0.6);bottom:0;margin:0;top:auto;padding:3px 5px;position:fixed;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.mfp-img-mobile .mfp-bottom-bar:empty{padding:0}.mfp-img-mobile .mfp-counter{right:5px;top:3px}.mfp-img-mobile .mfp-close{top:0;right:0;width:35px;height:35px;line-height:35px;background:rgba(0,0,0,0.6);position:fixed;text-align:center;padding:0}}@media all and (max-width: 900px){.mfp-arrow{-webkit-transform:scale(0.75);transform:scale(0.75)}.mfp-arrow-left{-webkit-transform-origin:0;transform-origin:0}.mfp-arrow-right{-webkit-transform-origin:100%;transform-origin:100%}.mfp-container{padding-left:6px;padding-right:6px}}.mfp-ie7 .mfp-img{padding:0}.mfp-ie7 .mfp-bottom-bar{width:600px;left:50%;margin-left:-300px;margin-top:5px;padding-bottom:5px}.mfp-ie7 .mfp-container{padding:0}.mfp-ie7 .mfp-content{padding-top:44px}.mfp-ie7 .mfp-close{top:0;right:0;padding-top:0}a:focus,button:focus{outline:thin dotted #00adb5;outline:5px auto #00adb5;outline-offset:-2px}*{box-sizing:border-box}html{box-sizing:border-box;background-color:#252a34;font-size:16px;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}@media (min-width: 48em){html{font-size:18px}}@media (min-width: 64em){html{font-size:20px}}@media (min-width: 80em){html{font-size:22px}}body{margin:0}::-moz-selection{color:#fff;background:#000}::selection{color:#fff;background:#000}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}a{color:#8cd2d5}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{max-width:100%;width:auto\9;height:auto;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{box-sizing:border-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}html{position:relative;min-height:100%}body{margin:0;padding:0;color:#eaeaea;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;line-height:1.5}body.overflow--hidden{overflow:hidden}h1,h2,h3,h4,h5,h6{margin:2em 0 0.5em;line-height:1.2;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:bold}h1{margin-top:0;font-size:1.563em}h2{font-size:1.25em}h3{font-size:1.125em}h4{font-size:1.0625em}h5{font-size:1.03125em}h6{font-size:1em}small,.small{font-size:.75em}p{margin-bottom:1.3em}u,ins{text-decoration:none;border-bottom:1px solid #eaeaea}u a,ins a{color:inherit}del a{color:inherit}p,pre,blockquote,ul,ol,dl,figure,table,fieldset{orphans:3;widows:3}abbr[title],abbr[data-original-title]{text-decoration:none;cursor:help;border-bottom:1px dotted #eaeaea}blockquote{margin:2em 1em 2em 0;padding-left:1em;padding-right:1em;font-style:italic;border-left:0.25em solid #00adb5}blockquote cite{font-style:italic}blockquote cite:before{content:"\2014";padding-right:5px}a:visited{color:#699ea0}a:hover{color:#a9dde0;outline:0}tt,code,kbd,samp,pre{font-family:Monaco,Consolas,"Lucida Console",monospace}pre{overflow-x:auto}p>code,a>code,li>code,figcaption>code,td>code{padding-top:0.1rem;padding-bottom:0.1rem;font-size:0.8em;background:#1f242c;border-radius:4px}p>code:before,p>code:after,a>code:before,a>code:after,li>code:before,li>code:after,figcaption>code:before,figcaption>code:after,td>code:before,td>code:after{letter-spacing:-0.2em;content:"\00a0"}hr{display:block;margin:1em 0;border:0;border-top:1px solid #51555d}ul li,ol li{margin-bottom:0.5em}li ul,li ol{margin-top:0.5em}figure{display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;-webkit-box-align:start;align-items:flex-start;flex-wrap:wrap;margin:2em 0}figure img,figure iframe,figure .fluid-width-video-wrapper{margin-bottom:1em}figure img{width:100%;border-radius:4px;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}figure>a{display:block}@media (min-width: 37.5em){figure.half>a,figure.half>img{width:calc(50% - 0.5em)}}figure.half figcaption{width:100%}@media (min-width: 37.5em){figure.third>a,figure.third>img{width:calc(33.3333% - 0.5em)}}figure.third figcaption{width:100%}figcaption{margin-bottom:0.5em;color:#eee;font-family:Georgia,Times,serif;font-size:.75em}figcaption a{-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}figcaption a:hover{color:#a9dde0}svg:not(:root){overflow:hidden}nav ul{margin:0;padding:0}nav li{list-style:none}nav a{text-decoration:none}nav ul li,nav ol li{margin-bottom:0}nav li ul,nav li ol{margin-top:0}b,i,strong,em,blockquote,p,q,span,figure,img,h1,h2,header,input,a,tr,td,form button,input[type="submit"],.btn,.highlight,.archive__item-teaser{-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}form{margin:0 0 5px 0;padding:1em;background-color:#1f242c}form fieldset{margin-bottom:5px;padding:0;border-width:0}form legend{display:block;width:100%;margin-bottom:10px;*margin-left:-7px;padding:0;color:#eaeaea;border:0;white-space:normal}form p{margin-bottom:2.5px}form ul{list-style-type:none;margin:0 0 5px 0;padding:0}form br{display:none}label,input,button,select,textarea{vertical-align:baseline;*vertical-align:middle}input,button,select,textarea{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}label{display:block;margin-bottom:0.25em;color:#eaeaea;cursor:pointer}label small{font-size:.75em}label input,label textarea,label select{display:block}input,textarea,select{display:inline-block;width:100%;padding:0.25em;margin-bottom:0.5em;color:#eaeaea;background-color:#252a34;border:#51555d;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,0.125)}.input-mini{width:60px}.input-small{width:90px}input[type="image"],input[type="checkbox"],input[type="radio"]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;border-radius:0;border:0 \9;box-shadow:none}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="image"]{border:0}input[type="file"]{width:auto;padding:initial;line-height:initial;border:initial;background-color:transparent;background-color:initial;box-shadow:none}input[type="button"],input[type="reset"],input[type="submit"]{width:auto;height:auto;cursor:pointer;*overflow:visible}select,input[type="file"]{*margin-top:4px}select{width:auto;background-color:#fff}select[multiple],select[size]{height:auto}textarea{resize:vertical;height:auto;overflow:auto;vertical-align:top}input[type="hidden"]{display:none}.form{position:relative}.radio,.checkbox{padding-left:18px;font-weight:normal}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{opacity:0.5;cursor:not-allowed}input:focus,textarea:focus{border-color:#00adb5;outline:0;outline:thin dotted \9;box-shadow:inset 0 1px 3px rgba(234,234,234,0.06),0 0 5px rgba(0,173,181,0.7)}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus,select:focus{box-shadow:none}.help-block,.help-inline{color:#eee}.help-block{display:block;margin-bottom:1em;line-height:1em}.help-inline{display:inline-block;vertical-align:middle;padding-left:5px}.form-group{margin-bottom:5px;padding:0;border-width:0}.form-inline input,.form-inline textarea,.form-inline select{display:inline-block;margin-bottom:0}.form-inline label{display:inline-block}.form-inline .radio,.form-inline .checkbox,.form-inline .radio{padding-left:0;margin-bottom:0;vertical-align:middle}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-left:0;margin-right:3px}.form-search input,.form-search textarea,.form-search select{display:inline-block;margin-bottom:0}.form-search .search-query{padding-left:14px;padding-right:14px;margin-bottom:0;border-radius:14px}.form-search label{display:inline-block}.form-search .radio,.form-search .checkbox,.form-inline .radio{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"]{float:left;margin-left:0;margin-right:3px}.form--loading:before{content:""}.form--loading .form__spinner{display:block}.form:before{position:absolute;top:0;left:0;width:100%;height:100%;background-color:rgba(255,255,255,0.7);z-index:10}.form__spinner{display:none;position:absolute;top:50%;left:50%;z-index:11}table{display:block;margin-bottom:1em;width:100%;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em;border-collapse:collapse;overflow-x:auto}table+table{margin-top:1em}thead{background-color:#51555d;border-bottom:2px solid #3d4046}th{padding:0.5em;font-weight:bold;text-align:left}td{padding:0.5em;border-bottom:1px solid #3d4046}tr,td,th{vertical-align:middle}@-webkit-keyframes intro{0%{opacity:0}100%{opacity:1}}@keyframes intro{0%{opacity:0}100%{opacity:1}}.btn{display:inline-block;margin-bottom:0.25em;padding:0.5em 1em;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em;font-weight:bold;text-align:center;text-decoration:none;border-width:0;border-radius:4px;cursor:pointer}.btn .icon{margin-right:0.5em}.btn .icon+.hidden{margin-left:-0.5em}.btn--primary{background-color:#00adb5;color:#fff}.btn--primary:visited{background-color:#00adb5;color:#fff}.btn--primary:hover{background-color:#008a91;color:#fff}.btn--inverse{background-color:#fff;color:#3d4144;border:1px solid #51555d}.btn--inverse:visited{background-color:#fff;color:#3d4144}.btn--inverse:hover{background-color:#ccc;color:#3d4144}.btn--light-outline{background-color:transparent;color:#fff;border:1px solid #fff}.btn--light-outline:visited{background-color:transparent;color:#fff}.btn--light-outline:hover{background-color:rgba(0,0,0,0.2);color:#fff}.btn--success{background-color:#3fa63f;color:#fff}.btn--success:visited{background-color:#3fa63f;color:#fff}.btn--success:hover{background-color:#328532;color:#fff}.btn--warning{background-color:#d67f05;color:#fff}.btn--warning:visited{background-color:#d67f05;color:#fff}.btn--warning:hover{background-color:#ab6604;color:#fff}.btn--danger{background-color:#ee5f5b;color:#fff}.btn--danger:visited{background-color:#ee5f5b;color:#fff}.btn--danger:hover{background-color:#be4c49;color:#fff}.btn--info{background-color:#3b9cba;color:#fff}.btn--info:visited{background-color:#3b9cba;color:#fff}.btn--info:hover{background-color:#2f7d95;color:#fff}.btn--facebook{background-color:#3b5998;color:#fff}.btn--facebook:visited{background-color:#3b5998;color:#fff}.btn--facebook:hover{background-color:#2f477a;color:#fff}.btn--twitter{background-color:#55acee;color:#fff}.btn--twitter:visited{background-color:#55acee;color:#fff}.btn--twitter:hover{background-color:#448abe;color:#fff}.btn--linkedin{background-color:#007bb6;color:#fff}.btn--linkedin:visited{background-color:#007bb6;color:#fff}.btn--linkedin:hover{background-color:#006292;color:#fff}.btn--block{display:block;width:100%}.btn--block+.btn--block{margin-top:0.25em}.btn--disabled{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);box-shadow:none;opacity:0.65}.btn--x-large{font-size:1.25em}.btn--large{font-size:1em}.btn--small{font-size:.6875em}.notice{margin:2em 0 !important;padding:1em;color:#eaeaea;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em !important;text-indent:initial;background-color:#434851;border-radius:4px;box-shadow:0 1px 1px rgba(189,193,196,0.25)}.notice h4{margin-top:0 !important;margin-bottom:0.75em;line-height:inherit}.page__content .notice h4{margin-bottom:0;font-size:1em}.notice p:last-child{margin-bottom:0 !important}.notice h4+p{margin-top:0;padding-top:0}.notice a{color:#aaaeb0}.notice a:hover{color:#5f6162}.notice code{background-color:#343942}.notice pre code{background-color:inherit}.notice ul:last-child{margin-bottom:0}.notice--primary{margin:2em 0 !important;padding:1em;color:#eaeaea;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em !important;text-indent:initial;background-color:#1e444e;border-radius:4px;box-shadow:0 1px 1px rgba(0,173,181,0.25)}.notice--primary h4{margin-top:0 !important;margin-bottom:0.75em;line-height:inherit}.page__content .notice--primary h4{margin-bottom:0;font-size:1em}.notice--primary p:last-child{margin-bottom:0 !important}.notice--primary h4+p{margin-top:0;padding-top:0}.notice--primary a{color:#009ca3}.notice--primary a:hover{color:#00575b}.notice--primary code{background-color:#213741}.notice--primary pre code{background-color:inherit}.notice--primary ul:last-child{margin-bottom:0}.notice--info{margin:2em 0 !important;padding:1em;color:#eaeaea;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em !important;text-indent:initial;background-color:#29414f;border-radius:4px;box-shadow:0 1px 1px rgba(59,156,186,0.25)}.notice--info h4{margin-top:0 !important;margin-bottom:0.75em;line-height:inherit}.page__content .notice--info h4{margin-bottom:0;font-size:1em}.notice--info p:last-child{margin-bottom:0 !important}.notice--info h4+p{margin-top:0;padding-top:0}.notice--info a{color:#358ca7}.notice--info a:hover{color:#1e4e5d}.notice--info code{background-color:#273541}.notice--info pre code{background-color:inherit}.notice--info ul:last-child{margin-bottom:0}.notice--warning{margin:2em 0 !important;padding:1em;color:#eaeaea;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em !important;text-indent:initial;background-color:#483b2b;border-radius:4px;box-shadow:0 1px 1px rgba(214,127,5,0.25)}.notice--warning h4{margin-top:0 !important;margin-bottom:0.75em;line-height:inherit}.page__content .notice--warning h4{margin-bottom:0;font-size:1em}.notice--warning p:last-child{margin-bottom:0 !important}.notice--warning h4+p{margin-top:0;padding-top:0}.notice--warning a{color:#c17205}.notice--warning a:hover{color:#6b4003}.notice--warning code{background-color:#37332f}.notice--warning pre code{background-color:inherit}.notice--warning ul:last-child{margin-bottom:0}.notice--success{margin:2em 0 !important;padding:1em;color:#eaeaea;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em !important;text-indent:initial;background-color:#2a4336;border-radius:4px;box-shadow:0 1px 1px rgba(63,166,63,0.25)}.notice--success h4{margin-top:0 !important;margin-bottom:0.75em;line-height:inherit}.page__content .notice--success h4{margin-bottom:0;font-size:1em}.notice--success p:last-child{margin-bottom:0 !important}.notice--success h4+p{margin-top:0;padding-top:0}.notice--success a{color:#399539}.notice--success a:hover{color:#205320}.notice--success code{background-color:#283635}.notice--success pre code{background-color:inherit}.notice--success ul:last-child{margin-bottom:0}.notice--danger{margin:2em 0 !important;padding:1em;color:#eaeaea;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em !important;text-indent:initial;background-color:#4d353c;border-radius:4px;box-shadow:0 1px 1px rgba(238,95,91,0.25)}.notice--danger h4{margin-top:0 !important;margin-bottom:0.75em;line-height:inherit}.page__content .notice--danger h4{margin-bottom:0;font-size:1em}.notice--danger p:last-child{margin-bottom:0 !important}.notice--danger h4+p{margin-top:0;padding-top:0}.notice--danger a{color:#d65652}.notice--danger a:hover{color:#77302e}.notice--danger code{background-color:#392f38}.notice--danger pre code{background-color:inherit}.notice--danger ul:last-child{margin-bottom:0}.masthead{position:relative;border-bottom:1px solid #51555d;-webkit-animation:intro 0.3s both;animation:intro 0.3s both;-webkit-animation-delay:0.15s;animation-delay:0.15s;z-index:20}.masthead__inner-wrap{clear:both;margin-left:auto;margin-right:auto;padding:1em;max-width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.masthead__inner-wrap::after{clear:both;content:"";display:table}@media (min-width: 80em){.masthead__inner-wrap{max-width:1280px}}.masthead__inner-wrap nav{z-index:10}.masthead__inner-wrap a{text-decoration:none}.site-logo img{max-height:2rem}.site-title{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-item-align:center;align-self:center;font-weight:bold}.site-subtitle{display:block;font-size:.625em}.masthead__menu{float:left;margin-left:0;margin-right:0;width:100%;clear:both}.masthead__menu .site-nav{margin-left:0}@media (min-width: 37.5em){.masthead__menu .site-nav{float:right}}.masthead__menu ul{margin:0;padding:0;clear:both;list-style-type:none}.masthead__menu-item{display:block;list-style-type:none;white-space:nowrap}.masthead__menu-item--lg{padding-right:2em;font-weight:700}.breadcrumbs{clear:both;margin:0 auto;max-width:100%;padding-left:1em;padding-right:1em;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;-webkit-animation:intro 0.3s both;animation:intro 0.3s both;-webkit-animation-delay:0.3s;animation-delay:0.3s}.breadcrumbs::after{clear:both;content:"";display:table}@media (min-width: 80em){.breadcrumbs{max-width:1280px}}.breadcrumbs ol{padding:0;list-style:none;font-size:.75em}@media (min-width: 64em){.breadcrumbs ol{float:right;width:calc(100% - 200px)}}@media (min-width: 80em){.breadcrumbs ol{width:calc(100% - 300px)}}.breadcrumbs li{display:inline}.breadcrumbs .current{font-weight:bold}.pagination{clear:both;float:left;margin-top:1em;padding-top:1em;width:100%}.pagination::after{clear:both;content:"";display:table}.pagination ul{margin:0;padding:0;list-style-type:none;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.pagination li{display:block;float:left;margin-left:-1px}.pagination li a{display:block;margin-bottom:0.25em;padding:0.5em 1em;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:14px;font-weight:bold;line-height:1.5;text-align:center;text-decoration:none;color:#eee;border:1px solid #3d4046;border-radius:0}.pagination li a:hover{color:#a9dde0}.pagination li a.current,.pagination li a.current.disabled{color:#fff;background:#00adb5}.pagination li a.disabled{color:rgba(238,238,238,0.5);pointer-events:none;cursor:not-allowed}.pagination li:first-child{margin-left:0}.pagination li:first-child a{border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination li:last-child a{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination--pager{display:block;padding:1em 2em;float:left;width:50%;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:1em;font-weight:bold;text-align:center;text-decoration:none;color:#eee;border:1px solid #3d4046;border-radius:4px}.pagination--pager:hover{background-color:#eee;color:#3d4144}.pagination--pager:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.pagination--pager:last-child{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.pagination--pager.disabled{color:rgba(238,238,238,0.5);pointer-events:none;cursor:not-allowed}.page__content+.pagination,.page__meta+.pagination,.comment__date+.pagination,.page__share+.pagination,.page__comments+.pagination{margin-top:2em;padding-top:2em;border-top:1px solid #51555d}.greedy-nav{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:2em;background:#252a34}.greedy-nav a{display:block;margin:0 1rem;color:#eaeaea;text-decoration:none;-webkit-transition:none;transition:none}.greedy-nav a:hover{color:#bbb}.greedy-nav a.site-logo{margin-left:0;margin-right:0.5rem}.greedy-nav a.site-title{margin-left:0}.greedy-nav img{-webkit-transition:none;transition:none}.greedy-nav__toggle{-ms-flex-item-align:center;align-self:center;height:2rem;border:0;outline:none;background-color:transparent;cursor:pointer}.greedy-nav .visible-links{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:hidden}.greedy-nav .visible-links li{-webkit-box-flex:0;-ms-flex:none;flex:none}.greedy-nav .visible-links a{position:relative}.greedy-nav .visible-links a:before{content:"";position:absolute;left:0;bottom:0;height:4px;background:#00adb5;width:100%;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;-webkit-transform:scaleX(0) translate3d(0, 0, 0);transform:scaleX(0) translate3d(0, 0, 0)}.greedy-nav .visible-links a:hover:before{-webkit-transform:scaleX(1);-ms-transform:scaleX(1);transform:scaleX(1)}.greedy-nav .hidden-links{position:absolute;top:100%;right:0;margin-top:15px;padding:5px;border:1px solid #51555d;border-radius:4px;background:#252a34;-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);box-shadow:0 2px 4px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}.greedy-nav .hidden-links.hidden{display:none}.greedy-nav .hidden-links a{margin:0;padding:10px 20px;font-size:1em}.greedy-nav .hidden-links a:hover{color:#bbb;background:#1a1d24}.greedy-nav .hidden-links:before{content:"";position:absolute;top:-11px;right:10px;width:0;border-style:solid;border-width:0 10px 10px;border-color:#51555d transparent;display:block;z-index:0}.greedy-nav .hidden-links:after{content:"";position:absolute;top:-10px;right:10px;width:0;border-style:solid;border-width:0 10px 10px;border-color:#252a34 transparent;display:block;z-index:1}.greedy-nav .hidden-links li{display:block;border-bottom:1px solid #51555d}.greedy-nav .hidden-links li:last-child{border-bottom:none}.no-js .greedy-nav .visible-links{-ms-flex-wrap:wrap;flex-wrap:wrap;overflow:visible}.nav__list{margin-bottom:1.5em}.nav__list input[type="checkbox"],.nav__list label{display:none}@media (max-width: 63.9375em){.nav__list label{position:relative;display:inline-block;padding:0.5em 2.5em 0.5em 1em;color:#7a8288;font-size:.75em;font-weight:bold;border:1px solid #bdc1c4;border-radius:4px;z-index:20;-webkit-transition:0.2s ease-out;transition:0.2s ease-out;cursor:pointer}.nav__list label:before,.nav__list label:after{content:"";position:absolute;right:1em;top:1.25em;width:0.75em;height:0.125em;line-height:1;background-color:#7a8288;-webkit-transition:0.2s ease-out;transition:0.2s ease-out}.nav__list label:after{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.nav__list label:hover{color:#fff;border-color:#7a8288;background-color:#333}.nav__list label:hover:before,.nav__list label:hover:after{background-color:#fff}.nav__list input:checked+label{color:white;background-color:#333}.nav__list input:checked+label:before,.nav__list input:checked+label:after{background-color:#fff}.nav__list label:hover:after{-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.nav__list input:checked+label:hover:after{-webkit-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0)}.nav__list ul{margin-bottom:1em}.nav__list a{display:block;padding:0.25em 0}}@media (max-width: 63.9375em) and (min-width: 64em){.nav__list a{padding-top:0.125em;padding-bottom:0.125em}}@media (max-width: 63.9375em){.nav__list a:hover{text-decoration:underline}}.nav__list .nav__items{margin:0;font-size:1.25rem}.nav__list .nav__items a{color:inherit}.nav__list .nav__items .active{margin-left:-0.5em;padding-left:0.5em;padding-right:0.5em;font-weight:bold}@media (max-width: 63.9375em){.nav__list .nav__items{position:relative;max-height:0;opacity:0%;overflow:hidden;z-index:10;-webkit-transition:0.3s ease-in-out;transition:0.3s ease-in-out;-webkit-transform:translate(0, 10%);-ms-transform:translate(0, 10%);transform:translate(0, 10%)}}@media (max-width: 63.9375em){.nav__list input:checked ~ .nav__items{-webkit-transition:0.5s ease-in-out;transition:0.5s ease-in-out;max-height:9999px;overflow:visible;opacity:1;margin-top:1em;-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);transform:translate(0, 0)}}.nav__title{margin:0;padding:0.5rem 0.75rem;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:1em;font-weight:bold}.nav__sub-title{display:block;margin:0.5rem 0;padding:0.25rem 0;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em;font-weight:bold;text-transform:uppercase;border-bottom:1px solid #51555d}.toc{font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;color:#7a8288;background-color:#252a34;border:1px solid #51555d;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.125);box-shadow:0 1px 1px rgba(0,0,0,0.125)}.toc .nav__title{color:#fff;font-size:.75em;background:#00adb5;border-top-left-radius:4px;border-top-right-radius:4px}.toc .active a{background-color:#cceff0;color:#3d4144}.toc__menu{margin:0;padding:0;width:100%;list-style:none;font-size:.75em}@media (min-width: 64em){.toc__menu{font-size:.6875em}}.toc__menu a{display:block;padding:0.25rem 0.75rem;color:#eee;font-weight:bold;line-height:1.5;border-bottom:1px solid #51555d}.toc__menu a:hover{color:#eaeaea}.toc__menu li ul>li a{padding-left:1.25rem;font-weight:normal}.toc__menu li ul li ul>li a{padding-left:1.75rem}.toc__menu li ul li ul li ul>li a{padding-left:2.25rem}.toc__menu li ul li ul li ul li ul>li a{padding-left:2.75rem}.toc__menu li ul li ul li ul li ul li ul>li a{padding-left:3.25rem}.page__footer{clear:both;float:left;margin-left:0;margin-right:0;width:100%;margin-top:3em;color:#eee;-webkit-animation:intro 0.3s both;animation:intro 0.3s both;-webkit-animation-delay:0.45s;animation-delay:0.45s;background-color:#1a1d24}.page__footer::after{clear:both;content:"";display:table}.page__footer footer{clear:both;margin-left:auto;margin-right:auto;margin-top:2em;max-width:100%;padding:0 1em 2em}.page__footer footer::after{clear:both;content:"";display:table}@media (min-width: 80em){.page__footer footer{max-width:1280px}}.page__footer a{color:inherit;text-decoration:none}.page__footer a:hover{text-decoration:underline}.page__footer .fas,.page__footer .fab,.page__footer .far,.page__footer .fal{color:#eee}.page__footer-copyright{font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.6875em}.page__footer-follow ul{margin:0;padding:0;list-style-type:none}.page__footer-follow li{display:inline-block;padding-top:5px;padding-bottom:5px;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em;text-transform:uppercase}.page__footer-follow li+li:before{content:"";padding-right:5px}.page__footer-follow a{padding-right:10px;font-weight:bold}.page__footer-follow .social-icons a{white-space:nowrap}.layout--search .archive__item-teaser{margin-bottom:0.25em}.search__toggle{margin-left:1rem;margin-right:1rem;height:2rem;border:0;outline:none;color:#00adb5;background-color:transparent;cursor:pointer;-webkit-transition:0.2s;transition:0.2s}.search__toggle:hover{color:#008288}.search-icon{width:100%;height:100%}.search-content{display:none;visibility:hidden;padding-top:1em;padding-bottom:1em}.search-content__inner-wrap{width:100%;margin-left:auto;margin-right:auto;padding-left:1em;padding-right:1em;-webkit-animation:intro 0.3s both;animation:intro 0.3s both;-webkit-animation-delay:0.15s;animation-delay:0.15s}@media (min-width: 80em){.search-content__inner-wrap{max-width:1280px}}.search-content__form{background-color:transparent}.search-content .search-input{display:block;margin-bottom:0;padding:0;border:none;outline:none;box-shadow:none;background-color:transparent;font-size:1.563em}@media (min-width: 64em){.search-content .search-input{font-size:1.953em}}@media (min-width: 80em){.search-content .search-input{font-size:2.441em}}.search-content.is--visible{display:block;visibility:visible}.search-content.is--visible::after{content:"";display:block}.search-content .results__found{margin-top:0.5em;font-size:.75em}.search-content .archive__item{margin-bottom:2em}@media (min-width: 64em){.search-content .archive__item{width:75%}}@media (min-width: 80em){.search-content .archive__item{width:50%}}.search-content .archive__item-title{margin-top:0}.search-content .archive__item-excerpt{margin-bottom:0}.ais-search-box{max-width:100% !important;margin-bottom:2em}.archive__item-title .ais-Highlight{color:#00adb5;font-style:normal;text-decoration:underline}.archive__item-excerpt .ais-Highlight{color:#00adb5;font-style:normal;font-weight:bold}div.highlighter-rouge,figure.highlight{position:relative;margin-bottom:1em;background:#263238;color:#eff;font-family:Monaco,Consolas,"Lucida Console",monospace;font-size:.75em;line-height:1.8;border-radius:4px}div.highlighter-rouge>pre,div.highlighter-rouge pre.highlight,figure.highlight>pre,figure.highlight pre.highlight{margin:0;padding:1em}.highlight table{margin-bottom:0;font-size:1em;border:0}.highlight table td{padding:0;width:calc(100% - 1em);border:0}.highlight table td.gutter,.highlight table td.rouge-gutter{padding-right:1em;width:1em;color:#b2ccd6;border-right:1px solid #b2ccd6;text-align:right}.highlight table td.code,.highlight table td.rouge-code{padding-left:1em}.highlight table pre{margin:0}.highlight pre{width:100%}.highlight .hll{background-color:#eff}.highlight .c{color:#b2ccd6}.highlight .err{color:#f07178}.highlight .k{color:#c792ea}.highlight .l{color:#f78c6c}.highlight .n{color:#eff}.highlight .o{color:#89ddff}.highlight .p{color:#eff}.highlight .cm{color:#b2ccd6}.highlight .cp{color:#b2ccd6}.highlight .c1{color:#b2ccd6}.highlight .cs{color:#b2ccd6}.highlight .gd{color:#f07178}.highlight .ge{font-style:italic}.highlight .gh{color:#eff;font-weight:bold}.highlight .gi{color:#c3e88d}.highlight .gp{color:#b2ccd6;font-weight:bold}.highlight .gs{font-weight:bold}.highlight .gu{color:#89ddff;font-weight:bold}.highlight .kc{color:#c792ea}.highlight .kd{color:#c792ea}.highlight .kn{color:#89ddff}.highlight .kp{color:#c792ea}.highlight .kr{color:#c792ea}.highlight .kt{color:#ffcb6b}.highlight .ld{color:#c3e88d}.highlight .m{color:#f78c6c}.highlight .s{color:#c3e88d}.highlight .na{color:#82aaff}.highlight .nb{color:#eff}.highlight .nc{color:#ffcb6b}.highlight .no{color:#f07178}.highlight .nd{color:#89ddff}.highlight .ni{color:#eff}.highlight .ne{color:#f07178}.highlight .nf{color:#82aaff}.highlight .nl{color:#eff}.highlight .nn{color:#ffcb6b}.highlight .nx{color:#82aaff}.highlight .py{color:#eff}.highlight .nt{color:#89ddff}.highlight .nv{color:#f07178}.highlight .ow{color:#89ddff}.highlight .w{color:#eff}.highlight .mf{color:#f78c6c}.highlight .mh{color:#f78c6c}.highlight .mi{color:#f78c6c}.highlight .mo{color:#f78c6c}.highlight .sb{color:#c3e88d}.highlight .sc{color:#eff}.highlight .sd{color:#b2ccd6}.highlight .s2{color:#c3e88d}.highlight .se{color:#f78c6c}.highlight .sh{color:#c3e88d}.highlight .si{color:#f78c6c}.highlight .sx{color:#c3e88d}.highlight .sr{color:#c3e88d}.highlight .s1{color:#c3e88d}.highlight .ss{color:#c3e88d}.highlight .bp{color:#eff}.highlight .vc{color:#f07178}.highlight .vg{color:#f07178}.highlight .vi{color:#f07178}.highlight .il{color:#f78c6c}.gist th,.gist td{border-bottom:0}.hidden,.is--hidden{display:none;visibility:hidden}.load{display:none}.transparent{opacity:0}.visually-hidden,.screen-reader-text,.screen-reader-text span,.screen-reader-shortcut{position:absolute !important;clip:rect(1px, 1px, 1px, 1px);height:1px !important;width:1px !important;border:0 !important;overflow:hidden}body:hover .visually-hidden a,body:hover .visually-hidden input,body:hover .visually-hidden button{display:none !important}.screen-reader-text:focus,.screen-reader-shortcut:focus{clip:auto !important;height:auto !important;width:auto !important;display:block;font-size:1em;font-weight:bold;padding:15px 23px 14px;background:#fff;z-index:100000;text-decoration:none;box-shadow:0 0 2px 2px rgba(0,0,0,0.6)}.skip-link{position:fixed;z-index:20;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;white-space:nowrap}.skip-link li{height:0;width:0;list-style:none}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.task-list{padding:0}.task-list li{list-style-type:none}.task-list .task-list-item-checkbox{margin-right:0.5em;opacity:1}.task-list .task-list{margin-left:1em}.cf{clear:both}.wrapper{margin-left:auto;margin-right:auto;width:100%}.align-left{display:block;margin-left:auto;margin-right:auto}@media (min-width: 37.5em){.align-left{float:left;margin-right:1em}}.align-right{display:block;margin-left:auto;margin-right:auto}@media (min-width: 37.5em){.align-right{float:right;margin-left:1em}}.align-center{display:block;margin-left:auto;margin-right:auto}@media (min-width: 64em){.full{margin-right:-20.3389830508% !important}}.icon{display:inline-block;fill:currentColor;width:1em;height:1.1em;line-height:1;position:relative;top:-0.1em;vertical-align:middle}.social-icons .fas,.social-icons .fab,.social-icons .far,.social-icons .fal{color:#eaeaea}.social-icons .fa-behance,.social-icons .fa-behance-square{color:#1769ff}.social-icons .fa-bitbucket{color:#205081}.social-icons .fa-dribbble,.social-icons .fa-dribble-square{color:#ea4c89}.social-icons .fa-facebook,.social-icons .fa-facebook-square,.social-icons .fa-facebook-f{color:#3b5998}.social-icons .fa-flickr{color:#ff0084}.social-icons .fa-foursquare{color:#0072b1}.social-icons .fa-github,.social-icons .fa-github-alt,.social-icons .fa-github-square{color:#171516}.social-icons .fa-gitlab{color:#e24329}.social-icons .fa-instagram{color:#517fa4}.social-icons .fa-keybase{color:#ef7639}.social-icons .fa-lastfm,.social-icons .fa-lastfm-square{color:#d51007}.social-icons .fa-linkedin,.social-icons .fa-linkedin-in{color:#007bb6}.social-icons .fa-mastodon,.social-icons .fa-mastodon-square{color:#2b90d9}.social-icons .fa-pinterest,.social-icons .fa-pinterest-p,.social-icons .fa-pinterest-square{color:#cb2027}.social-icons .fa-reddit{color:#ff4500}.social-icons .fa-rss,.social-icons .fa-rss-square{color:#fa9b39}.social-icons .fa-soundcloud{color:#f30}.social-icons .fa-stack-exchange,.social-icons .fa-stack-overflow{color:#fe7a15}.social-icons .fa-tumblr,.social-icons .fa-tumblr-square{color:#32506d}.social-icons .fa-twitter,.social-icons .fa-twitter-square{color:#55acee}.social-icons .fa-vimeo,.social-icons .fa-vimeo-square,.social-icons .fa-vimeo-v{color:#1ab7ea}.social-icons .fa-vine{color:#00bf8f}.social-icons .fa-youtube{color:#b00}.social-icons .fa-xing,.social-icons .fa-xing-square{color:#006567}.navicon{position:relative;width:1.5rem;height:.25rem;background:#00adb5;margin:auto;-webkit-transition:0.3s;transition:0.3s}.navicon:before,.navicon:after{content:"";position:absolute;left:0;width:1.5rem;height:.25rem;background:#00adb5;-webkit-transition:0.3s;transition:0.3s}.navicon:before{top:-.5rem}.navicon:after{bottom:-.5rem}.close .navicon{background:transparent}.close .navicon:before,.close .navicon:after{-webkit-transform-origin:50% 50%;-ms-transform-origin:50% 50%;transform-origin:50% 50%;top:0;width:1.5rem}.close .navicon:before{-webkit-transform:rotate3d(0, 0, 1, 45deg);transform:rotate3d(0, 0, 1, 45deg)}.close .navicon:after{-webkit-transform:rotate3d(0, 0, 1, -45deg);transform:rotate3d(0, 0, 1, -45deg)}@supports (pointer-events: none){.greedy-nav__toggle:before{content:'';position:fixed;top:0;left:0;width:100%;height:100%;opacity:0;background-color:#252a34;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;pointer-events:none}}.greedy-nav__toggle.close:before{opacity:0.9;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;pointer-events:auto}.greedy-nav__toggle:hover .navicon,.greedy-nav__toggle:hover .navicon:before,.greedy-nav__toggle:hover .navicon:after{background:#008288}.greedy-nav__toggle:hover.close .navicon{background:transparent}@media (min-width: 64em){.sticky{clear:both;position:-webkit-sticky;position:sticky;top:2em}.sticky::after{clear:both;content:"";display:table}.sticky>*{display:block}}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.show-modal{overflow:hidden;position:relative}.show-modal:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;z-index:999;background-color:rgba(255,255,255,0.85)}.show-modal .modal{display:block}.modal{display:none;position:fixed;width:300px;top:50%;left:50%;margin-left:-150px;margin-top:-150px;min-height:0;z-index:9999;background:#fff;border:1px solid #51555d;border-radius:4px;box-shadow:0 1px 1px rgba(0,0,0,0.125)}.modal__title{margin:0;padding:0.5em 1em}.modal__supporting-text{padding:0 1em 0.5em 1em}.modal__actions{padding:0.5em 1em;border-top:1px solid #51555d}.footnote{color:#9ba1a6;text-decoration:none}.footnotes{color:#9ba1a6}.footnotes ol,.footnotes li,.footnotes p{margin-bottom:0;font-size:.75em}a.reversefootnote{color:#7a8288;text-decoration:none}a.reversefootnote:hover{text-decoration:underline}.required{color:#ee5f5b;font-weight:bold}.gsc-control-cse table,.gsc-control-cse tr,.gsc-control-cse td{border:0}.responsive-video-container{position:relative;margin-bottom:1em;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%}.responsive-video-container iframe,.responsive-video-container object,.responsive-video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}:-webkit-full-screen-ancestor .masthead,:-webkit-full-screen-ancestor .page__footer{position:static}#main{clear:both;margin-left:auto;margin-right:auto;padding-left:1em;padding-right:1em;-webkit-animation:intro 0.3s both;animation:intro 0.3s both;max-width:100%;-webkit-animation-delay:0.15s;animation-delay:0.15s}#main::after{clear:both;content:"";display:table}@media (min-width: 80em){#main{max-width:1280px}}body{display:-webkit-box;display:-ms-flexbox;display:flex;min-height:100vh;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.initial-content,.search-content{flex:1 0 auto}@media (min-width: 64em){.page{float:right;width:calc(100% - 200px);padding-right:200px}}@media (min-width: 80em){.page{width:calc(100% - 300px);padding-right:300px}}.page .page__inner-wrap{float:left;margin-top:1em;margin-left:0;margin-right:0;width:100%;clear:both}.page .page__inner-wrap .page__content,.page .page__inner-wrap .page__meta,.page .page__inner-wrap .comment__date,.page .page__inner-wrap .page__share{position:relative;float:left;margin-left:0;margin-right:0;width:100%;clear:both}.page__title{margin-top:0;line-height:1}.page__title+.page__meta,.page__title+.comment__date{margin-top:-0.5em}.page__lead{font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:1.25em}.page__content h2{padding-bottom:0.5em;border-bottom:1px solid #51555d}.page__content h1 .header-link,.page__content h2 .header-link,.page__content h3 .header-link,.page__content h4 .header-link,.page__content h5 .header-link,.page__content h6 .header-link{position:relative;left:0.5em;opacity:0;font-size:0.8em;-webkit-transition:opacity 0.2s ease-in-out 0.1s;-moz-transition:opacity 0.2s ease-in-out 0.1s;-o-transition:opacity 0.2s ease-in-out 0.1s;transition:opacity 0.2s ease-in-out 0.1s}.page__content h1:hover .header-link,.page__content h2:hover .header-link,.page__content h3:hover .header-link,.page__content h4:hover .header-link,.page__content h5:hover .header-link,.page__content h6:hover .header-link{opacity:1}.page__content p,.page__content li,.page__content dl{font-size:1em}.page__content p{margin:0 0 1.3em}.page__content a:not(.btn):hover{text-decoration:underline}.page__content a:not(.btn):hover img{box-shadow:0 0 10px rgba(0,0,0,0.25)}.page__content dt{margin-top:1em;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-weight:bold}.page__content dd{margin-left:1em;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em}.page__content .small{font-size:.75em}.page__content blockquote+.small{margin-top:-1.5em;padding-left:1.25rem}.page__hero{position:relative;margin-bottom:2em;clear:both;-webkit-animation:intro 0.3s both;animation:intro 0.3s both;-webkit-animation-delay:0.25s;animation-delay:0.25s}.page__hero::after{clear:both;content:"";display:table}.page__hero--overlay{position:relative;margin-bottom:2em;padding:3em 0;clear:both;background-size:cover;background-repeat:no-repeat;background-position:center;-webkit-animation:intro 0.3s both;animation:intro 0.3s both;-webkit-animation-delay:0.25s;animation-delay:0.25s}.page__hero--overlay::after{clear:both;content:"";display:table}.page__hero--overlay a{color:#fff}.page__hero--overlay .wrapper{padding-left:1em;padding-right:1em}@media (min-width: 80em){.page__hero--overlay .wrapper{max-width:1280px}}.page__hero--overlay .page__title,.page__hero--overlay .page__meta,.page__hero--overlay .comment__date,.page__hero--overlay .page__lead,.page__hero--overlay .btn{color:#fff;text-shadow:1px 1px 4px rgba(0,0,0,0.5)}.page__hero--overlay .page__lead{max-width:768px}.page__hero--overlay .page__title{font-size:1.953em}@media (min-width: 37.5em){.page__hero--overlay .page__title{font-size:2.441em}}.page__hero-image{width:100%;height:auto;-ms-interpolation-mode:bicubic}.page__hero-caption{position:absolute;bottom:0;right:0;margin:0 auto;padding:2px 5px;color:#fff;font-family:Georgia,Times,serif;font-size:.6875em;background:#000;text-align:right;z-index:5;opacity:0.5;border-radius:4px 0 0 0}@media (min-width: 64em){.page__hero-caption{padding:5px 10px}}.page__hero-caption a{color:#fff;text-decoration:none}.page__share{margin-top:2em;padding-top:1em;border-top:1px solid #51555d}@media (max-width: 37.5em){.page__share .btn span{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}}.page__share-title{margin-bottom:10px;font-size:.75em;text-transform:uppercase}.page__meta,.comment__date{margin-top:2em;color:#eee;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em}.page__meta p,.comment__date p{margin:0}.page__meta a,.comment__date a{color:inherit}.page__meta-title{margin-bottom:10px;font-size:.75em;text-transform:uppercase}.page__meta-sep::before{content:"\2022";padding-left:0.5em;padding-right:0.5em}.page__taxonomy .sep{display:none}.page__taxonomy strong{margin-right:10px}.page__taxonomy-item{display:inline-block;margin-right:5px;margin-bottom:8px;padding:5px 10px;text-decoration:none;border:1px solid #3d4046;border-radius:4px}.page__taxonomy-item:hover{text-decoration:none;color:#a9dde0}.taxonomy__section{margin-bottom:2em;padding-bottom:1em}.taxonomy__section:not(:last-child){border-bottom:solid 1px #51555d}.taxonomy__section .archive__item-title{margin-top:0}.taxonomy__section .archive__subtitle{clear:both;border:0}.taxonomy__section+.taxonomy__section{margin-top:2em}.taxonomy__title{margin-bottom:0.5em;color:#eee}.taxonomy__count{color:#eee}.taxonomy__index{display:grid;grid-column-gap:2em;grid-template-columns:repeat(2, 1fr);margin:1.414em 0;padding:0;font-size:0.75em;list-style:none}@media (min-width: 64em){.taxonomy__index{grid-template-columns:repeat(3, 1fr)}}.taxonomy__index a{display:-webkit-box;display:-ms-flexbox;display:flex;padding:0.25em 0;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;color:inherit;text-decoration:none;border-bottom:1px solid #51555d}.back-to-top{display:block;clear:both;color:#eee;font-size:0.6em;text-transform:uppercase;text-align:right;text-decoration:none}.page__comments{float:left;margin-left:0;margin-right:0;width:100%;clear:both}.page__comments-title{margin-top:2rem;margin-bottom:10px;padding-top:2rem;font-size:.75em;border-top:1px solid #51555d;text-transform:uppercase}.page__comments-form{-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.page__comments-form.disabled input,.page__comments-form.disabled button,.page__comments-form.disabled textarea,.page__comments-form.disabled label{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);box-shadow:none;opacity:0.65}.comment{clear:both;margin:1em 0}.comment::after{clear:both;content:"";display:table}.comment:not(:last-child){border-bottom:1px solid #51555d}.comment__avatar-wrapper{float:left;width:60px;height:60px}@media (min-width: 64em){.comment__avatar-wrapper{width:100px;height:100px}}.comment__avatar{width:40px;height:40px;border-radius:50%}@media (min-width: 64em){.comment__avatar{width:80px;height:80px;padding:5px;border:1px solid #51555d}}.comment__content-wrapper{float:right;width:calc(100% - 60px)}@media (min-width: 64em){.comment__content-wrapper{width:calc(100% - 100px)}}.comment__author{margin:0}.comment__author a{text-decoration:none}.comment__date{margin:0}.comment__date a{text-decoration:none}.page__related{clear:both;float:left;margin-top:2em;padding-top:1em;border-top:1px solid #51555d}.page__related::after{clear:both;content:"";display:table}@media (min-width: 64em){.page__related{float:right;width:calc(100% - 200px)}}@media (min-width: 80em){.page__related{width:calc(100% - 300px)}}.page__related a{color:inherit;text-decoration:none}.page__related-title{margin-bottom:10px;font-size:.75em;text-transform:uppercase}@media (min-width: 64em){.wide .page{padding-right:0}}@media (min-width: 80em){.wide .page{padding-right:0}}@media (min-width: 64em){.wide .page__related{padding-right:0}}@media (min-width: 80em){.wide .page__related{padding-right:0}}.archive{margin-top:1em;margin-bottom:2em}@media (min-width: 64em){.archive{float:right;width:calc(100% - 200px);padding-right:200px}}@media (min-width: 80em){.archive{width:calc(100% - 300px);padding-right:300px}}.archive__item{position:relative}.archive__item a{position:relative;z-index:10}.archive__item a[rel="permalink"]{position:static}.archive__subtitle{margin:1.414em 0 0.5em;padding-bottom:0.5em;font-size:1em;color:#eee;border-bottom:1px solid #51555d}.archive__subtitle+.list__item .archive__item-title{margin-top:0.5em}.archive__item-title{margin-bottom:0.25em;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;line-height:initial;overflow:hidden;text-overflow:ellipsis}.archive__item-title a[rel="permalink"]::before{content:'';position:absolute;left:0;top:0;right:0;bottom:0}.archive__item-title a+a{opacity:0.5}.page__content .archive__item-title{margin-top:1em;border-bottom:none}.archive__item-excerpt{margin-top:0;font-size:.75em}.archive__item-excerpt+p{text-indent:0}.archive__item-excerpt a{position:relative}.archive__item-teaser{position:relative;border-radius:4px;overflow:hidden}.archive__item-teaser img{width:100%}.archive__item-caption{position:absolute;bottom:0;right:0;margin:0 auto;padding:2px 5px;color:#fff;font-family:Georgia,Times,serif;font-size:.625em;background:#000;text-align:right;z-index:5;opacity:0.5;border-radius:4px 0 0 0}@media (min-width: 64em){.archive__item-caption{padding:5px 10px}}.archive__item-caption a{color:#fff;text-decoration:none}.list__item .page__meta,.list__item .comment__date{margin:0 0 4px;font-size:0.6em}@media (min-width: 64em){.archive .grid__wrapper{margin-right:-200px}}@media (min-width: 80em){.archive .grid__wrapper{margin-right:-300px}}.grid__item{margin-bottom:2em}@media (min-width: 37.5em){.grid__item{float:left;width:48.9795918367%}.grid__item:nth-child(2n+1){clear:both;margin-left:0}.grid__item:nth-child(2n+2){clear:none;margin-left:2.0408163265%}}@media (min-width: 48em){.grid__item{margin-left:0;margin-right:0;width:23.7288135593%}.grid__item:nth-child(2n+1){clear:none}.grid__item:nth-child(4n+1){clear:both}.grid__item:nth-child(4n+2){clear:none;margin-left:1.6949152542%}.grid__item:nth-child(4n+3){clear:none;margin-left:1.6949152542%}.grid__item:nth-child(4n+4){clear:none;margin-left:1.6949152542%}}.grid__item .page__meta,.grid__item .comment__date{margin:0 0 4px;font-size:0.6em}.grid__item .page__meta-sep{display:block}.grid__item .page__meta-sep::before{display:none}.grid__item .archive__item-title{margin-top:0.5em;font-size:1em}.grid__item .archive__item-excerpt{display:none}@media (min-width: 48em){.grid__item .archive__item-excerpt{display:block;font-size:.75em}}@media (min-width: 37.5em){.grid__item .archive__item-teaser{max-height:200px}}@media (min-width: 48em){.grid__item .archive__item-teaser{max-height:120px}}.feature__wrapper{clear:both;margin-bottom:2em;border-bottom:1px solid #51555d}.feature__wrapper::after{clear:both;content:"";display:table}.feature__wrapper .archive__item-title{margin-bottom:0}.feature__item{position:relative;margin-bottom:2em;font-size:1.125em}@media (min-width: 37.5em){.feature__item{float:left;margin-bottom:0;width:32.2033898305%}.feature__item:nth-child(3n+1){clear:both;margin-left:0}.feature__item:nth-child(3n+2){clear:none;margin-left:1.6949152542%}.feature__item:nth-child(3n+3){clear:none;margin-left:1.6949152542%}.feature__item .feature__item-teaser{max-height:200px;overflow:hidden}}.feature__item .archive__item-body{padding-left:1.6949152542%;padding-right:1.6949152542%}.feature__item a.btn::before{content:'';position:absolute;left:0;top:0;right:0;bottom:0}.feature__item--left{position:relative;float:left;margin-left:0;margin-right:0;width:100%;clear:both;font-size:1.125em}.feature__item--left .archive__item{float:left}.feature__item--left .archive__item-teaser{margin-bottom:2em}.feature__item--left a.btn::before{content:'';position:absolute;left:0;top:0;right:0;bottom:0}@media (min-width: 37.5em){.feature__item--left .archive__item-teaser{float:left;width:40.6779661017%}.feature__item--left .archive__item-body{float:right;padding-left:1.6949152542%;padding-right:1.6949152542%;width:57.6271186441%}}.feature__item--right{position:relative;float:left;margin-left:0;margin-right:0;width:100%;clear:both;font-size:1.125em}.feature__item--right .archive__item{float:left}.feature__item--right .archive__item-teaser{margin-bottom:2em}.feature__item--right a.btn::before{content:'';position:absolute;left:0;top:0;right:0;bottom:0}@media (min-width: 37.5em){.feature__item--right{text-align:right}.feature__item--right .archive__item-teaser{float:right;width:40.6779661017%}.feature__item--right .archive__item-body{float:left;width:57.6271186441%;padding-left:1.6949152542%;padding-right:1.6949152542%}}.feature__item--center{position:relative;float:left;margin-left:0;margin-right:0;width:100%;clear:both;font-size:1.125em}.feature__item--center .archive__item{float:left;width:100%}.feature__item--center .archive__item-teaser{margin-bottom:2em}.feature__item--center a.btn::before{content:'';position:absolute;left:0;top:0;right:0;bottom:0}@media (min-width: 37.5em){.feature__item--center{text-align:center}.feature__item--center .archive__item-teaser{margin:0 auto;width:40.6779661017%}.feature__item--center .archive__item-body{margin:0 auto;width:57.6271186441%}}.archive .feature__wrapper .archive__item-title{margin-top:0.25em;font-size:1em}.archive .feature__item,.archive .feature__item--left,.archive .feature__item--center,.archive .feature__item--right{font-size:1em}@media (min-width: 64em){.wide .archive{padding-right:0}}@media (min-width: 80em){.wide .archive{padding-right:0}}.layout--single .feature__wrapper{display:inline-block}.sidebar{clear:both}.sidebar::after{clear:both;content:"";display:table}@media (min-width: 64em){.sidebar{float:left;width:calc(200px - 1em);opacity:0.75;-webkit-transition:opacity 0.2s ease-in-out;transition:opacity 0.2s ease-in-out}.sidebar:hover{opacity:1}.sidebar.sticky{overflow-y:auto;max-height:calc(100vh - 2em - 2em)}}@media (min-width: 80em){.sidebar{width:calc(300px - 1em)}}.sidebar>*{margin-top:1em;margin-bottom:1em}.sidebar h2,.sidebar h3,.sidebar h4,.sidebar h5,.sidebar h6{margin-bottom:0;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif}.sidebar p,.sidebar li{font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:.75em;line-height:1.5}.sidebar img{width:100%}.sidebar img.emoji{width:20px;height:20px}.sidebar__right{margin-bottom:1em}@media (min-width: 64em){.sidebar__right{position:absolute;top:0;right:0;width:200px;margin-right:-200px;padding-left:1em;z-index:10}.sidebar__right.sticky{clear:both;position:-webkit-sticky;position:sticky;top:2em;float:right}.sidebar__right.sticky::after{clear:both;content:"";display:table}}@media (min-width: 80em){.sidebar__right{width:300px;margin-right:-300px}}@media (min-width: 64em){.splash .sidebar__right{position:relative;float:right;margin-right:0}}@media (min-width: 80em){.splash .sidebar__right{margin-right:0}}.author__avatar{display:table-cell;vertical-align:top;width:36px;height:36px}@media (min-width: 64em){.author__avatar{display:block;width:auto;height:auto}}.author__avatar img{max-width:110px;border-radius:50%}@media (min-width: 64em){.author__avatar img{padding:5px;border:1px solid #51555d}}.author__content{display:table-cell;vertical-align:top;padding-left:15px;padding-right:25px;line-height:1}@media (min-width: 64em){.author__content{display:block;width:100%;padding-left:0;padding-right:0}}.author__content a{color:inherit;text-decoration:none}.author__name{margin:0}@media (min-width: 64em){.author__name{margin-top:10px;margin-bottom:10px}}.sidebar .author__name{font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;font-size:1em}.author__bio{margin:0}@media (min-width: 64em){.author__bio{margin-top:10px;margin-bottom:20px}}.author__urls-wrapper{position:relative;display:table-cell;vertical-align:middle;font-family:-apple-system,BlinkMacSystemFont,"Roboto","Segoe UI","Helvetica Neue","Lucida Grande",Arial,sans-serif;z-index:20;cursor:pointer}.author__urls-wrapper li:last-child a{margin-bottom:0}.author__urls-wrapper .author__urls span.label{padding-left:5px}@media (min-width: 64em){.author__urls-wrapper{display:block}}.author__urls-wrapper button{position:relative;margin-bottom:0}@supports (pointer-events: none){.author__urls-wrapper button:before{content:'';position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none}}.author__urls-wrapper button.open:before{pointer-events:auto}@media (min-width: 64em){.author__urls-wrapper button{display:none}}.author__urls{display:none;position:absolute;right:0;margin-top:15px;padding:10px;list-style-type:none;border:1px solid #51555d;border-radius:4px;background:#252a34;box-shadow:0 2px 4px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);cursor:default}.author__urls.is--visible{display:block}@media (min-width: 64em){.author__urls{display:block;position:relative;margin:0;padding:0;border:0;background:transparent;box-shadow:none}}.author__urls:before{display:block;content:"";position:absolute;top:-11px;left:calc(50% - 10px);width:0;border-style:solid;border-width:0 10px 10px;border-color:#51555d transparent;z-index:0}@media (min-width: 64em){.author__urls:before{display:none}}.author__urls:after{display:block;content:"";position:absolute;top:-10px;left:calc(50% - 10px);width:0;border-style:solid;border-width:0 10px 10px;border-color:#252a34 transparent;z-index:1}@media (min-width: 64em){.author__urls:after{display:none}}.author__urls ul{padding:10px;list-style-type:none}.author__urls li{white-space:nowrap}.author__urls a{display:block;margin-bottom:5px;padding-right:5px;padding-top:2px;padding-bottom:2px;color:inherit;font-size:1em;text-decoration:none}.author__urls a:hover{text-decoration:underline}.wide .sidebar__right{margin-bottom:1em}@media (min-width: 64em){.wide .sidebar__right{position:initial;top:initial;right:initial;width:initial;margin-right:initial;padding-left:initial;z-index:initial}.wide .sidebar__right.sticky{float:none}}@media (min-width: 80em){.wide .sidebar__right{width:initial;margin-right:initial}}@media print{[hidden]{display:none}*{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}html{margin:0;padding:0;min-height:auto !important;font-size:16px}body{margin:0 auto;background:#fff !important;color:#000 !important;font-size:1rem;line-height:1.5;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}h1,h2,h3,h4,h5,h6{color:#000;line-height:1.2;margin-bottom:0.75rem;margin-top:0}h1{font-size:2.5rem}h2{font-size:2rem}h3{font-size:1.75rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1rem}a,a:visited{color:#000;text-decoration:underline;word-wrap:break-word}table{border-collapse:collapse}thead{display:table-header-group}table,th,td{border-bottom:1px solid #000}td,th{padding:8px 16px}img{border:0;display:block;max-width:100% !important;vertical-align:middle}hr{border:0;border-bottom:2px solid #bbb;height:0;margin:2.25rem 0;padding:0}dt{font-weight:bold}dd{margin:0;margin-bottom:0.75rem}abbr[title],acronym[title]{border:0;text-decoration:none}table,blockquote,pre,code,figure,li,hr,ul,ol,a,tr{page-break-inside:avoid}h2,h3,h4,p,a{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid;page-break-inside:avoid}h1+p,h2+p,h3+p{page-break-before:avoid}img{page-break-after:auto;page-break-before:auto;page-break-inside:avoid}pre{white-space:pre-wrap !important;word-wrap:break-word}a[href^='http://']:after,a[href^='https://']:after,a[href^='ftp://']:after{content:" (" attr(href) ")";font-size:80%}abbr[title]:after,acronym[title]:after{content:" (" attr(title) ")"}#main{max-width:100%}.page{margin:0;padding:0;width:100%}.page-break,.page-break-before{page-break-before:always}.page-break-after{page-break-after:always}.no-print{display:none}a.no-reformat:after{content:''}abbr[title].no-reformat:after,acronym[title].no-reformat:after{content:''}.page__hero-caption{color:#000 !important;background:#fff !important;opacity:1}.page__hero-caption a{color:#000 !important}.masthead,.toc,.page__share,.page__related,.pagination,.ads,.page__footer,.page__comments-form,.author__avatar,.author__content,.author__urls-wrapper,.nav__list,.sidebar,.adsbygoogle{display:none !important;height:1px !important}} diff --git a/assets/images/alfons-morales-YLSwjSy7stw-unsplash.jpg b/assets/images/alfons-morales-YLSwjSy7stw-unsplash.jpg new file mode 100644 index 0000000..3705cfd Binary files /dev/null and b/assets/images/alfons-morales-YLSwjSy7stw-unsplash.jpg differ diff --git a/assets/images/android_webrtc.png b/assets/images/android_webrtc.png new file mode 100755 index 0000000..ddb41c2 Binary files /dev/null and b/assets/images/android_webrtc.png differ diff --git a/assets/images/bio-photo.jpg b/assets/images/bio-photo.jpg new file mode 100644 index 0000000..dac6171 Binary files /dev/null and b/assets/images/bio-photo.jpg differ diff --git a/assets/images/create_a_new_repo_on_github.png b/assets/images/create_a_new_repo_on_github.png new file mode 100644 index 0000000..6b826d9 Binary files /dev/null and b/assets/images/create_a_new_repo_on_github.png differ diff --git a/assets/images/design_pattern_4_uml.png b/assets/images/design_pattern_4_uml.png new file mode 100644 index 0000000..4856544 Binary files /dev/null and b/assets/images/design_pattern_4_uml.png differ diff --git a/assets/images/design_pattern_4_uml_aggregation.png b/assets/images/design_pattern_4_uml_aggregation.png new file mode 100644 index 0000000..1180142 Binary files /dev/null and b/assets/images/design_pattern_4_uml_aggregation.png differ diff --git a/assets/images/design_pattern_4_uml_aggregation_sign.png b/assets/images/design_pattern_4_uml_aggregation_sign.png new file mode 100644 index 0000000..026f6dc Binary files /dev/null and b/assets/images/design_pattern_4_uml_aggregation_sign.png differ diff --git a/assets/images/design_pattern_4_uml_association.png b/assets/images/design_pattern_4_uml_association.png new file mode 100644 index 0000000..c57bce8 Binary files /dev/null and b/assets/images/design_pattern_4_uml_association.png differ diff --git a/assets/images/design_pattern_4_uml_association_sign.png b/assets/images/design_pattern_4_uml_association_sign.png new file mode 100644 index 0000000..ea53f1e Binary files /dev/null and b/assets/images/design_pattern_4_uml_association_sign.png differ diff --git a/assets/images/design_pattern_4_uml_attribute.png b/assets/images/design_pattern_4_uml_attribute.png new file mode 100644 index 0000000..badaf2f Binary files /dev/null and b/assets/images/design_pattern_4_uml_attribute.png differ diff --git a/assets/images/design_pattern_4_uml_class.png b/assets/images/design_pattern_4_uml_class.png new file mode 100644 index 0000000..b89ef2e Binary files /dev/null and b/assets/images/design_pattern_4_uml_class.png differ diff --git a/assets/images/design_pattern_4_uml_compare_association_aggregation_composition.png b/assets/images/design_pattern_4_uml_compare_association_aggregation_composition.png new file mode 100644 index 0000000..eb3ba83 Binary files /dev/null and b/assets/images/design_pattern_4_uml_compare_association_aggregation_composition.png differ diff --git a/assets/images/design_pattern_4_uml_composition.png b/assets/images/design_pattern_4_uml_composition.png new file mode 100644 index 0000000..786656a Binary files /dev/null and b/assets/images/design_pattern_4_uml_composition.png differ diff --git a/assets/images/design_pattern_4_uml_composition_sign.png b/assets/images/design_pattern_4_uml_composition_sign.png new file mode 100644 index 0000000..f828a3b Binary files /dev/null and b/assets/images/design_pattern_4_uml_composition_sign.png differ diff --git a/assets/images/design_pattern_4_uml_dependency.png b/assets/images/design_pattern_4_uml_dependency.png new file mode 100644 index 0000000..c0665fe Binary files /dev/null and b/assets/images/design_pattern_4_uml_dependency.png differ diff --git a/assets/images/design_pattern_4_uml_dependency_sign.png b/assets/images/design_pattern_4_uml_dependency_sign.png new file mode 100644 index 0000000..ddba4f5 Binary files /dev/null and b/assets/images/design_pattern_4_uml_dependency_sign.png differ diff --git a/assets/images/design_pattern_4_uml_generalization_inheritance.png b/assets/images/design_pattern_4_uml_generalization_inheritance.png new file mode 100644 index 0000000..f81df24 Binary files /dev/null and b/assets/images/design_pattern_4_uml_generalization_inheritance.png differ diff --git a/assets/images/design_pattern_4_uml_generalization_inheritance_sign.png b/assets/images/design_pattern_4_uml_generalization_inheritance_sign.png new file mode 100644 index 0000000..9e1c691 Binary files /dev/null and b/assets/images/design_pattern_4_uml_generalization_inheritance_sign.png differ diff --git a/assets/images/design_pattern_4_uml_interface_1.png b/assets/images/design_pattern_4_uml_interface_1.png new file mode 100644 index 0000000..1bce6c7 Binary files /dev/null and b/assets/images/design_pattern_4_uml_interface_1.png differ diff --git a/assets/images/design_pattern_4_uml_interface_2.png b/assets/images/design_pattern_4_uml_interface_2.png new file mode 100644 index 0000000..59d0f83 Binary files /dev/null and b/assets/images/design_pattern_4_uml_interface_2.png differ diff --git a/assets/images/design_pattern_4_uml_realization_implementation.png b/assets/images/design_pattern_4_uml_realization_implementation.png new file mode 100644 index 0000000..298b197 Binary files /dev/null and b/assets/images/design_pattern_4_uml_realization_implementation.png differ diff --git a/assets/images/design_pattern_4_uml_realization_implementation_sign.png b/assets/images/design_pattern_4_uml_realization_implementation_sign.png new file mode 100644 index 0000000..5b3686c Binary files /dev/null and b/assets/images/design_pattern_4_uml_realization_implementation_sign.png differ diff --git a/assets/images/design_pattern_abstract_factory_pattern_uml_1.png b/assets/images/design_pattern_abstract_factory_pattern_uml_1.png new file mode 100644 index 0000000..2f53d71 Binary files /dev/null and b/assets/images/design_pattern_abstract_factory_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_abstract_factory_pattern_uml_2.png b/assets/images/design_pattern_abstract_factory_pattern_uml_2.png new file mode 100644 index 0000000..393f9a8 Binary files /dev/null and b/assets/images/design_pattern_abstract_factory_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_adapter_pattern_uml_1.png b/assets/images/design_pattern_adapter_pattern_uml_1.png new file mode 100644 index 0000000..370488f Binary files /dev/null and b/assets/images/design_pattern_adapter_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_adapter_pattern_uml_2.png b/assets/images/design_pattern_adapter_pattern_uml_2.png new file mode 100644 index 0000000..6dee297 Binary files /dev/null and b/assets/images/design_pattern_adapter_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_adapter_pattern_uml_3.png b/assets/images/design_pattern_adapter_pattern_uml_3.png new file mode 100644 index 0000000..c7d88c2 Binary files /dev/null and b/assets/images/design_pattern_adapter_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_bridge_pattern_uml_1.png b/assets/images/design_pattern_bridge_pattern_uml_1.png new file mode 100644 index 0000000..ced584b Binary files /dev/null and b/assets/images/design_pattern_bridge_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_bridge_pattern_uml_2.png b/assets/images/design_pattern_bridge_pattern_uml_2.png new file mode 100644 index 0000000..43206bf Binary files /dev/null and b/assets/images/design_pattern_bridge_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_bridge_pattern_uml_3.png b/assets/images/design_pattern_bridge_pattern_uml_3.png new file mode 100644 index 0000000..2fa5636 Binary files /dev/null and b/assets/images/design_pattern_bridge_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_builder_pattern_uml_1.png b/assets/images/design_pattern_builder_pattern_uml_1.png new file mode 100644 index 0000000..d77365f Binary files /dev/null and b/assets/images/design_pattern_builder_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_builder_pattern_uml_2.png b/assets/images/design_pattern_builder_pattern_uml_2.png new file mode 100644 index 0000000..9630869 Binary files /dev/null and b/assets/images/design_pattern_builder_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_builder_pattern_uml_3.png b/assets/images/design_pattern_builder_pattern_uml_3.png new file mode 100644 index 0000000..1fb35b5 Binary files /dev/null and b/assets/images/design_pattern_builder_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_builder_pattern_uml_4.png b/assets/images/design_pattern_builder_pattern_uml_4.png new file mode 100644 index 0000000..835e1a9 Binary files /dev/null and b/assets/images/design_pattern_builder_pattern_uml_4.png differ diff --git a/assets/images/design_pattern_chain_of_responsibility_pattern_uml_1.png b/assets/images/design_pattern_chain_of_responsibility_pattern_uml_1.png new file mode 100644 index 0000000..4da41fa Binary files /dev/null and b/assets/images/design_pattern_chain_of_responsibility_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_chain_of_responsibility_pattern_uml_2.png b/assets/images/design_pattern_chain_of_responsibility_pattern_uml_2.png new file mode 100644 index 0000000..eaa9c91 Binary files /dev/null and b/assets/images/design_pattern_chain_of_responsibility_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_chain_of_responsibility_pattern_uml_3.png b/assets/images/design_pattern_chain_of_responsibility_pattern_uml_3.png new file mode 100644 index 0000000..e244ddf Binary files /dev/null and b/assets/images/design_pattern_chain_of_responsibility_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_command_pattern_uml_1.png b/assets/images/design_pattern_command_pattern_uml_1.png new file mode 100644 index 0000000..1904560 Binary files /dev/null and b/assets/images/design_pattern_command_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_command_pattern_uml_2.png b/assets/images/design_pattern_command_pattern_uml_2.png new file mode 100644 index 0000000..3ba5cfa Binary files /dev/null and b/assets/images/design_pattern_command_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_command_pattern_uml_3.png b/assets/images/design_pattern_command_pattern_uml_3.png new file mode 100644 index 0000000..3045845 Binary files /dev/null and b/assets/images/design_pattern_command_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_composite_pattern_uml_1.png b/assets/images/design_pattern_composite_pattern_uml_1.png new file mode 100644 index 0000000..44e70f0 Binary files /dev/null and b/assets/images/design_pattern_composite_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_composite_pattern_uml_2.png b/assets/images/design_pattern_composite_pattern_uml_2.png new file mode 100644 index 0000000..33a3e51 Binary files /dev/null and b/assets/images/design_pattern_composite_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_composite_pattern_uml_3.png b/assets/images/design_pattern_composite_pattern_uml_3.png new file mode 100644 index 0000000..080d35d Binary files /dev/null and b/assets/images/design_pattern_composite_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_decorator_pattern_uml_1.png b/assets/images/design_pattern_decorator_pattern_uml_1.png new file mode 100644 index 0000000..db17622 Binary files /dev/null and b/assets/images/design_pattern_decorator_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_decorator_pattern_uml_2.png b/assets/images/design_pattern_decorator_pattern_uml_2.png new file mode 100644 index 0000000..e22b056 Binary files /dev/null and b/assets/images/design_pattern_decorator_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_decorator_pattern_uml_3.png b/assets/images/design_pattern_decorator_pattern_uml_3.png new file mode 100644 index 0000000..8e87e67 Binary files /dev/null and b/assets/images/design_pattern_decorator_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_design_principle_architecture.png b/assets/images/design_pattern_design_principle_architecture.png new file mode 100644 index 0000000..f34765e Binary files /dev/null and b/assets/images/design_pattern_design_principle_architecture.png differ diff --git a/assets/images/design_pattern_design_principle_favor_composition_over_inheritance_1.png b/assets/images/design_pattern_design_principle_favor_composition_over_inheritance_1.png new file mode 100644 index 0000000..e404497 Binary files /dev/null and b/assets/images/design_pattern_design_principle_favor_composition_over_inheritance_1.png differ diff --git a/assets/images/design_pattern_design_principle_favor_composition_over_inheritance_2.png b/assets/images/design_pattern_design_principle_favor_composition_over_inheritance_2.png new file mode 100644 index 0000000..a645dd3 Binary files /dev/null and b/assets/images/design_pattern_design_principle_favor_composition_over_inheritance_2.png differ diff --git a/assets/images/design_pattern_design_principle_loose_coupling_1.png b/assets/images/design_pattern_design_principle_loose_coupling_1.png new file mode 100644 index 0000000..07164c2 Binary files /dev/null and b/assets/images/design_pattern_design_principle_loose_coupling_1.png differ diff --git a/assets/images/design_pattern_design_principle_loose_coupling_2.png b/assets/images/design_pattern_design_principle_loose_coupling_2.png new file mode 100644 index 0000000..f1e4da4 Binary files /dev/null and b/assets/images/design_pattern_design_principle_loose_coupling_2.png differ diff --git a/assets/images/design_pattern_design_principle_mind.png b/assets/images/design_pattern_design_principle_mind.png new file mode 100644 index 0000000..05d1032 Binary files /dev/null and b/assets/images/design_pattern_design_principle_mind.png differ diff --git a/assets/images/design_pattern_design_principle_program_to_interface_1.png b/assets/images/design_pattern_design_principle_program_to_interface_1.png new file mode 100644 index 0000000..fb8b711 Binary files /dev/null and b/assets/images/design_pattern_design_principle_program_to_interface_1.png differ diff --git a/assets/images/design_pattern_design_principle_program_to_interface_2.png b/assets/images/design_pattern_design_principle_program_to_interface_2.png new file mode 100644 index 0000000..02aedde Binary files /dev/null and b/assets/images/design_pattern_design_principle_program_to_interface_2.png differ diff --git a/assets/images/design_pattern_facade_pattern_uml_1.png b/assets/images/design_pattern_facade_pattern_uml_1.png new file mode 100644 index 0000000..c78205e Binary files /dev/null and b/assets/images/design_pattern_facade_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_facade_pattern_uml_2.png b/assets/images/design_pattern_facade_pattern_uml_2.png new file mode 100644 index 0000000..55a093f Binary files /dev/null and b/assets/images/design_pattern_facade_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_facade_pattern_uml_3.png b/assets/images/design_pattern_facade_pattern_uml_3.png new file mode 100644 index 0000000..3ea864c Binary files /dev/null and b/assets/images/design_pattern_facade_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_factory_method_pattern_uml_1.png b/assets/images/design_pattern_factory_method_pattern_uml_1.png new file mode 100644 index 0000000..f53823a Binary files /dev/null and b/assets/images/design_pattern_factory_method_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_factory_method_pattern_uml_2.png b/assets/images/design_pattern_factory_method_pattern_uml_2.png new file mode 100644 index 0000000..8afb342 Binary files /dev/null and b/assets/images/design_pattern_factory_method_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_factory_method_pattern_uml_3.png b/assets/images/design_pattern_factory_method_pattern_uml_3.png new file mode 100644 index 0000000..2ecb3e7 Binary files /dev/null and b/assets/images/design_pattern_factory_method_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_flyweight_pattern_uml_1.png b/assets/images/design_pattern_flyweight_pattern_uml_1.png new file mode 100644 index 0000000..42abe9e Binary files /dev/null and b/assets/images/design_pattern_flyweight_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_flyweight_pattern_uml_2.png b/assets/images/design_pattern_flyweight_pattern_uml_2.png new file mode 100644 index 0000000..7fa5e63 Binary files /dev/null and b/assets/images/design_pattern_flyweight_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_flyweight_pattern_uml_3.png b/assets/images/design_pattern_flyweight_pattern_uml_3.png new file mode 100644 index 0000000..d01ddaf Binary files /dev/null and b/assets/images/design_pattern_flyweight_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_iterator_pattern_uml_1.png b/assets/images/design_pattern_iterator_pattern_uml_1.png new file mode 100644 index 0000000..89ed2ac Binary files /dev/null and b/assets/images/design_pattern_iterator_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_iterator_pattern_uml_2.png b/assets/images/design_pattern_iterator_pattern_uml_2.png new file mode 100644 index 0000000..df62f5d Binary files /dev/null and b/assets/images/design_pattern_iterator_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_iterator_pattern_uml_3.png b/assets/images/design_pattern_iterator_pattern_uml_3.png new file mode 100644 index 0000000..43c098c Binary files /dev/null and b/assets/images/design_pattern_iterator_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_mediator_pattern_uml_1.png b/assets/images/design_pattern_mediator_pattern_uml_1.png new file mode 100644 index 0000000..d03bfb7 Binary files /dev/null and b/assets/images/design_pattern_mediator_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_mediator_pattern_uml_2.png b/assets/images/design_pattern_mediator_pattern_uml_2.png new file mode 100644 index 0000000..3424124 Binary files /dev/null and b/assets/images/design_pattern_mediator_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_mediator_pattern_uml_3.png b/assets/images/design_pattern_mediator_pattern_uml_3.png new file mode 100644 index 0000000..bdbea13 Binary files /dev/null and b/assets/images/design_pattern_mediator_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_memento_pattern_uml_1.png b/assets/images/design_pattern_memento_pattern_uml_1.png new file mode 100644 index 0000000..d074fe9 Binary files /dev/null and b/assets/images/design_pattern_memento_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_memento_pattern_uml_2.png b/assets/images/design_pattern_memento_pattern_uml_2.png new file mode 100644 index 0000000..669d9cd Binary files /dev/null and b/assets/images/design_pattern_memento_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_memento_pattern_uml_3.png b/assets/images/design_pattern_memento_pattern_uml_3.png new file mode 100644 index 0000000..98914ac Binary files /dev/null and b/assets/images/design_pattern_memento_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_observer_pattern_uml_1.png b/assets/images/design_pattern_observer_pattern_uml_1.png new file mode 100644 index 0000000..8662897 Binary files /dev/null and b/assets/images/design_pattern_observer_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_observer_pattern_uml_2.png b/assets/images/design_pattern_observer_pattern_uml_2.png new file mode 100644 index 0000000..b591940 Binary files /dev/null and b/assets/images/design_pattern_observer_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_observer_pattern_uml_3.png b/assets/images/design_pattern_observer_pattern_uml_3.png new file mode 100644 index 0000000..2be23e7 Binary files /dev/null and b/assets/images/design_pattern_observer_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_prototype_pattern_uml_1.png b/assets/images/design_pattern_prototype_pattern_uml_1.png new file mode 100644 index 0000000..3b65d16 Binary files /dev/null and b/assets/images/design_pattern_prototype_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_prototype_pattern_uml_2.png b/assets/images/design_pattern_prototype_pattern_uml_2.png new file mode 100644 index 0000000..3be093a Binary files /dev/null and b/assets/images/design_pattern_prototype_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_prototype_pattern_uml_3.png b/assets/images/design_pattern_prototype_pattern_uml_3.png new file mode 100644 index 0000000..54daa9f Binary files /dev/null and b/assets/images/design_pattern_prototype_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_proxy_pattern_uml_1.png b/assets/images/design_pattern_proxy_pattern_uml_1.png new file mode 100644 index 0000000..d961861 Binary files /dev/null and b/assets/images/design_pattern_proxy_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_proxy_pattern_uml_2.png b/assets/images/design_pattern_proxy_pattern_uml_2.png new file mode 100644 index 0000000..ce4d470 Binary files /dev/null and b/assets/images/design_pattern_proxy_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_proxy_pattern_uml_3.png b/assets/images/design_pattern_proxy_pattern_uml_3.png new file mode 100644 index 0000000..951e6ed Binary files /dev/null and b/assets/images/design_pattern_proxy_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_simple_factory_pattern_uml_1.png b/assets/images/design_pattern_simple_factory_pattern_uml_1.png new file mode 100644 index 0000000..c80dd79 Binary files /dev/null and b/assets/images/design_pattern_simple_factory_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_simple_factory_pattern_uml_2.png b/assets/images/design_pattern_simple_factory_pattern_uml_2.png new file mode 100644 index 0000000..b6d3545 Binary files /dev/null and b/assets/images/design_pattern_simple_factory_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_simple_factory_pattern_uml_3.png b/assets/images/design_pattern_simple_factory_pattern_uml_3.png new file mode 100644 index 0000000..6c05cc0 Binary files /dev/null and b/assets/images/design_pattern_simple_factory_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_singleton_pattern_uml_1.png b/assets/images/design_pattern_singleton_pattern_uml_1.png new file mode 100644 index 0000000..83ae986 Binary files /dev/null and b/assets/images/design_pattern_singleton_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_singleton_pattern_uml_2.png b/assets/images/design_pattern_singleton_pattern_uml_2.png new file mode 100644 index 0000000..5f59bcd Binary files /dev/null and b/assets/images/design_pattern_singleton_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_singleton_pattern_uml_3.png b/assets/images/design_pattern_singleton_pattern_uml_3.png new file mode 100644 index 0000000..c90480f Binary files /dev/null and b/assets/images/design_pattern_singleton_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_state_pattern_uml_1.png b/assets/images/design_pattern_state_pattern_uml_1.png new file mode 100644 index 0000000..caf4024 Binary files /dev/null and b/assets/images/design_pattern_state_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_state_pattern_uml_2.png b/assets/images/design_pattern_state_pattern_uml_2.png new file mode 100644 index 0000000..c16b633 Binary files /dev/null and b/assets/images/design_pattern_state_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_state_pattern_uml_3.png b/assets/images/design_pattern_state_pattern_uml_3.png new file mode 100644 index 0000000..babcac1 Binary files /dev/null and b/assets/images/design_pattern_state_pattern_uml_3.png differ diff --git a/assets/images/design_pattern_strategy_pattern_uml_1.png b/assets/images/design_pattern_strategy_pattern_uml_1.png new file mode 100644 index 0000000..b0761a0 Binary files /dev/null and b/assets/images/design_pattern_strategy_pattern_uml_1.png differ diff --git a/assets/images/design_pattern_strategy_pattern_uml_2.png b/assets/images/design_pattern_strategy_pattern_uml_2.png new file mode 100644 index 0000000..20a85e8 Binary files /dev/null and b/assets/images/design_pattern_strategy_pattern_uml_2.png differ diff --git a/assets/images/design_pattern_strategy_pattern_uml_3.png b/assets/images/design_pattern_strategy_pattern_uml_3.png new file mode 100644 index 0000000..13b7a38 Binary files /dev/null and b/assets/images/design_pattern_strategy_pattern_uml_3.png differ diff --git a/assets/images/design_patterns.jpg b/assets/images/design_patterns.jpg new file mode 100644 index 0000000..7ee2e6f Binary files /dev/null and b/assets/images/design_patterns.jpg differ diff --git a/assets/images/fabien-moline-XaQXyRU9pgo-unsplash.jpg b/assets/images/fabien-moline-XaQXyRU9pgo-unsplash.jpg new file mode 100644 index 0000000..f48027d Binary files /dev/null and b/assets/images/fabien-moline-XaQXyRU9pgo-unsplash.jpg differ diff --git a/assets/images/github_container_registry.png b/assets/images/github_container_registry.png new file mode 100644 index 0000000..d3074e1 Binary files /dev/null and b/assets/images/github_container_registry.png differ diff --git a/assets/images/github_container_registry_docker_build_image.png b/assets/images/github_container_registry_docker_build_image.png new file mode 100644 index 0000000..dc31e8f Binary files /dev/null and b/assets/images/github_container_registry_docker_build_image.png differ diff --git a/assets/images/github_container_registry_docker_images.png b/assets/images/github_container_registry_docker_images.png new file mode 100644 index 0000000..b75ec45 Binary files /dev/null and b/assets/images/github_container_registry_docker_images.png differ diff --git a/assets/images/github_container_registry_download_image.png b/assets/images/github_container_registry_download_image.png new file mode 100644 index 0000000..5ddac9f Binary files /dev/null and b/assets/images/github_container_registry_download_image.png differ diff --git a/assets/images/github_container_registry_generate_github_token.png b/assets/images/github_container_registry_generate_github_token.png new file mode 100644 index 0000000..ee58c13 Binary files /dev/null and b/assets/images/github_container_registry_generate_github_token.png differ diff --git a/assets/images/github_container_registry_github_package.png b/assets/images/github_container_registry_github_package.png new file mode 100644 index 0000000..9f2393b Binary files /dev/null and b/assets/images/github_container_registry_github_package.png differ diff --git a/assets/images/github_container_registry_sample_website.png b/assets/images/github_container_registry_sample_website.png new file mode 100644 index 0000000..b80eda5 Binary files /dev/null and b/assets/images/github_container_registry_sample_website.png differ diff --git a/assets/images/google_adsense.jpg b/assets/images/google_adsense.jpg new file mode 100644 index 0000000..21c8a63 Binary files /dev/null and b/assets/images/google_adsense.jpg differ diff --git a/assets/images/google_adsense_ad_style.png b/assets/images/google_adsense_ad_style.png new file mode 100644 index 0000000..baeb691 Binary files /dev/null and b/assets/images/google_adsense_ad_style.png differ diff --git a/assets/images/google_adsense_code.png b/assets/images/google_adsense_code.png new file mode 100644 index 0000000..41eab83 Binary files /dev/null and b/assets/images/google_adsense_code.png differ diff --git a/assets/images/google_adsense_infomation.png b/assets/images/google_adsense_infomation.png new file mode 100644 index 0000000..3f9a8e5 Binary files /dev/null and b/assets/images/google_adsense_infomation.png differ diff --git a/assets/images/google_adsense_main.png b/assets/images/google_adsense_main.png new file mode 100644 index 0000000..fe713c1 Binary files /dev/null and b/assets/images/google_adsense_main.png differ diff --git a/assets/images/google_search_console.png b/assets/images/google_search_console.png new file mode 100644 index 0000000..7f7f14f Binary files /dev/null and b/assets/images/google_search_console.png differ diff --git a/assets/images/google_search_console_sitemap.png b/assets/images/google_search_console_sitemap.png new file mode 100644 index 0000000..a9c1f6b Binary files /dev/null and b/assets/images/google_search_console_sitemap.png differ diff --git a/assets/images/google_search_console_verified.png b/assets/images/google_search_console_verified.png new file mode 100644 index 0000000..eb9e644 Binary files /dev/null and b/assets/images/google_search_console_verified.png differ diff --git a/assets/images/google_search_console_verify.png b/assets/images/google_search_console_verify.png new file mode 100644 index 0000000..01c0ed5 Binary files /dev/null and b/assets/images/google_search_console_verify.png differ diff --git a/assets/images/google_wallet_smart_tap_communication_flow_example1.png b/assets/images/google_wallet_smart_tap_communication_flow_example1.png new file mode 100644 index 0000000..e8846aa Binary files /dev/null and b/assets/images/google_wallet_smart_tap_communication_flow_example1.png differ diff --git a/assets/images/google_wallet_smart_tap_communication_flow_example2.png b/assets/images/google_wallet_smart_tap_communication_flow_example2.png new file mode 100644 index 0000000..2cac7b7 Binary files /dev/null and b/assets/images/google_wallet_smart_tap_communication_flow_example2.png differ diff --git a/assets/images/google_wallet_smart_tap_communication_flow_example3.png b/assets/images/google_wallet_smart_tap_communication_flow_example3.png new file mode 100644 index 0000000..fbbd198 Binary files /dev/null and b/assets/images/google_wallet_smart_tap_communication_flow_example3.png differ diff --git a/assets/images/google_wallet_smart_tap_public_key.png b/assets/images/google_wallet_smart_tap_public_key.png new file mode 100644 index 0000000..9470bbe Binary files /dev/null and b/assets/images/google_wallet_smart_tap_public_key.png differ diff --git a/assets/images/google_wallet_smart_tap_result1.png b/assets/images/google_wallet_smart_tap_result1.png new file mode 100644 index 0000000..630dc74 Binary files /dev/null and b/assets/images/google_wallet_smart_tap_result1.png differ diff --git a/assets/images/google_wallet_smart_tap_result2.png b/assets/images/google_wallet_smart_tap_result2.png new file mode 100644 index 0000000..f93edd5 Binary files /dev/null and b/assets/images/google_wallet_smart_tap_result2.png differ diff --git a/assets/images/google_wallet_smart_tap_result3.png b/assets/images/google_wallet_smart_tap_result3.png new file mode 100644 index 0000000..7524806 Binary files /dev/null and b/assets/images/google_wallet_smart_tap_result3.png differ diff --git a/assets/images/ios_uuid.png b/assets/images/ios_uuid.png new file mode 100644 index 0000000..333efcd Binary files /dev/null and b/assets/images/ios_uuid.png differ diff --git a/assets/images/ios_webrtc.png b/assets/images/ios_webrtc.png new file mode 100644 index 0000000..fa28b79 Binary files /dev/null and b/assets/images/ios_webrtc.png differ diff --git a/assets/images/jekyll_github_pages.png b/assets/images/jekyll_github_pages.png new file mode 100644 index 0000000..df86d4b Binary files /dev/null and b/assets/images/jekyll_github_pages.png differ diff --git a/assets/images/jekyll_local_test.png b/assets/images/jekyll_local_test.png new file mode 100644 index 0000000..78df4d6 Binary files /dev/null and b/assets/images/jekyll_local_test.png differ diff --git a/assets/images/jekyll_with_minimal_mistakes_theme_github_pages.png b/assets/images/jekyll_with_minimal_mistakes_theme_github_pages.png new file mode 100644 index 0000000..d174265 Binary files /dev/null and b/assets/images/jekyll_with_minimal_mistakes_theme_github_pages.png differ diff --git a/assets/images/jekyll_with_minimal_mistakes_theme_local_test.png b/assets/images/jekyll_with_minimal_mistakes_theme_local_test.png new file mode 100644 index 0000000..352cfda Binary files /dev/null and b/assets/images/jekyll_with_minimal_mistakes_theme_local_test.png differ diff --git a/assets/images/jenkins.jpg b/assets/images/jenkins.jpg new file mode 100644 index 0000000..863650a Binary files /dev/null and b/assets/images/jenkins.jpg differ diff --git a/assets/images/jenkins_setup_initialAdminPassword.png b/assets/images/jenkins_setup_initialAdminPassword.png new file mode 100644 index 0000000..12bbc0c Binary files /dev/null and b/assets/images/jenkins_setup_initialAdminPassword.png differ diff --git a/assets/images/jenkins_setup_main_page.png b/assets/images/jenkins_setup_main_page.png new file mode 100644 index 0000000..fb7da93 Binary files /dev/null and b/assets/images/jenkins_setup_main_page.png differ diff --git a/assets/images/jordan-harrison-40XgDxBfYXM-unsplash.jpg b/assets/images/jordan-harrison-40XgDxBfYXM-unsplash.jpg new file mode 100644 index 0000000..0d801bb Binary files /dev/null and b/assets/images/jordan-harrison-40XgDxBfYXM-unsplash.jpg differ diff --git a/assets/images/matter.jpg b/assets/images/matter.jpg new file mode 100644 index 0000000..6e2f2fe Binary files /dev/null and b/assets/images/matter.jpg differ diff --git a/assets/images/mika-baumeister-m7HWPWVjfJ4-unsplash.jpg b/assets/images/mika-baumeister-m7HWPWVjfJ4-unsplash.jpg new file mode 100644 index 0000000..9172a37 Binary files /dev/null and b/assets/images/mika-baumeister-m7HWPWVjfJ4-unsplash.jpg differ diff --git a/assets/images/nasa-1lfI7wkGWZ4-unsplash.jpg b/assets/images/nasa-1lfI7wkGWZ4-unsplash.jpg new file mode 100644 index 0000000..9599881 Binary files /dev/null and b/assets/images/nasa-1lfI7wkGWZ4-unsplash.jpg differ diff --git a/assets/images/nick-brunner-k4xDXNskVsQ-unsplash.jpg b/assets/images/nick-brunner-k4xDXNskVsQ-unsplash.jpg new file mode 100644 index 0000000..6b891b0 Binary files /dev/null and b/assets/images/nick-brunner-k4xDXNskVsQ-unsplash.jpg differ diff --git a/assets/images/octopress_github_pages.png b/assets/images/octopress_github_pages.png new file mode 100644 index 0000000..33e3417 Binary files /dev/null and b/assets/images/octopress_github_pages.png differ diff --git a/assets/images/p2p_centralized.png b/assets/images/p2p_centralized.png new file mode 100644 index 0000000..28bbb6f Binary files /dev/null and b/assets/images/p2p_centralized.png differ diff --git a/assets/images/p2p_centralized_connect.png b/assets/images/p2p_centralized_connect.png new file mode 100644 index 0000000..354b09b Binary files /dev/null and b/assets/images/p2p_centralized_connect.png differ diff --git a/assets/images/p2p_decentralized.png b/assets/images/p2p_decentralized.png new file mode 100644 index 0000000..d909fd5 Binary files /dev/null and b/assets/images/p2p_decentralized.png differ diff --git a/assets/images/p2p_distributed.png b/assets/images/p2p_distributed.png new file mode 100644 index 0000000..3d11bb4 Binary files /dev/null and b/assets/images/p2p_distributed.png differ diff --git a/assets/images/p2p_distributed_connect.png b/assets/images/p2p_distributed_connect.png new file mode 100644 index 0000000..a853f9e Binary files /dev/null and b/assets/images/p2p_distributed_connect.png differ diff --git a/assets/images/p2p_full_cone_nat.png b/assets/images/p2p_full_cone_nat.png new file mode 100644 index 0000000..912a495 Binary files /dev/null and b/assets/images/p2p_full_cone_nat.png differ diff --git a/assets/images/p2p_ice.png b/assets/images/p2p_ice.png new file mode 100644 index 0000000..2230cc3 Binary files /dev/null and b/assets/images/p2p_ice.png differ diff --git a/assets/images/p2p_nat_1.png b/assets/images/p2p_nat_1.png new file mode 100644 index 0000000..4230239 Binary files /dev/null and b/assets/images/p2p_nat_1.png differ diff --git a/assets/images/p2p_nat_2.png b/assets/images/p2p_nat_2.png new file mode 100644 index 0000000..6d1dd21 Binary files /dev/null and b/assets/images/p2p_nat_2.png differ diff --git a/assets/images/p2p_nat_3.png b/assets/images/p2p_nat_3.png new file mode 100644 index 0000000..cf22fa7 Binary files /dev/null and b/assets/images/p2p_nat_3.png differ diff --git a/assets/images/p2p_nat_4.png b/assets/images/p2p_nat_4.png new file mode 100644 index 0000000..f4e05ea Binary files /dev/null and b/assets/images/p2p_nat_4.png differ diff --git a/assets/images/p2p_nat_5.png b/assets/images/p2p_nat_5.png new file mode 100644 index 0000000..9a9fd02 Binary files /dev/null and b/assets/images/p2p_nat_5.png differ diff --git a/assets/images/p2p_nat_6.png b/assets/images/p2p_nat_6.png new file mode 100644 index 0000000..0e0837c Binary files /dev/null and b/assets/images/p2p_nat_6.png differ diff --git a/assets/images/p2p_port_restricted_cone_nat.png b/assets/images/p2p_port_restricted_cone_nat.png new file mode 100644 index 0000000..b03f990 Binary files /dev/null and b/assets/images/p2p_port_restricted_cone_nat.png differ diff --git a/assets/images/p2p_restricted_cone_nat.png b/assets/images/p2p_restricted_cone_nat.png new file mode 100644 index 0000000..a0698ee Binary files /dev/null and b/assets/images/p2p_restricted_cone_nat.png differ diff --git a/assets/images/p2p_stun.png b/assets/images/p2p_stun.png new file mode 100644 index 0000000..9b36dda Binary files /dev/null and b/assets/images/p2p_stun.png differ diff --git a/assets/images/p2p_symmetric_nat.png b/assets/images/p2p_symmetric_nat.png new file mode 100644 index 0000000..b932356 Binary files /dev/null and b/assets/images/p2p_symmetric_nat.png differ diff --git a/assets/images/p2p_turn.png b/assets/images/p2p_turn.png new file mode 100644 index 0000000..5de046e Binary files /dev/null and b/assets/images/p2p_turn.png differ diff --git a/assets/images/p2p_webrtc.png b/assets/images/p2p_webrtc.png new file mode 100644 index 0000000..99a6078 Binary files /dev/null and b/assets/images/p2p_webrtc.png differ diff --git a/assets/images/rsa-algorithm.jpg b/assets/images/rsa-algorithm.jpg new file mode 100644 index 0000000..2b557c8 Binary files /dev/null and b/assets/images/rsa-algorithm.jpg differ diff --git a/assets/images/taptap_app_copy.png b/assets/images/taptap_app_copy.png new file mode 100644 index 0000000..c8a67bd Binary files /dev/null and b/assets/images/taptap_app_copy.png differ diff --git a/assets/images/taptap_app_edit.png b/assets/images/taptap_app_edit.png new file mode 100644 index 0000000..7e05a65 Binary files /dev/null and b/assets/images/taptap_app_edit.png differ diff --git a/assets/images/taptap_app_paste.png b/assets/images/taptap_app_paste.png new file mode 100644 index 0000000..04fab66 Binary files /dev/null and b/assets/images/taptap_app_paste.png differ diff --git a/assets/images/v2osk-c9OfrVeD_tQ-unsplash.jpg b/assets/images/v2osk-c9OfrVeD_tQ-unsplash.jpg new file mode 100644 index 0000000..3057065 Binary files /dev/null and b/assets/images/v2osk-c9OfrVeD_tQ-unsplash.jpg differ diff --git a/assets/images/wireshark_rvi0_interface.png b/assets/images/wireshark_rvi0_interface.png new file mode 100644 index 0000000..3129228 Binary files /dev/null and b/assets/images/wireshark_rvi0_interface.png differ diff --git a/assets/images/wireshark_test_1.png b/assets/images/wireshark_test_1.png new file mode 100644 index 0000000..cfc3498 Binary files /dev/null and b/assets/images/wireshark_test_1.png differ diff --git a/assets/images/wireshark_test_2.png b/assets/images/wireshark_test_2.png new file mode 100644 index 0000000..6331950 Binary files /dev/null and b/assets/images/wireshark_test_2.png differ diff --git a/assets/js/_main.js b/assets/js/_main.js new file mode 100644 index 0000000..0efc970 --- /dev/null +++ b/assets/js/_main.js @@ -0,0 +1,136 @@ +/* ========================================================================== + jQuery plugin settings and other scripts + ========================================================================== */ + +$(document).ready(function() { + // FitVids init + $("#main").fitVids(); + + // Sticky sidebar + var stickySideBar = function() { + var show = + $(".author__urls-wrapper button").length === 0 + ? $(window).width() > 1024 // width should match $large Sass variable + : !$(".author__urls-wrapper button").is(":visible"); + if (show) { + // fix + $(".sidebar").addClass("sticky"); + } else { + // unfix + $(".sidebar").removeClass("sticky"); + } + }; + + stickySideBar(); + + $(window).resize(function() { + stickySideBar(); + }); + + // Follow menu drop down + $(".author__urls-wrapper button").on("click", function() { + $(".author__urls").toggleClass("is--visible"); + $(".author__urls-wrapper button").toggleClass("open"); + }); + + // Close search screen with Esc key + $(document).keyup(function(e) { + if (e.keyCode === 27) { + if ($(".initial-content").hasClass("is--hidden")) { + $(".search-content").toggleClass("is--visible"); + $(".initial-content").toggleClass("is--hidden"); + } + } + }); + + // Search toggle + $(".search__toggle").on("click", function() { + $(".search-content").toggleClass("is--visible"); + $(".initial-content").toggleClass("is--hidden"); + // set focus on input + setTimeout(function() { + $(".search-content input").focus(); + }, 400); + }); + + // Smooth scrolling + var scroll = new SmoothScroll('a[href*="#"]', { + offset: 20, + speed: 400, + speedAsDuration: true, + durationMax: 500 + }); + + // Gumshoe scroll spy init + if($("nav.toc").length > 0) { + var spy = new Gumshoe("nav.toc a", { + // Active classes + navClass: "active", // applied to the nav list item + contentClass: "active", // applied to the content + + // Nested navigation + nested: false, // if true, add classes to parents of active link + nestedClass: "active", // applied to the parent items + + // Offset & reflow + offset: 20, // how far from the top of the page to activate a content area + reflow: true, // if true, listen for reflows + + // Event support + events: true // if true, emit custom events + }); + } + + // add lightbox class to all image links + $( + "a[href$='.jpg'],a[href$='.jpeg'],a[href$='.JPG'],a[href$='.png'],a[href$='.gif'],a[href$='.webp']" + ).addClass("image-popup"); + + // Magnific-Popup options + $(".image-popup").magnificPopup({ + // disableOn: function() { + // if( $(window).width() < 500 ) { + // return false; + // } + // return true; + // }, + type: "image", + tLoading: "Loading image #%curr%...", + gallery: { + enabled: true, + navigateByImgClick: true, + preload: [0, 1] // Will preload 0 - before current, and 1 after the current image + }, + image: { + tError: 'Image #%curr% could not be loaded.' + }, + removalDelay: 500, // Delay in milliseconds before popup is removed + // Class that is added to body when popup is open. + // make it unique to apply your CSS animations just to this exact popup + mainClass: "mfp-zoom-in", + callbacks: { + beforeOpen: function() { + // just a hack that adds mfp-anim class to markup + this.st.image.markup = this.st.image.markup.replace( + "mfp-figure", + "mfp-figure mfp-with-anim" + ); + } + }, + closeOnContentClick: true, + midClick: true // allow opening popup on middle mouse click. Always set it to true if you don't provide alternative source. + }); + + // Add anchors for headings + $('.page__content').find('h1, h2, h3, h4, h5, h6').each(function() { + var id = $(this).attr('id'); + if (id) { + var anchor = document.createElement("a"); + anchor.className = 'header-link'; + anchor.href = '#' + id; + anchor.innerHTML = 'Permalink'; + anchor.title = "Permalink"; + $(this).append(anchor); + } + }); +}); diff --git a/assets/js/lunr/lunr-en.js b/assets/js/lunr/lunr-en.js new file mode 100644 index 0000000..d1400a7 --- /dev/null +++ b/assets/js/lunr/lunr-en.js @@ -0,0 +1,69 @@ +var idx = lunr(function () { + this.field('title') + this.field('excerpt') + this.field('categories') + this.field('tags') + this.ref('id') + + this.pipeline.remove(lunr.trimmer) + + for (var item in store) { + this.add({ + title: store[item].title, + excerpt: store[item].excerpt, + categories: store[item].categories, + tags: store[item].tags, + id: item + }) + } +}); + +$(document).ready(function() { + $('input#search').on('keyup', function () { + var resultdiv = $('#results'); + var query = $(this).val().toLowerCase(); + var result = + idx.query(function (q) { + query.split(lunr.tokenizer.separator).forEach(function (term) { + q.term(term, { boost: 100 }) + if(query.lastIndexOf(" ") != query.length-1){ + q.term(term, { usePipeline: false, wildcard: lunr.Query.wildcard.TRAILING, boost: 10 }) + } + if (term != ""){ + q.term(term, { usePipeline: false, editDistance: 1, boost: 1 }) + } + }) + }); + resultdiv.empty(); + resultdiv.prepend('

'+result.length+' Result(s) found

'); + for (var item in result) { + var ref = result[item].ref; + if(store[ref].teaser){ + var searchitem = + '
'+ + '
'+ + '

'+ + ''+store[ref].title+''+ + '

'+ + '
'+ + ''+ + '
'+ + '

'+store[ref].excerpt.split(" ").splice(0,20).join(" ")+'...

'+ + '
'+ + '
'; + } + else{ + var searchitem = + '
'+ + '
'+ + '

'+ + ''+store[ref].title+''+ + '

'+ + '

'+store[ref].excerpt.split(" ").splice(0,20).join(" ")+'...

'+ + '
'+ + '
'; + } + resultdiv.append(searchitem); + } + }); +}); diff --git a/assets/js/lunr/lunr-gr.js b/assets/js/lunr/lunr-gr.js new file mode 100644 index 0000000..e829362 --- /dev/null +++ b/assets/js/lunr/lunr-gr.js @@ -0,0 +1,522 @@ +step1list = new Array(); +step1list["ΦΑΓΙΑ"] = "ΦΑ"; +step1list["ΦΑΓΙΟΥ"] = "ΦΑ"; +step1list["ΦΑΓΙΩΝ"] = "ΦΑ"; +step1list["ΣΚΑΓΙΑ"] = "ΣΚΑ"; +step1list["ΣΚΑΓΙΟΥ"] = "ΣΚΑ"; +step1list["ΣΚΑΓΙΩΝ"] = "ΣΚΑ"; +step1list["ΟΛΟΓΙΟΥ"] = "ΟΛΟ"; +step1list["ΟΛΟΓΙΑ"] = "ΟΛΟ"; +step1list["ΟΛΟΓΙΩΝ"] = "ΟΛΟ"; +step1list["ΣΟΓΙΟΥ"] = "ΣΟ"; +step1list["ΣΟΓΙΑ"] = "ΣΟ"; +step1list["ΣΟΓΙΩΝ"] = "ΣΟ"; +step1list["ΤΑΤΟΓΙΑ"] = "ΤΑΤΟ"; +step1list["ΤΑΤΟΓΙΟΥ"] = "ΤΑΤΟ"; +step1list["ΤΑΤΟΓΙΩΝ"] = "ΤΑΤΟ"; +step1list["ΚΡΕΑΣ"] = "ΚΡΕ"; +step1list["ΚΡΕΑΤΟΣ"] = "ΚΡΕ"; +step1list["ΚΡΕΑΤΑ"] = "ΚΡΕ"; +step1list["ΚΡΕΑΤΩΝ"] = "ΚΡΕ"; +step1list["ΠΕΡΑΣ"] = "ΠΕΡ"; +step1list["ΠΕΡΑΤΟΣ"] = "ΠΕΡ"; +step1list["ΠΕΡΑΤΑ"] = "ΠΕΡ"; +step1list["ΠΕΡΑΤΩΝ"] = "ΠΕΡ"; +step1list["ΤΕΡΑΣ"] = "ΤΕΡ"; +step1list["ΤΕΡΑΤΟΣ"] = "ΤΕΡ"; +step1list["ΤΕΡΑΤΑ"] = "ΤΕΡ"; +step1list["ΤΕΡΑΤΩΝ"] = "ΤΕΡ"; +step1list["ΦΩΣ"] = "ΦΩ"; +step1list["ΦΩΤΟΣ"] = "ΦΩ"; +step1list["ΦΩΤΑ"] = "ΦΩ"; +step1list["ΦΩΤΩΝ"] = "ΦΩ"; +step1list["ΚΑΘΕΣΤΩΣ"] = "ΚΑΘΕΣΤ"; +step1list["ΚΑΘΕΣΤΩΤΟΣ"] = "ΚΑΘΕΣΤ"; +step1list["ΚΑΘΕΣΤΩΤΑ"] = "ΚΑΘΕΣΤ"; +step1list["ΚΑΘΕΣΤΩΤΩΝ"] = "ΚΑΘΕΣΤ"; +step1list["ΓΕΓΟΝΟΣ"] = "ΓΕΓΟΝ"; +step1list["ΓΕΓΟΝΟΤΟΣ"] = "ΓΕΓΟΝ"; +step1list["ΓΕΓΟΝΟΤΑ"] = "ΓΕΓΟΝ"; +step1list["ΓΕΓΟΝΟΤΩΝ"] = "ΓΕΓΟΝ"; + +v = "[ΑΕΗΙΟΥΩ]"; +v2 = "[ΑΕΗΙΟΩ]" + +function stemWord(w) { + var stem; + var suffix; + var firstch; + var origword = w; + test1 = new Boolean(true); + + if(w.length < 4) { + return w; + } + + var re; + var re2; + var re3; + var re4; + + re = /(.*)(ΦΑΓΙΑ|ΦΑΓΙΟΥ|ΦΑΓΙΩΝ|ΣΚΑΓΙΑ|ΣΚΑΓΙΟΥ|ΣΚΑΓΙΩΝ|ΟΛΟΓΙΟΥ|ΟΛΟΓΙΑ|ΟΛΟΓΙΩΝ|ΣΟΓΙΟΥ|ΣΟΓΙΑ|ΣΟΓΙΩΝ|ΤΑΤΟΓΙΑ|ΤΑΤΟΓΙΟΥ|ΤΑΤΟΓΙΩΝ|ΚΡΕΑΣ|ΚΡΕΑΤΟΣ|ΚΡΕΑΤΑ|ΚΡΕΑΤΩΝ|ΠΕΡΑΣ|ΠΕΡΑΤΟΣ|ΠΕΡΑΤΑ|ΠΕΡΑΤΩΝ|ΤΕΡΑΣ|ΤΕΡΑΤΟΣ|ΤΕΡΑΤΑ|ΤΕΡΑΤΩΝ|ΦΩΣ|ΦΩΤΟΣ|ΦΩΤΑ|ΦΩΤΩΝ|ΚΑΘΕΣΤΩΣ|ΚΑΘΕΣΤΩΤΟΣ|ΚΑΘΕΣΤΩΤΑ|ΚΑΘΕΣΤΩΤΩΝ|ΓΕΓΟΝΟΣ|ΓΕΓΟΝΟΤΟΣ|ΓΕΓΟΝΟΤΑ|ΓΕΓΟΝΟΤΩΝ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + w = stem + step1list[suffix]; + test1 = false; + } + + re = /^(.+?)(ΑΔΕΣ|ΑΔΩΝ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + + reg1 = /(ΟΚ|ΜΑΜ|ΜΑΝ|ΜΠΑΜΠ|ΠΑΤΕΡ|ΓΙΑΓΙ|ΝΤΑΝΤ|ΚΥΡ|ΘΕΙ|ΠΕΘΕΡ)$/; + + if(!(reg1.test(w))) { + w = w + "ΑΔ"; + } + } + + re2 = /^(.+?)(ΕΔΕΣ|ΕΔΩΝ)$/; + + if(re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + w = stem; + + exept2 = /(ΟΠ|ΙΠ|ΕΜΠ|ΥΠ|ΓΗΠ|ΔΑΠ|ΚΡΑΣΠ|ΜΙΛ)$/; + + if(exept2.test(w)) { + w = w + "ΕΔ"; + } + } + + re3 = /^(.+?)(ΟΥΔΕΣ|ΟΥΔΩΝ)$/; + + if(re3.test(w)) { + var fp = re3.exec(w); + stem = fp[1]; + w = stem; + + exept3 = /(ΑΡΚ|ΚΑΛΙΑΚ|ΠΕΤΑΛ|ΛΙΧ|ΠΛΕΞ|ΣΚ|Σ|ΦΛ|ΦΡ|ΒΕΛ|ΛΟΥΛ|ΧΝ|ΣΠ|ΤΡΑΓ|ΦΕ)$/; + + if(exept3.test(w)) { + w = w + "ΟΥΔ"; + } + } + + re4 = /^(.+?)(ΕΩΣ|ΕΩΝ)$/; + + if(re4.test(w)) { + var fp = re4.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept4 = /^(Θ|Δ|ΕΛ|ΓΑΛ|Ν|Π|ΙΔ|ΠΑΡ)$/; + + if(exept4.test(w)) { + w = w + "Ε"; + } + } + + re = /^(.+?)(ΙΑ|ΙΟΥ|ΙΩΝ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + re2 = new RegExp(v + "$"); + test1 = false; + + if(re2.test(w)) { + w = stem + "Ι"; + } + } + + re = /^(.+?)(ΙΚΑ|ΙΚΟ|ΙΚΟΥ|ΙΚΩΝ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + re2 = new RegExp(v + "$"); + exept5 = /^(ΑΛ|ΑΔ|ΕΝΔ|ΑΜΑΝ|ΑΜΜΟΧΑΛ|ΗΘ|ΑΝΗΘ|ΑΝΤΙΔ|ΦΥΣ|ΒΡΩΜ|ΓΕΡ|ΕΞΩΔ|ΚΑΛΠ|ΚΑΛΛΙΝ|ΚΑΤΑΔ|ΜΟΥΛ|ΜΠΑΝ|ΜΠΑΓΙΑΤ|ΜΠΟΛ|ΜΠΟΣ|ΝΙΤ|ΞΙΚ|ΣΥΝΟΜΗΛ|ΠΕΤΣ|ΠΙΤΣ|ΠΙΚΑΝΤ|ΠΛΙΑΤΣ|ΠΟΣΤΕΛΝ|ΠΡΩΤΟΔ|ΣΕΡΤ|ΣΥΝΑΔ|ΤΣΑΜ|ΥΠΟΔ|ΦΙΛΟΝ|ΦΥΛΟΔ|ΧΑΣ)$/; + + if((exept5.test(w)) || (re2.test(w))) { + w = w + "ΙΚ"; + } + } + + re = /^(.+?)(ΑΜΕ)$/; + re2 = /^(.+?)(ΑΓΑΜΕ|ΗΣΑΜΕ|ΟΥΣΑΜΕ|ΗΚΑΜΕ|ΗΘΗΚΑΜΕ)$/; + if(w == "ΑΓΑΜΕ") { + w = "ΑΓΑΜ"; + } + + if(re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + } + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept6 = /^(ΑΝΑΠ|ΑΠΟΘ|ΑΠΟΚ|ΑΠΟΣΤ|ΒΟΥΒ|ΞΕΘ|ΟΥΛ|ΠΕΘ|ΠΙΚΡ|ΠΟΤ|ΣΙΧ|Χ)$/; + + if(exept6.test(w)) { + w = w + "ΑΜ"; + } + } + + re2 = /^(.+?)(ΑΝΕ)$/; + re3 = /^(.+?)(ΑΓΑΝΕ|ΗΣΑΝΕ|ΟΥΣΑΝΕ|ΙΟΝΤΑΝΕ|ΙΟΤΑΝΕ|ΙΟΥΝΤΑΝΕ|ΟΝΤΑΝΕ|ΟΤΑΝΕ|ΟΥΝΤΑΝΕ|ΗΚΑΝΕ|ΗΘΗΚΑΝΕ)$/; + + if(re3.test(w)) { + var fp = re3.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + re3 = /^(ΤΡ|ΤΣ)$/; + + if(re3.test(w)) { + w = w + "ΑΓΑΝ"; + } + } + + if(re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + re2 = new RegExp(v2 + "$"); + exept7 = /^(ΒΕΤΕΡ|ΒΟΥΛΚ|ΒΡΑΧΜ|Γ|ΔΡΑΔΟΥΜ|Θ|ΚΑΛΠΟΥΖ|ΚΑΣΤΕΛ|ΚΟΡΜΟΡ|ΛΑΟΠΛ|ΜΩΑΜΕΘ|Μ|ΜΟΥΣΟΥΛΜ|Ν|ΟΥΛ|Π|ΠΕΛΕΚ|ΠΛ|ΠΟΛΙΣ|ΠΟΡΤΟΛ|ΣΑΡΑΚΑΤΣ|ΣΟΥΛΤ|ΤΣΑΡΛΑΤ|ΟΡΦ|ΤΣΙΓΓ|ΤΣΟΠ|ΦΩΤΟΣΤΕΦ|Χ|ΨΥΧΟΠΛ|ΑΓ|ΟΡΦ|ΓΑΛ|ΓΕΡ|ΔΕΚ|ΔΙΠΛ|ΑΜΕΡΙΚΑΝ|ΟΥΡ|ΠΙΘ|ΠΟΥΡΙΤ|Σ|ΖΩΝΤ|ΙΚ|ΚΑΣΤ|ΚΟΠ|ΛΙΧ|ΛΟΥΘΗΡ|ΜΑΙΝΤ|ΜΕΛ|ΣΙΓ|ΣΠ|ΣΤΕΓ|ΤΡΑΓ|ΤΣΑΓ|Φ|ΕΡ|ΑΔΑΠ|ΑΘΙΓΓ|ΑΜΗΧ|ΑΝΙΚ|ΑΝΟΡΓ|ΑΠΗΓ|ΑΠΙΘ|ΑΤΣΙΓΓ|ΒΑΣ|ΒΑΣΚ|ΒΑΘΥΓΑΛ|ΒΙΟΜΗΧ|ΒΡΑΧΥΚ|ΔΙΑΤ|ΔΙΑΦ|ΕΝΟΡΓ|ΘΥΣ|ΚΑΠΝΟΒΙΟΜΗΧ|ΚΑΤΑΓΑΛ|ΚΛΙΒ|ΚΟΙΛΑΡΦ|ΛΙΒ|ΜΕΓΛΟΒΙΟΜΗΧ|ΜΙΚΡΟΒΙΟΜΗΧ|ΝΤΑΒ|ΞΗΡΟΚΛΙΒ|ΟΛΙΓΟΔΑΜ|ΟΛΟΓΑΛ|ΠΕΝΤΑΡΦ|ΠΕΡΗΦ|ΠΕΡΙΤΡ|ΠΛΑΤ|ΠΟΛΥΔΑΠ|ΠΟΛΥΜΗΧ|ΣΤΕΦ|ΤΑΒ|ΤΕΤ|ΥΠΕΡΗΦ|ΥΠΟΚΟΠ|ΧΑΜΗΛΟΔΑΠ|ΨΗΛΟΤΑΒ)$/; + + if((re2.test(w)) || (exept7.test(w))) { + w = w + "ΑΝ"; + } + } + + re3 = /^(.+?)(ΕΤΕ)$/; + re4 = /^(.+?)(ΗΣΕΤΕ)$/; + + if(re4.test(w)) { + var fp = re4.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + } + + if(re3.test(w)) { + var fp = re3.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + re3 = new RegExp(v2 + "$"); + exept8 = /(ΟΔ|ΑΙΡ|ΦΟΡ|ΤΑΘ|ΔΙΑΘ|ΣΧ|ΕΝΔ|ΕΥΡ|ΤΙΘ|ΥΠΕΡΘ|ΡΑΘ|ΕΝΘ|ΡΟΘ|ΣΘ|ΠΥΡ|ΑΙΝ|ΣΥΝΔ|ΣΥΝ|ΣΥΝΘ|ΧΩΡ|ΠΟΝ|ΒΡ|ΚΑΘ|ΕΥΘ|ΕΚΘ|ΝΕΤ|ΡΟΝ|ΑΡΚ|ΒΑΡ|ΒΟΛ|ΩΦΕΛ)$/; + exept9 = /^(ΑΒΑΡ|ΒΕΝ|ΕΝΑΡ|ΑΒΡ|ΑΔ|ΑΘ|ΑΝ|ΑΠΛ|ΒΑΡΟΝ|ΝΤΡ|ΣΚ|ΚΟΠ|ΜΠΟΡ|ΝΙΦ|ΠΑΓ|ΠΑΡΑΚΑΛ|ΣΕΡΠ|ΣΚΕΛ|ΣΥΡΦ|ΤΟΚ|Υ|Δ|ΕΜ|ΘΑΡΡ|Θ)$/; + + if((re3.test(w)) || (exept8.test(w)) || (exept9.test(w))) { + w = w + "ΕΤ"; + } + } + + re = /^(.+?)(ΟΝΤΑΣ|ΩΝΤΑΣ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept10 = /^(ΑΡΧ)$/; + exept11 = /(ΚΡΕ)$/; + if(exept10.test(w)) { + w = w + "ΟΝΤ"; + } + if(exept11.test(w)) { + w = w + "ΩΝΤ"; + } + } + + re = /^(.+?)(ΟΜΑΣΤΕ|ΙΟΜΑΣΤΕ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept11 = /^(ΟΝ)$/; + + if(exept11.test(w)) { + w = w + "ΟΜΑΣΤ"; + } + } + + re = /^(.+?)(ΕΣΤΕ)$/; + re2 = /^(.+?)(ΙΕΣΤΕ)$/; + + if(re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + re2 = /^(Π|ΑΠ|ΣΥΜΠ|ΑΣΥΜΠ|ΑΚΑΤΑΠ|ΑΜΕΤΑΜΦ)$/; + + if(re2.test(w)) { + w = w + "ΙΕΣΤ"; + } + } + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept12 = /^(ΑΛ|ΑΡ|ΕΚΤΕΛ|Ζ|Μ|Ξ|ΠΑΡΑΚΑΛ|ΑΡ|ΠΡΟ|ΝΙΣ)$/; + + if(exept12.test(w)) { + w = w + "ΕΣΤ"; + } + } + + re = /^(.+?)(ΗΚΑ|ΗΚΕΣ|ΗΚΕ)$/; + re2 = /^(.+?)(ΗΘΗΚΑ|ΗΘΗΚΕΣ|ΗΘΗΚΕ)$/; + + if(re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + } + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept13 = /(ΣΚΩΛ|ΣΚΟΥΛ|ΝΑΡΘ|ΣΦ|ΟΘ|ΠΙΘ)$/; + exept14 = /^(ΔΙΑΘ|Θ|ΠΑΡΑΚΑΤΑΘ|ΠΡΟΣΘ|ΣΥΝΘ|)$/; + + if((exept13.test(w)) || (exept14.test(w))) { + w = w + "ΗΚ"; + } + } + + re = /^(.+?)(ΟΥΣΑ|ΟΥΣΕΣ|ΟΥΣΕ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept15 = /^(ΦΑΡΜΑΚ|ΧΑΔ|ΑΓΚ|ΑΝΑΡΡ|ΒΡΟΜ|ΕΚΛΙΠ|ΛΑΜΠΙΔ|ΛΕΧ|Μ|ΠΑΤ|Ρ|Λ|ΜΕΔ|ΜΕΣΑΖ|ΥΠΟΤΕΙΝ|ΑΜ|ΑΙΘ|ΑΝΗΚ|ΔΕΣΠΟΖ|ΕΝΔΙΑΦΕΡ|ΔΕ|ΔΕΥΤΕΡΕΥ|ΚΑΘΑΡΕΥ|ΠΛΕ|ΤΣΑ)$/; + exept16 = /(ΠΟΔΑΡ|ΒΛΕΠ|ΠΑΝΤΑΧ|ΦΡΥΔ|ΜΑΝΤΙΛ|ΜΑΛΛ|ΚΥΜΑΤ|ΛΑΧ|ΛΗΓ|ΦΑΓ|ΟΜ|ΠΡΩΤ)$/; + + if((exept15.test(w)) || (exept16.test(w))) { + w = w + "ΟΥΣ"; + } + } + + re = /^(.+?)(ΑΓΑ|ΑΓΕΣ|ΑΓΕ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept17 = /^(ΨΟΦ|ΝΑΥΛΟΧ)$/; + exept20 = /(ΚΟΛΛ)$/; + exept18 = /^(ΑΒΑΣΤ|ΠΟΛΥΦ|ΑΔΗΦ|ΠΑΜΦ|Ρ|ΑΣΠ|ΑΦ|ΑΜΑΛ|ΑΜΑΛΛΙ|ΑΝΥΣΤ|ΑΠΕΡ|ΑΣΠΑΡ|ΑΧΑΡ|ΔΕΡΒΕΝ|ΔΡΟΣΟΠ|ΞΕΦ|ΝΕΟΠ|ΝΟΜΟΤ|ΟΛΟΠ|ΟΜΟΤ|ΠΡΟΣΤ|ΠΡΟΣΩΠΟΠ|ΣΥΜΠ|ΣΥΝΤ|Τ|ΥΠΟΤ|ΧΑΡ|ΑΕΙΠ|ΑΙΜΟΣΤ|ΑΝΥΠ|ΑΠΟΤ|ΑΡΤΙΠ|ΔΙΑΤ|ΕΝ|ΕΠΙΤ|ΚΡΟΚΑΛΟΠ|ΣΙΔΗΡΟΠ|Λ|ΝΑΥ|ΟΥΛΑΜ|ΟΥΡ|Π|ΤΡ|Μ)$/; + exept19 = /(ΟΦ|ΠΕΛ|ΧΟΡΤ|ΛΛ|ΣΦ|ΡΠ|ΦΡ|ΠΡ|ΛΟΧ|ΣΜΗΝ)$/; + + if(((exept18.test(w)) || (exept19.test(w))) && !((exept17.test(w)) || (exept20.test(w)))) { + w = w + "ΑΓ"; + } + } + + re = /^(.+?)(ΗΣΕ|ΗΣΟΥ|ΗΣΑ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept21 = /^(Ν|ΧΕΡΣΟΝ|ΔΩΔΕΚΑΝ|ΕΡΗΜΟΝ|ΜΕΓΑΛΟΝ|ΕΠΤΑΝ)$/; + + if(exept21.test(w)) { + w = w + "ΗΣ"; + } + } + + re = /^(.+?)(ΗΣΤΕ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept22 = /^(ΑΣΒ|ΣΒ|ΑΧΡ|ΧΡ|ΑΠΛ|ΑΕΙΜΝ|ΔΥΣΧΡ|ΕΥΧΡ|ΚΟΙΝΟΧΡ|ΠΑΛΙΜΨ)$/; + + if(exept22.test(w)) { + w = w + "ΗΣΤ"; + } + } + + re = /^(.+?)(ΟΥΝΕ|ΗΣΟΥΝΕ|ΗΘΟΥΝΕ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept23 = /^(Ν|Ρ|ΣΠΙ|ΣΤΡΑΒΟΜΟΥΤΣ|ΚΑΚΟΜΟΥΤΣ|ΕΞΩΝ)$/; + + if(exept23.test(w)) { + w = w + "ΟΥΝ"; + } + } + + re = /^(.+?)(ΟΥΜΕ|ΗΣΟΥΜΕ|ΗΘΟΥΜΕ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + test1 = false; + + exept24 = /^(ΠΑΡΑΣΟΥΣ|Φ|Χ|ΩΡΙΟΠΛ|ΑΖ|ΑΛΛΟΣΟΥΣ|ΑΣΟΥΣ)$/; + + if(exept24.test(w)) { + w = w + "ΟΥΜ"; + } + } + + re = /^(.+?)(ΜΑΤΑ|ΜΑΤΩΝ|ΜΑΤΟΣ)$/; + re2 = /^(.+?)(Α|ΑΓΑΤΕ|ΑΓΑΝ|ΑΕΙ|ΑΜΑΙ|ΑΝ|ΑΣ|ΑΣΑΙ|ΑΤΑΙ|ΑΩ|Ε|ΕΙ|ΕΙΣ|ΕΙΤΕ|ΕΣΑΙ|ΕΣ|ΕΤΑΙ|Ι|ΙΕΜΑΙ|ΙΕΜΑΣΤΕ|ΙΕΤΑΙ|ΙΕΣΑΙ|ΙΕΣΑΣΤΕ|ΙΟΜΑΣΤΑΝ|ΙΟΜΟΥΝ|ΙΟΜΟΥΝΑ|ΙΟΝΤΑΝ|ΙΟΝΤΟΥΣΑΝ|ΙΟΣΑΣΤΑΝ|ΙΟΣΑΣΤΕ|ΙΟΣΟΥΝ|ΙΟΣΟΥΝΑ|ΙΟΤΑΝ|ΙΟΥΜΑ|ΙΟΥΜΑΣΤΕ|ΙΟΥΝΤΑΙ|ΙΟΥΝΤΑΝ|Η|ΗΔΕΣ|ΗΔΩΝ|ΗΘΕΙ|ΗΘΕΙΣ|ΗΘΕΙΤΕ|ΗΘΗΚΑΤΕ|ΗΘΗΚΑΝ|ΗΘΟΥΝ|ΗΘΩ|ΗΚΑΤΕ|ΗΚΑΝ|ΗΣ|ΗΣΑΝ|ΗΣΑΤΕ|ΗΣΕΙ|ΗΣΕΣ|ΗΣΟΥΝ|ΗΣΩ|Ο|ΟΙ|ΟΜΑΙ|ΟΜΑΣΤΑΝ|ΟΜΟΥΝ|ΟΜΟΥΝΑ|ΟΝΤΑΙ|ΟΝΤΑΝ|ΟΝΤΟΥΣΑΝ|ΟΣ|ΟΣΑΣΤΑΝ|ΟΣΑΣΤΕ|ΟΣΟΥΝ|ΟΣΟΥΝΑ|ΟΤΑΝ|ΟΥ|ΟΥΜΑΙ|ΟΥΜΑΣΤΕ|ΟΥΝ|ΟΥΝΤΑΙ|ΟΥΝΤΑΝ|ΟΥΣ|ΟΥΣΑΝ|ΟΥΣΑΤΕ|Υ|ΥΣ|Ω|ΩΝ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "ΜΑ"; + } + + if((re2.test(w)) && (test1)) { + var fp = re2.exec(w); + stem = fp[1]; + w = stem; + + } + + re = /^(.+?)(ΕΣΤΕΡ|ΕΣΤΑΤ|ΟΤΕΡ|ΟΤΑΤ|ΥΤΕΡ|ΥΤΑΤ|ΩΤΕΡ|ΩΤΑΤ)$/; + + if(re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem; + } + + return w; +}; + +var greekStemmer = function (token) { + return token.update(function (word) { + return stemWord(word); + }) +} + +var idx = lunr(function () { + this.field('title') + this.field('excerpt') + this.field('categories') + this.field('tags') + this.ref('id') + + this.pipeline.remove(lunr.trimmer) + this.pipeline.add(greekStemmer) + this.pipeline.remove(lunr.stemmer) + + for (var item in store) { + this.add({ + title: store[item].title, + excerpt: store[item].excerpt, + categories: store[item].categories, + tags: store[item].tags, + id: item + }) + } +}); + +$(document).ready(function() { + $('input#search').on('keyup', function () { + var resultdiv = $('#results'); + var query = $(this).val().toLowerCase(); + var result = + idx.query(function (q) { + query.split(lunr.tokenizer.separator).forEach(function (term) { + q.term(term, { boost: 100 }) + if(query.lastIndexOf(" ") != query.length-1){ + q.term(term, { usePipeline: false, wildcard: lunr.Query.wildcard.TRAILING, boost: 10 }) + } + if (term != ""){ + q.term(term, { usePipeline: false, editDistance: 1, boost: 1 }) + } + }) + }); + resultdiv.empty(); + resultdiv.prepend('

'+result.length+' Result(s) found

'); + for (var item in result) { + var ref = result[item].ref; + if(store[ref].teaser){ + var searchitem = + '
'+ + '
'+ + '

'+ + ''+store[ref].title+''+ + '

'+ + '
'+ + ''+ + '
'+ + '

'+store[ref].excerpt.split(" ").splice(0,20).join(" ")+'...

'+ + '
'+ + '
'; + } + else{ + var searchitem = + '
'+ + '
'+ + '

'+ + ''+store[ref].title+''+ + '

'+ + '

'+store[ref].excerpt.split(" ").splice(0,20).join(" ")+'...

'+ + '
'+ + '
'; + } + resultdiv.append(searchitem); + } + }); +}); diff --git a/assets/js/lunr/lunr-store.js b/assets/js/lunr/lunr-store.js new file mode 100644 index 0000000..53a15a8 --- /dev/null +++ b/assets/js/lunr/lunr-store.js @@ -0,0 +1,265 @@ +var store = [{ + "title": "使用 Octopress 架設靜態 Blog - Octopress x GitHub pages x Blog", + "excerpt":"為什麼要寫 Blog? 工作中受到了很多教學網站及博客文章(簡書, CSDN, Medium…)非常多的幫助,因此想寫部落格將自己學到的技術記錄下來,一方面讓自己複習,一方面也希望能幫助到有需要的人。 為什麼用 Octopress? 其實有很多現成免費的部落格平台像是 Medium, Blogger 等…,但最後我還是決定用 Octopress 來建立自己的部落格,它吸引我的原因如下: 使用 Git 做版本控管,託管於 Github 使用 Markdown 寫文章 可以學到東西 免費 Git 對軟體工程師再熟悉不過了,且託管於工程師最愛充滿開源專案的 Github 平台,用 Markdown 寫文章,也能訓練自己寫 README 語法的熟練程度,又可以學到東西,因此選擇 Octopress。 事前準備 申請Github帳號 安裝Git brew install git 安裝Ruby brew install ruby 確認版本 ruby --version 建置 Octopress git clone git://github.com/imathis/octopress.git octopress...","categories": ["Blog"], + "tags": ["Octopress"], + "url": "/blog/blog/octopress-setup/", + "teaser": "/blog/assets/images/alfons-morales-YLSwjSy7stw-unsplash.jpg" + },{ + "title": "如何使 Octopress website 能被 Google 搜索到 - Google x Search x Blog", + "excerpt":"在 Google 搜尋不到我的 Blog 當我們在建立完 Jekyll Blog 後,會發現竟然 Google 不到我們的 Blog 網站,別擔心這是因為我們尚未將網站提交加入 Google Search Console 中 將網站加入 Google Search Console 進入 Google Search Console 這邊使用網址前綴方式添加,加你的 Blog domain 填入並繼續 下載 googlexxxxxxxxxx.html 檔案放到 octopress/source 目錄下,commit 上傳到 GitHub 上,點擊驗證 rake gen_deploy 驗證成功 👍 Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂 ","categories": ["Blog"], + "tags": ["Octopress"], + "url": "/blog/blog/how-to-add-your-octopress-blog-website-to-google-search-console/", + "teaser": "/blog/assets/images/alfons-morales-YLSwjSy7stw-unsplash.jpg" + },{ + "title": "使用 Jekyll + minimal-mistakes 在 Github pages 上架設自己的部落格 - Jekyll x Minimal-Mistakes x GitHub pages", + "excerpt":"為什麼寫 Blog 文章 工作上受到了很多教學網站及 Blog 文章非常多的幫助,因此想架設一個自己的 Blog,將工作及生活學到的技術知識記錄下來,幫助自己複習,也希望能幫助到有需要的人。 為什麼選用 Jekyll + minimal-mistakes 選用 Jekyll 的原因 可用 Markdown 語法寫文章,Jekyll 會將 markdown 轉成 html 檔案 有非常豐富的主題 可客製化 選用 Minimal-mistakes 主題的原因 較多人使用,Github 上 star 有 9k 支援 Dark skin 可以放大圖,跟 Medium 類似 其實原本我是用 Octopress ,後來因為 Octopress 已不再維護,且主題沒有 Jekyll 來得豐富,最後決定使用 Jekyll 重新架一個 Blog Octopress 廢話不多說,現在讓我們一起用...","categories": ["Blog"], + "tags": ["Jekyll","Minimal-Mistakes","Theme"], + "url": "/blog/blog/creating_a_github_pages_with_jekyll_and_minimal_mistakes/", + "teaser": "/blog/assets/images/alfons-morales-YLSwjSy7stw-unsplash.jpg" + },{ + "title": "如何使 Jekyll website 能被 Google 搜索到 - Google x Search x Blog", + "excerpt":"在 Google 搜尋不到我的 Blog 當我們在建立完 Jekyll Blog 後,會發現竟然 Google 不到我們的 Blog 網站,別擔心這是因為我們尚未將網站提交加入 Google Search Console 中 將網站加入 Google Search Console 進入 Google Search Console 這邊使用網址前綴方式添加,加你的 Blog domain 填入並繼續 下載 googlexxxxxxxxxx.html 檔案放到 Jekyll 根目錄,commit 上傳到 GitHub 上,點擊驗證 驗證成功 👍 提交 Sitemap 「Sitemap」是一種用來提供網站資訊的檔案,您可以在其中列出網頁、影片和其他檔案的資訊,並呈現這些內容彼此間的關係。Google 等搜尋引擎都會讀取網站的 Sitemap 檔案,藉此以更有效率的方式檢索網站。 瞭解 Sitemap 打開 Gemfile 在 group...","categories": ["Blog"], + "tags": ["Jekyll","Google"], + "url": "/blog/blog/how-to-add-your-jekyll-blog-website-to-google-search-console/", + "teaser": "/blog/assets/images/alfons-morales-YLSwjSy7stw-unsplash.jpg" + },{ + "title": "3D Graphic Engine Tips - 三角形 x UV mapping x Vertices & Indices", + "excerpt":"前言 前陣子在 iOS, Android 上開發 AR 相關的功能,因為本身沒有 3D 繪圖的相關知識,後來仔細研究,終於有一些成果,這篇用來紀錄有關 3D 繪圖的小小知識,希望可以幫助到其他人. 3D 圖形世界所有物件都是由小三角形所構成 https://www.researchgate.net/figure/3D-mesh-triangles-with-different-resolution-3D-Modelling-for-programmers-Available-at_fig2_322096576 UV mapping 將 2D 圖像投影到 3D 模型表面進行 Texture 映射的 3D 建模過程 https://en.wikipedia.org/wiki/UV_mapping 以立方體為例,將各面 Texture 壓平為 2D,在映射到 UV 座標中 UV 座標,U 為橫軸,V 為縱軸,左上為(0,0),右上為(1,1) https://wiki.povray.org/content/Reference:UV_Mapping Vertices and Indices https://www.oreilly.com/library/view/real-time-3d-graphics/9781788629690/0e5b1b24-f1a7-414d-868b-37df694749ad.xhtml 順著頂點標示出頂點的 Index,根據要畫出的三角形,以逆時鐘方向依序填入 Index 另一種方式可以用安培右手定則,旋轉方向為 index 的順序,大拇指指向方向為面朝向的方位 因此在 3D 世界要畫出雙面三角形,code...","categories": ["Mobile"], + "tags": ["iOS","Android","3D","OpenGL","ARKit","ARCore","Sceneform","SceneKit"], + "url": "/blog/mobile/3d_graphic_tips/", + "teaser": "/blog/assets/images/nick-brunner-k4xDXNskVsQ-unsplash.jpg" + },{ + "title": "搞懂 P2P 技術 (1) - P2P x IPv4 x NAT", + "excerpt":"前言 之前工作上遇到需要將自家 IPCam 與 iOS/Android 手機做 P2P 串流影音,研究了許多有關 P2P 的技術,因此想寫一系列有關 P2P 技術的文章. P2P 為什麼會需要 P2P 在了解一門技術前,我們先來看看為什麼會需要這門技術,這門技術是為了解決什麼問題 Centralized vs Decentralized vs Distributed 中心化網路 (Centralized) 所有 client 都連接到同一台 Server,Server 擁有所有 client 的數據訊息 可以想成是國家央行發行貨幣,所有人的錢都來自國家央行 優點 部署簡單好維護 集中管理數據 缺點 安全及隱私風險 (中心 server 遭破解串改,全部都會受影響) 離 Server 較遠的 client 拿資料的時間會更長 server 一但故障,其他 client 都不可使用 去中心化網路 (Decentralized)...","categories": ["P2P"], + "tags": ["iOS","Android","IPv4","NAT"], + "url": "/blog/p2p/p2p-tech-1-ipv4-nat/", + "teaser": "/blog/assets/images/nasa-1lfI7wkGWZ4-unsplash.jpg" + },{ + "title": "搞懂 P2P 技術 (2) - STUN x TURN x ICE", + "excerpt":"前言 上一篇介紹完中心化、去中心化、分佈式網路以及 IPv4、NAT、NAT 類型,但我們依舊還有些問題未解決 A, B 兩端要如何知道彼此的內部網路 IP 及外部網路 IP (STUN) 上篇有提到 NAT 類型如果是對稱型時,會無法打通 P2P,當遇到此情形時該 (TURN) 有沒有一種框架整合這整個 P2P 流程 (ICE) STUN STUN(Session Traversal Utilities for NAT,NAT 對談穿越應用程式)是一種網路協定,它允許位於 NAT(或多重 NAT)後的客戶端找出自己的公網位址,查出自己位於哪種類型的 NAT 之後以及 NAT 為某一個本地埠所繫結的 Internet 端埠。這些資訊被用來在兩個同時處於 NAT 路由器之後的主機之間建立 UDP 通信。該協定由 RFC 5389 定義。 STUN Wiki TURN TURN(全名 Traversal Using Relay NAT),是一種資料傳輸協定(data-transfer...","categories": ["P2P"], + "tags": ["iOS","Android","STUN","TURN","ICE"], + "url": "/blog/p2p/p2p-tech-2-stun-turn-ice/", + "teaser": "/blog/assets/images/nasa-1lfI7wkGWZ4-unsplash.jpg" + },{ + "title": "搞懂 P2P 技術 (3) - WebRTC x AWS x KVS", + "excerpt":"WebRTC 全名 Web Real-Time Communication,是一個支援網頁瀏覽器進行即時語音對話或影片對話的 API WebRTC Wiki WebRTC 的底層就是使用 ICE 來進行 P2P 打洞 Signaling Server 信令伺服器,用來交換雙方的 SDP 及 Ice candidate 來完成 P2P 打洞 實作 Signaling Server WebRTC 沒有明確定義如何實作 Signaling Server,主要原因在於如果雙方一開始就知道對方的資訊,那其實就不需要 Signaling Server 來交換資訊 實作一個 Signaling Server 方式有很多種,可以用 HTTP 協議也可以用 WebSocket 協議,只要能順利將雙方的資訊做交換即可 SDP (Session Description Protocol) 會話描述協議(Session Description Protocol 或簡寫...","categories": ["P2P","AWS"], + "tags": ["iOS","Android","WebRTC","KVS"], + "url": "/blog/p2p/aws/p2p-tech-3-webrtc-kvs/", + "teaser": "/blog/assets/images/nasa-1lfI7wkGWZ4-unsplash.jpg" + },{ + "title": "如何抓取 Android 的網路封包", + "excerpt":"前言 最近工作上遇到需要抓封包分析才能釐清的問題 以前在開發 iOS 非常簡單,可以用 rvictl -s [iOS UUID] 在 Mac 上創建一個虛擬網卡介面,就可以開 Wireshark 抓 iOS 上的封包了 最近開發 Android 也遇到需要抓封包的情況,上網學習了一下,在這邊紀錄,希望可以幫到其他人🙂 事前準備 一台 root 過的 Android Device tcpdump tool Wireshark 補充說明: 如果沒有 root 的 Android Device,可以嘗試用 tPacketCapture 來抓包,它的原理類似開一個類似 VPN 的 app,發送的封包此 app 會抓取,但我個人實測,有時候有些封包會好像沒發出去,影響實際情況,在此不推薦! 將 tcpdump 放入 Android 裝置 adb push tcpdump /data/local/tcpdump...","categories": ["Tools"], + "tags": ["Android","Network","Packet","Wireshark"], + "url": "/blog/tools/how-to-capture-network-packet-on-android-using-tcpdump/", + "teaser": "/blog/assets/images/jordan-harrison-40XgDxBfYXM-unsplash.jpg" + },{ + "title": "如何抓取 iOS 的網路封包", + "excerpt":"前言 上一篇介紹了 如何抓取 Android 的網路封包,順便也將之前如何抓 iOS 上的網路封包紀錄一下,希望能幫助到其他人. 事前準備 iOS Device rvictl (通常安裝 XCode 時,會順便安裝附加工具) Wireshark 查看 Mac 上的網路介面 ifconfig -l lo0 gif0 stf0 anpi1 anpi0 anpi2 en4 en5 en6 en1 en2 en3 ap1 en0 awdl0 bridge0 utun0 utun1 utun2 en7 查詢 iOS 裝置網路介面 將 iOS 裝置連接到 Mac 打開 XCode ->...","categories": ["Tools"], + "tags": ["iOS","Network","Packet","Wireshark","rvictl"], + "url": "/blog/tools/how-to-capture-network-packet-on-ios/", + "teaser": "/blog/assets/images/jordan-harrison-40XgDxBfYXM-unsplash.jpg" + },{ + "title": "Design Pattern (1) - Object-Oriented Concepts (物件導向概念)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 Object-Oriented Concepts 物件導向概念 物件導向設計的四大核心概念,為設計模式的理解奠定基礎。讓我們一同簡單探索這些概念。 Encapsulation 封裝 封裝是將屬性及方法的實作細節隱藏在類別中,只暴露必要的方法給使用者,保護內部屬性和方法不被隨意修改。 就像開車時,我們只需知道踩油門會加速,踩煞車會停止,而不必了解馬達、電瓶、發動機等原理。這些細節都被封裝在引擎蓋下。 Inheritance 繼承 繼承使子類別可以繼承父類別的屬性和方法,達到程式碼重複使用的目的。 在自然界中,狗與貓都是動物,都能呼吸和行動;花草和樹木都是植物,都能進行光合作用。這就是繼承的概念。 Polymorphism 多型 多型為不同的類別提供統一的介面或抽象類別,以操作不同的實體物件。 以 iPhone 6S 為例,不論是由台積電還是三星代工的晶片,使用者拿到的 iPhone 6S 功能都一樣。這就是多型。 Abstraction 抽象 抽象通過類型或接口隱藏實作細節,只提供必要的功能給使用者。 我們在手機上安裝的 App 是對各種應用程式的抽象名稱;去市場買水果,不論是蘋果還是香蕉,水果也是一種抽象名稱。這就是抽象。 總結 理解了物件導向的核心概念後,我們將進一步探討這些概念如何引導我們進入設計原則的世界。物件導向概念為我們提供了創建模塊化、可重用和易於維護程式碼的基礎,而設計原則則教我們如何有效地應用這些概念來解決更複雜的設計問題。接下來,我們將探討這些原則,並了解它們如何幫助我們實現高質量的軟體設計。 Object-Oriented Concepts -> Design Principle -> Design Pattern 參考 Head First Design Patterns...","categories": ["Design Pattern"], + "tags": ["Object-Oriented Concepts"], + "url": "/blog/design%20pattern/design-pattern-1-object-oriented-concepts/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (2) - Design Principles (設計原則)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 Design Principle Design Principle 是用來幫助我們改善物件導向設計的建議,幫助我們設計出更好的軟體。 SOLID 物件導向程式設計基本五大原則 Single Responsibility Principle (SRP) 單一職責原則 物件應該僅具有一種單一功能,應只會有一個理由去改變此物件 e.g. 我們要做登入頁面功能,我們會這樣寫 Swift Kotlin 依照單一職責原則,我們應該要將 API 及 DB 的功能分開,修改如下 Swift Kotlin 有些文章會說 save, delete function 也須拆開在不同 class(DeleteDBService, SaveDBService)處理, 因為 save. delete 是不同職責修改項目, 不應動到另一個 class, 但我認為這樣 Over Design 反而不好維護, 拆分職責應適當不過度 Open...","categories": ["Design Pattern"], + "tags": ["Design Principle"], + "url": "/blog/design%20pattern/design-pattern-2-design-principle/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (3) - Design Patterns (設計模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 Design Pattern 是什麼? Design Pattern 是在軟體工程中,用於常見問題解決的一種標準化方法。它們是經過驗證的解決方案,可以用來解決設計中的特定問題。 設計模式的組成要素 Context Context是指設計模式應用的具體場景或背景。它描述了模式應用的環境和條件。 Forces Forces是指在設計過程中需要考慮的各種因素,包括但不限於性能需求、可擴展性、維護性等。 Problem Problem是指在特定Context和Forces下,開發者面臨的具體設計問題。 Solution Solution是指設計模式提供的解決方案,它幫助開發者解決Problem,並考慮到了Forces的影響。 設計模式的應用步驟 物件導向程式分析(OOA):從高層次理解應用的需求和結構。(此步驟需要劃出UML圖) 看清楚Context:透過UML圖理解模式應用的具體場景。 察覺Forces:識別影響設計的關鍵因素。 找到Problem:明確需要解決的設計問題。 套用模式:根據Problem及Forces選擇合適的設計模式。 得到新的Resulting Context:應用模式後,獲得改進後的設計方案。(此步驟需要劃出UML圖) 誤記導向程式設計 (OOP):透過新的Resulting Context UML圖,開始撰寫程式碼實作。 Design Patterns Categories Design Pattern 可以分為三種基本的類型 Creational 創建型 創建實例化物件有關的 Patterns Factory Method Pattern Abstract Factory Pattern...","categories": ["Design Pattern"], + "tags": ["Design Pattern"], + "url": "/blog/design%20pattern/design-pattern-3-design-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "深入解析 Google Wallet Smart Tap:未來的支付方式", + "excerpt":"前言 最近因工作之需,深入研究了 Google Wallet Smart Tap 相關技術,因此想撰寫這篇文章來記錄所學。這不僅能幫助我自己進行複習,也希望能對其他開發者提供幫助。🙂 什麼是 NFC NFC(Near Field Communication,近場通訊)是一種使兩個裝置在幾厘米距離內進行通訊的短距離無線通訊技術。它被廣泛應用於支付、票務、資料交換等領域。 Google Wallet Smart Tap 簡介 Smart Tap 是 Google 利用 NFC 技術開發的一種專有通訊協議。它允許用戶通過移動裝置在支持的終端機上進行快速且安全的交易和資料交換。 如果你們公司是實作 Terminal 端,必須獲得認證才能使用此協議,這部分我有寫信問 Google 得到如下回覆,需要提供資訊簽署協議,Google 才會提供機密文件讓你實作。 If you are a terminal provider and would like to certify your terminal for use with Google Wallet, please provide...","categories": ["Pay","Technology"], + "tags": ["Google Wallet","Smart Tap","NFC","Payment Systems"], + "url": "/blog/pay/technology/google-wallet-smart-tap-exploring/", + "teaser": "/blog/assets/images/mika-baumeister-m7HWPWVjfJ4-unsplash.jpg" + },{ + "title": "Design Pattern (4) - UML (統一建模語言)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 UML (Unified Modeling Language) UML 是一種用視覺圖形化來規劃建構軟體的方法。 不要急著寫程式,尤其是遇到較複雜的功能,先思考如何設計架構畫出 UML 圖,程式才會具有可讀性、維護性及擴展性。 Class 類別 如圖分為三列依序是 Class 名稱 Attribute 屬性 Operations 方法 Interface 介面 Interface 有兩種表示法 一般表示法 一般型式與 Class 並無太大區別,只要在 Class Name 上方標註 <<interface>> 即可 棒棒糖表示法 用球狀來表示介面 Attribute 屬性 Visibility 可視範圍 Sign Modifiers + Public # Protected...","categories": ["Design Pattern"], + "tags": ["UML"], + "url": "/blog/design%20pattern/design-pattern-4-uml/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (5) - Simple Factory Pattern (簡單工廠模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們的目標是創建一套能夠根據用戶選擇動態生成飲料對象的點餐系統。首先,讓我們通過UML來分析系統的基本結構。 物件導向分析 (OOA) Swift Kotlin 察覺 Forces 隨著飲料店越來越多新飲品,我們也需要修改 order 方法,但這樣容易影響不會變動的程式碼,於是我們需要找出 需要變動 以及 不需變動 的程式碼,把它們分隔開來 需要變動的程式碼 Swift Kotlin 不需變動的程式碼 Swift Kotlin 找出後該如何做呢,這時候需要用到 簡單工廠模式 來將其分離 套用 Solution 套用 Simple Factory Pattern 得到新的 Context (Resulting Context) 先來看一下 Simple Factory Pattern 的 UML 其實就是定義一個工廠類別來專門處理創建物件的邏輯 我們來將飲料點餐系統套用 Simple...","categories": ["Design Pattern"], + "tags": ["Simple Factory Pattern"], + "url": "/blog/design%20pattern/design-pattern-5-simple-factory-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (6) - Factory Method Pattern (工廠方法模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 引言:一個全球化的挑戰 想像一下,你的飲料點餐系統在全球範圍內大受歡迎。隨著業務的擴展,你面臨著一個挑戰:如何滿足不同地區顧客的特定偏好? 上一篇我們利用簡單工廠模式模式成功地將需要變動 以及 不需變動 的程式碼分離。今天,我們將探討如何進一步提升我們系統的靈活性和擴展性。 需求:滿足全球化的味蕾 飲料點餐系統受到客戶的喜愛,業績非常好,於是客戶在世界各地迅速擴店。但很快的問題出現了——不同地區的顧客有著不同的偏好。 案例分析: 美國喜歡錫蘭紅茶 歐洲喜歡伯爵紅茶 我們的目標是,不增加過多成本的同時,滿足這些多樣化的需求。 (成本考量我們不將所有紅茶種類都加入菜單,只用最符合當地口味的茶葉製作紅茶) 物件導向分析(OOA) 於是我們修改簡單工廠的程式碼,新增 USBeverageFactory 及 EUBeverageFactory 來製作符合美國及歐洲當地口味的飲品 Swift Kotlin 察覺 Forces 這樣做雖然可以滿足分店從不同工廠取得該地區的飲品,但每當有新的分店加入,就必須動到 BeverageShop 的程式碼來添加新的分店工廠,違反了 Open Closed Principle 套用 Solution 看清楚整個 Context,察覺 Forces 後,就可以套用 Factory Method Pattern 來解決這個問題 先來看一下 Factory Method Pattern...","categories": ["Design Pattern"], + "tags": ["Factory Method Pattern"], + "url": "/blog/design%20pattern/design-pattern-6-factory-method-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (7) - Abstract Factory Pattern (抽象工廠模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 引言:全球化的挑戰擴展 想像一下,隨著你的飲料點餐系統在全球範圍內的擴展,你面臨著如何滿足不同地區顧客特定偏好的挑戰。 需求:滿足全球化的味蕾 隨著業務的全球化擴展,不同地區的顧客有著不同的偏好。且我們也不能只賣紅茶及綠茶,需要為我們的菜單增加新的飲品,一邊新增菜單一邊擴展店舖。 物件導向分析(OOA) Swift Kotlin 如何處理多個產品在不同分店的組合,這時就需要用到 Abstract Factory Pattern 察覺 Forces 當我們每增加一種飲品到菜單中,我們必須要修改所有的 Factory 中的方法,違反了 Open Closed Principle 套用 Solution 看清楚整個 Context,察覺 Forces 後,就可以套用 Abstract Factory Pattern 來解決這個問題 先來看一下 Abstract Factory Pattern 的 UML 透過將工廠抽象,使子類別能創建一系列的實體物件。 抽象工廠有個重要的判斷方式,當你所要創建的產品是一整個系列產品,且不同需求要創建不同系列,這個關係能夠畫成二維關係,這時就非常適合使用抽象工廠來建立產品 如下圖 Country / Tea BlackTea GreenTea...","categories": ["Design Pattern"], + "tags": ["Abstract Factory Pattern"], + "url": "/blog/design%20pattern/design-pattern-7-abstract-factory-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (8) - Builder Pattern (建造者模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 今天我們要設計一個能自動做出手搖飲的機器,但如果手搖飲店只賣紅茶、綠茶,肯定滿足不了廣大的客群需求,因此我們要能夠讓手搖飲加入各種配料,來吸引顧客。 珍珠 (Pearls) 椰果 (Coconut Jelly) 紅豆 (Red Beans) 仙草凍 (Grass Jelly) 布丁 (Pudding) 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 但這麼做會有一個問題,假如我們今天只要加入紅豆以及布丁,就必須在其他用不到的參數傳入 false or null,參數越多越難以維護且可讀性也不高。 因此聰明的你可能想到了,可以利用寫多個不同的 constructor 來解決,如此就不需傳入不需要的參數。 察覺 Forces 這邊我們會發現當參數越多, 所需寫的 constructor 就越多,這樣既不好維護,也使得類別的實例化過程錯綜複雜,這個現象可以稱為 telescoping constructor Telescoping constructor 是當一個類別有多個構造器,每個構造器參數數量不同,導致類別難以維護和使用的問題。 套用 Builder Pattern ( Solution ) 得到新的...","categories": ["Design Pattern"], + "tags": ["Builder Pattern"], + "url": "/blog/design%20pattern/design-pattern-8-builder-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "How to build CHIPTool for Android", + "excerpt":"前言 最近因工作之需,研究了一下如何 Build CHIPTool 的 apk,因為按照官方文件步驟實作會有錯誤,自己一步一步解決最後終於成功,因此想寫一篇文章紀錄,幫助我自己進行複習,也希望能對其他開發者提供幫助。🙂 簡介 Matter(前稱為Project CHIP,即Connected Home over IP)是一個統一的開源連接標準,旨在增強智能家居設備之間的互操作性和兼容性。這個標準由連接標準聯盟(CSA)開發,成員包括亞馬遜、蘋果、谷歌和Zigbee聯盟等主要行業參與者。 Matter的目標是簡化製造商的開發過程,確保智能家居產品的安全性、可靠性和易用性。它利用互聯網協議(IP)來實現各種設備、移動應用和雲服務之間的通信,支持Thread和Wi-Fi網絡傳輸。 Matter的目標是創建一個無縫且互操作的智能家居生態系統,使不同製造商的設備能夠順利協同工作。 事前準備 因為在個人開發環境 Build 容易破壞環境,會需要修改 ANDROID_HOME & ANDROID_NDK_HOME 等等…,因為 CHIP 有提供 build 環境的 image,所以這邊我選擇用 docker 使用 CHIP 的 image 啟 container 來 build. Docker Pull Docker Image 這個步驟會需要一點時間,可以去喝杯咖啡 docker pull ghcr.io/project-chip/chip-build-android:latest Run container docker run -it -v...","categories": ["Tools"], + "tags": ["Android","CHIP","Matter"], + "url": "/blog/tools/how-to-build-chiptool-for-android/", + "teaser": "/blog/assets/images/matter.jpg" + },{ + "title": "Design Pattern (9) - Prototype Pattern (原型模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 前言 這次的 Pattern 讓我想到以前做過的一個 App,但當時還沒有學習到 Pattern,所以沒有用 Pattern 來處理,現在發現這個功能很適合套用 prototype pattern 這是一個用來編輯音樂燈光秀的 App,有興趣的讀者可以下載玩玩看 🙂 Asante TapTap 3 需求 今天收到了客人的需求,客人反應編輯完一條燈光還要編輯另外六條好浪費時間,能不能新增 Copy & Paste 的功能,加快編輯以節省時間,如下圖 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 當我們需要複製 LightShowData 時,只需要同樣的 jsonObject 資料重新 new 一個 LightShowData 即可複製一份 察覺 Forces 來看看上面這樣的設計會有哪些問題 如果我們的 constructor 很複雜,參數非常多,那麼重新 new 一個實體會需要知道很多細節。 如果...","categories": ["Design Pattern"], + "tags": ["Prototype Pattern"], + "url": "/blog/design%20pattern/design-pattern-9-prototype-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Getting Started with GitHub Container Registry", + "excerpt":"為什麼會寫這篇文章 隨著公司專案數量增加,每個專案的環境需求也變得更加多樣化。我們決定將原本使用Docker建置的Android Jenkins Server轉型為更靈活的架構:一個主要的Jenkins Server(Master)搭配多個Android Build Environment(Slave),後者透過Docker創建乾淨的環境。這篇文章旨在記錄此過程,不僅作為個人學習的回顧,也希望能對其他開發者提供幫助。 文章簡介 本文將引導初學者及希望深入了解如何將GitHub的新工具融入CI/CD流程的開發者,透過簡明的指南和實用的技巧,學習如何將容器映像推送至GitHub Container Registry。我將一步步展示如何設定GitHub Actions,自動化構建與部署過程,讓你的開發工作變得更加高效。 開始之前 在深入主題之前,讓我們先透過 express 框架,快速搭建一個運行於Node.js上的簡易應用。 Create a node_sample folder mkdir node_sample cd node_sample Install node package express npm init -y npm install express Create an app.js file vim app.js const express = require('express'); const app = express(); const port...","categories": ["DevOps"], + "tags": ["Docker","Container Registry","GitHub Actions","CI/CD","DevOps Tools"], + "url": "/blog/devops/getting-started-with-github-container-registry/", + "teaser": "/blog/assets/images/github_container_registry.png" + },{ + "title": "How to Enable RSA Encryption Algorithm Key in OpenSSH 8.8", + "excerpt":"前言 最近在修改公司的 jenkins CI/CD 架構,Dockerize 我們的 Android Building Environment。 在我啟動 Debian 12 container 來建構我的環境時,配置 RSA Key 到公司的 GitLab server,卻無法抓取 source code,故有了這篇文章希望未來有遇到可以拿出來快速解決問題,也希望幫助到遇到一樣問題的人。 經過分析後才發現,原來 Debian 12 預設使用 OpenSSH 8.8,而 OpenSSH 8.8 預設將 RSA 加密算法關閉,因為安全是以及過時的問題 但公司 GitLab server 較舊,目前只支援較舊的 ssh Key 演算法 RSA,因此需要找方法使其支援。 準備作業 如果你已經有環境,可以跳過此段落 首先我先用 Docker 啟一台 Debian 12 的 container 如下...","categories": ["Cryptography","OpenSSH","Security"], + "tags": ["RSA Encryption","OpenSSH 8.8","Encryption Support"], + "url": "/blog/cryptography/openssh/security/how-to-enable-rsa-encryption-algorithm-key-in-openssh-8.8/", + "teaser": "/blog/assets/images/rsa-algorithm.jpg" + },{ + "title": "Design Pattern (10) - Singleton Pattern (單例模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們收到了一個需求:開發一個應用程式,該應用程式需要與資料庫進行頻繁的交互。為了確保資料庫連接的效率和資源的合理使用,我們需要設計一個系統來管理資料庫連接。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 我們有 CRUD 四個 function 以及 constructor 用來建立 DatabaseClient 察覺 Forces 來看看上面這樣的設計會有哪些問題 資源管理:多個資料庫連接會消耗大量資源,導致性能下降。 一致性:需要確保所有資料庫操作使用相同的連接,以避免數據不一致。 效率:頻繁創建和銷毀資料庫連接會降低系統效率。 套用 Singleton Pattern ( Solution ) 得到新的 Context ( Resulting Context ) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Singleton Pattern 解決這個問題 先來看一下 Singleton Pattern 的...","categories": ["Design Pattern"], + "tags": ["Singleton Pattern"], + "url": "/blog/design%20pattern/design-pattern-10-singleton-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Jenkins (1) - 什麼是 Jenkins", + "excerpt":"什麼是 Jenkins Jenkins 是一個開源的自動化伺服器,主要用於實現持續整合(CI)和持續交付(CD)。它能夠自動化各種任務,包括建置、測試和部署軟體,從而幫助開發團隊提升效率和品質。 為什麼選擇 Jenkins 開源且免費:Jenkins 是一個開源專案,任何人都可以免費使用和修改。 豐富的插件生態系統:Jenkins 擁有超過 1,500 個插件,能夠擴展其功能以滿足不同的需求。 社群支持:Jenkins 擁有一個活躍的社群,提供豐富的資源和支援。 易於整合:Jenkins 可以與多種工具和平台整合,如 Git、Docker、Kubernetes 等。 Jenkins 的核心概念 Pipeline:Jenkins Pipeline 是一套插件,支持實現和集成持續交付流水線。Pipeline 用程式碼來定義整個建置過程,包括建置、測試和部署。 Node:Node 是 Jenkins 執行工作的機器,可以是 Jenkins 主伺服器或其他代理伺服器。 Job:Job 是 Jenkins 中的基本建置單位,定義了具體的建置、測試和部署任務。 Executor:Executor 是 Jenkins 用來執行 Job 的工作單元,每個 Node 可以有多個 Executor。 總結 Jenkins 是一個功能強大且靈活的自動化伺服器,能夠幫助開發團隊實現持續整合和持續交付。通過使用 Jenkins,團隊可以顯著提高開發效率和軟體品質。如果你還沒有使用 Jenkins,現在就是開始的好時機! 想了解更多關於 Jenkins...","categories": ["DevOps"], + "tags": ["Jenkins","CI/CD","DevOps"], + "url": "/blog/devops/jenkins-1-what-is-jenkins/", + "teaser": "/blog/assets/images/jenkins.jpg" + },{ + "title": "Jenkins (2) - 如何架設 Jenkins 伺服器", + "excerpt":"如何架設 Jenkins 伺服器 在這篇文章中,我們將介紹如何使用 Docker 映像檔來架設 Jenkins 伺服器。這種方法不僅簡單快捷,還能確保環境的一致性。 步驟一:拉取 Docker 映像檔 首先,我們需要從 GitHub Container Registry 拉取 Jenkins 的 Docker 映像檔。打開終端機並執行以下指令: docker pull jenkins/jenkins:lts-jdk17 # 單純 jenkins 環境 or docker pull ghcr.io/nickhuangcyh/docker-jenkins-and-android-env:v1.0.0-jdk17 # jenkins 環境 + Android 建構環境 步驟二:運行 Jenkins 容器 接下來,我們將運行 Jenkins 容器。請確保替換 ${volume path} 為你希望 Jenkins 資料儲存的本地路徑。執行以下指令: docker run...","categories": ["DevOps"], + "tags": ["Jenkins","CI/CD","DevOps","Docker"], + "url": "/blog/devops/jenkins-2-how-to-setup-jenkins-server/", + "teaser": "/blog/assets/images/jenkins.jpg" + },{ + "title": "Jenkins (3) - 如何配置 Credentials 以透過 SSH 從 git 上拉取程式碼", + "excerpt":"如何配置 Credentials 以透過 SSH 從 git 上拉取程式碼 在這篇文章中,我們將介紹如何在 Jenkins 中配置憑證(Credentials),以便 Jenkins 能夠透過 SSH 安全地從版本控制系統(如 GitHub 或 GitLab)拉取程式碼。 步驟一:生成 SSH 金鑰 首先,我們需要生成一對 SSH 金鑰。打開終端機並執行以下指令: ssh-keygen -t rsa -b 4096 -C \"your_email@example.com\" 按照提示完成金鑰生成過程,並記下生成的公鑰和私鑰的路徑。 步驟二:將公鑰添加到版本控制系統 將生成的公鑰內容複製並添加到你的版本控制系統中。例如,在 GitHub 中,你可以按照以下步驟操作: 登入 GitHub,進入「Settings」。 在左側菜單中選擇「SSH and GPG keys」。 點擊「New SSH key」,並將公鑰內容貼上,然後點擊「Add SSH key」。 步驟三:在 Jenkins 中添加憑證 打開...","categories": ["DevOps"], + "tags": ["Jenkins","CI/CD","DevOps","Credentials","SSH"], + "url": "/blog/devops/jenkins-3-configure-credentials-ssh/", + "teaser": "/blog/assets/images/jenkins.jpg" + },{ + "title": "Google AdSense", + "excerpt":"前言 我平時喜歡研究各種不同的技術,最近在想如何一邊分享技術幫助他人,一邊又能獲得一些收益用來再投入其他 Side Project 上,我還是想把我的內容免費提供給需要幫助的開發者,所以想採用置入一些廣告在網頁上的方式我想比較合適 什麼是 Google AdSense? Google AdSense 是一個廣告推送平台,讓網站擁有者可以在自己的網站上展示與內容相關的廣告,並透過點擊或展示量賺取收益。這是一種被動收入的好方式,適合部落客、內容創作者或網站管理員。 為什麼選擇 Google AdSense? 免費使用:註冊與使用 AdSense 完全免費。 自動化廣告投放:系統會自動根據您的網站內容匹配相關廣告。 多樣化廣告形式:可選擇文字、圖片、影片等多種形式的廣告。 透明收益報告:提供詳細的報表分析,幫助您了解廣告效益。 開始使用 Google AdSense 的步驟 1. 註冊 AdSense 帳戶 前往 Google AdSense 官方網站。 使用您的 Google 帳號登入並申請 AdSense 帳戶。 進入 AdSense 主頁面 2. 輸入資訊 點擊”輸入資訊”提供資訊 填寫你的網站 domain、國家以及接受協議 3. 選擇廣告呈現樣式 點擊”探索”廣告在網站上的呈現模樣,並選擇 4. 將...","categories": ["Google"], + "tags": ["AdSense"], + "url": "/blog/google/google-adsense/", + "teaser": "/blog/assets/images/google_adsense.jpg" + },{ + "title": "Design Pattern (11) - Adapter Pattern (轉接器模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們收到了一個需求:公司現有的 股票數據系統 使用 XML 格式 存儲與傳遞數據,而新引入的 第三方股票分析系統 僅支援 JSON 格式。為了整合兩個系統,我們需要設計一個解決方案,使得現有數據可以被第三方分析系統正確接收和處理。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,上述程式碼可以運行,但存在以下問題: 責任分散: Client 負責數據轉換,這違反了單一職責原則(SRP)。 一旦轉換邏輯變複雜,Client 的程式碼將變得難以維護。 重複性高: 如果其他系統需要同樣的轉換邏輯,程式碼將無法重複使用,導致重複性問題。 耦合性高: Client 必須了解 XmlStockData 與 JsonAnalyzer 的具體實現細節,導致高耦合性。 未來若資料來源或目標格式改變,Client 必須大幅修改。 無法適應變化: 若引入更多資料格式(如 CSV 或 YAML),每種格式都需要在 Client 中實現轉換邏輯,難以擴展。 套用 Adapter Pattern...","categories": ["Design Pattern"], + "tags": ["Adapter Pattern"], + "url": "/blog/design%20pattern/design-pattern-11-adapter-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (12) - Bridge Pattern (橋接模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們收到了一個需求:公司現有的 保全系統,在偵測到不同類型的事件(如火警、竊盜警鈴)時,需要以多種通知方式向用戶發送警報訊息。支援的通知方式包括: APNS (Apple iOS Push) FCM (Google Android Push) Email SMS 警報事件則可能包括: Fire (火警) Burglar (竊盜警鈴) 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,上述程式碼可以運行,但存在以下問題: 高耦合性 (Tight Coupling): 警報類型 和 通知方式 被緊密地耦合在一起,這使得每次新增警報類型或通知方式時,都必須在多個類別中進行修改。 系統的維護成本較高,每個新需求都可能導致代碼的重構。 難以擴展 (Difficulty in Extending): 每增加一種新的警報類型或通知方式,都需要在每個組合中創建新的類別,導致代碼增長迅速。 如果需求變更(例如新增一種新的通知方式或警報類型),則需要修改大量的程式碼。 重複代碼 (Code Duplication): 由於每一種通知方式與警報事件的組合都需要實現一個具體的類別,導致了大量重複代碼,增加了程式碼維護的困難。 靈活性差...","categories": ["Design Pattern"], + "tags": ["Bridge Pattern"], + "url": "/blog/design%20pattern/design-pattern-12-bridge-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Jenkins (3) - 如何配置 Credentials 以透過 SSH 從 git 上拉取程式碼", + "excerpt":"如何配置 Credentials 以透過 SSH 從 git 上拉取程式碼 在這篇文章中,我們將介紹如何在 Jenkins 中配置憑證(Credentials),以便 Jenkins 能夠透過 SSH 安全地從版本控制系統(如 GitHub 或 GitLab)拉取程式碼。 步驟一:生成 SSH 金鑰 首先,我們需要生成一對 SSH 金鑰。打開終端機並執行以下指令: ssh-keygen -t rsa -b 4096 -C \"your_email@example.com\" 按照提示完成金鑰生成過程,並記下生成的公鑰和私鑰的路徑。 步驟二:將公鑰添加到版本控制系統 將生成的公鑰內容複製並添加到你的版本控制系統中。例如,在 GitHub 中,你可以按照以下步驟操作: 登入 GitHub,進入「Settings」。 在左側菜單中選擇「SSH and GPG keys」。 點擊「New SSH key」,並將公鑰內容貼上,然後點擊「Add SSH key」。 步驟三:在 Jenkins 中添加憑證 打開...","categories": ["DevOps"], + "tags": ["Jenkins","CI/CD","DevOps","Credentials","SSH"], + "url": "/blog/devops/jenkins-3-configure-credentials-ssh/", + "teaser": "/blog/assets/images/jenkins.jpg" + },{ + "title": "Design Pattern (13) - Composite Pattern (組合模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們收到了一個需求:實作一個檔案系統,其目錄可以包含檔案或子目錄,並且需要提供統一的操作介面來列出目錄內容。此系統應支援以下功能: 支援樹狀結構的表示。 可操作單一檔案和目錄。 新增檔案或目錄時無需大幅修改現有程式碼。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,上述需求可能會遇到以下問題: 高耦合性 (Tight Coupling): 單一檔案和目錄集合的操作邏輯分散在多個類別中,導致系統維護困難。 重複代碼 (Code Duplication): 每次操作目錄內容時,需分別處理檔案與子目錄,導致相似邏輯多處重複。 難以擴展 (Difficulty in Extending): 新增檔案或目錄類型時,需大幅修改程式碼,影響系統穩定性。 靈活性差 (Lack of Flexibility): 操作層需清楚區分單一檔案與目錄集合,增加程式碼複雜度。 套用 Composite Pattern ( Solution ) 得到新的 Context ( Resulting Context ) 做完 OOA,察覺...","categories": ["Design Pattern"], + "tags": ["Composite Pattern"], + "url": "/blog/design%20pattern/design-pattern-13-composite-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (14) - Decorator Pattern (裝飾者模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們收到了一個需求:咖啡店的 POS 系統需要計算不同咖啡及其附加選項(如牛奶、糖漿、奶泡等)的價格。具體需求如下: 咖啡種類包括基本款的 Espresso 和 House Blend。 每種咖啡都可以加不同的附加項,例如牛奶、巧克力糖漿、奶泡。 系統應該支持動態組合不同的附加項,而不需要針對每種組合定義類別。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,上述需求可能會遇到以下問題: 類別爆炸 (Class Explosion): 為每一種咖啡及其附加選項組合創建類別,導致類別數量迅速增長。 高耦合性 (Tight Coupling): 咖啡與附加選項緊密耦合,修改某一部分時可能影響整體。 靈活性差 (Lack of Flexibility): 系統無法動態地添加或移除附加選項,只能依賴預先定義的組合。 重複代碼 (Code Duplication): 每種組合的實作邏輯重複,導致維護困難。 套用 Decorator Pattern (Solution) 得到新的 Context (Resulting Context) 做完...","categories": ["Design Pattern"], + "tags": ["Decorator Pattern"], + "url": "/blog/design%20pattern/design-pattern-14-decorator-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (15) - Facade Pattern (外觀模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 假設我們正在開發一個家庭影院系統,該系統包含以下子系統: DVD 播放器 環繞音響 燈光 投影機 用戶希望能輕鬆開啟或關閉家庭影院的所有功能,而不需要逐一操作各個設備。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在設計階段,我們注意到以下設計難題: 子系統過於複雜:需要多個步驟才能完成操作。 操作繁瑣:用戶需要熟悉每個子系統的細節。 缺乏一致性:不同子系統之間的操作方式可能不同,導致混亂。 這些 Forces 驅使我們採用外觀模式來簡化介面,減少系統的操作複雜度。 套用 Facade Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Facade Pattern 解決這個問題。 先來看一下 Facade Pattern 的 UML: Subsystems (子系統):表示系統中的一組類別或模組,它們各自負責不同的功能。例如,在家庭影院系統中,包括...","categories": ["Design Pattern"], + "tags": ["Facade Pattern"], + "url": "/blog/design%20pattern/design-pattern-15-facade-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (16) - Flyweight Pattern (享元模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 假設我們正在開發一個森林場景的渲染系統,該系統需要顯示數百棵甚至數千棵樹木。 每棵樹包含兩類資料: 內部狀態 (Intrinsic State):不隨環境改變的資料,例如樹的種類、顏色、紋理等。 外部狀態 (Extrinsic State):因環境而異的資料,例如樹的座標 (x, y)。 如果為每棵樹都建立完整的物件,將導致記憶體消耗過大。因此,我們需要一種共享內部狀態的方式來優化記 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在設計階段,我們注意到以下設計難題: 大量重複資料:每棵樹都包含相同的種類、顏色和紋理資料。 性能問題:對於數千棵樹的場景渲染,過多的物件會導致記憶體不足或性能瓶頸。 共享與獨立的平衡:如何在共享資料的同時,保留每棵樹的獨立外部狀態。 為解決這些問題,我們採用了享元模式。 套用 Flyweight Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Flyweight Pattern 解決這個問題。 先來看一下 flyweight Pattern 的 UML:...","categories": ["Design Pattern"], + "tags": ["Flyweight Pattern"], + "url": "/blog/design%20pattern/design-pattern-16-flyweight-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (17) - Proxy Pattern (代理模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們的任務是建立一個影片播放系統,需求如下: 應用能播放多個影片,但避免每次都重複下載相同的影片。 影片需要在用戶第一次訪問時下載,之後從快取中取得以節省資源。 提供一個透明的介面,無需讓客戶端知道影片是透過代理取得的。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,我們可能面臨以下挑戰: 高頻寬消耗 (High Bandwidth Usage): 如果每次播放影片都重新下載,將導致不必要的頻寬浪費。 延遲時間 (High Latency): 每次下載影片會增加播放前的等待時間,影響用戶體驗。 客戶端耦合 (Client Coupling): 如果客戶端需要處理影片的下載邏輯,會增加不必要的複雜性。 套用 Proxy Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Proxy Pattern 解決這個問題。 Proxy Pattern 提供了解決方案,通過引入...","categories": ["Design Pattern"], + "tags": ["Proxy Pattern"], + "url": "/blog/design%20pattern/design-pattern-17-proxy-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (18) - Chain of Responsibility Pattern (責任鏈模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們的任務是建立一個日誌處理系統,需求如下: 系統支持多層次日誌處理(如 Console、File、Database 等)。 請求可以被多個處理器處理,且處理器的組合應具備動態調整能力。 確保每層處理器的責任彼此獨立,並能擴展新處理器而不影響既有邏輯。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,我們可能面臨以下挑戰: 高耦合性 (High Coupling): 如果客戶端需要直接控制每個日誌處理器,將導致代碼過於複雜且難以維護。 缺乏靈活性 (Lack of Flexibility): 無法輕鬆地調整處理器的執行順序或新增處理器。 違反開放關閉原則 (Violates OCP): 若需支持新的日誌處理方式,必須修改客戶端邏輯,導致系統穩定性下降。 套用 Chain of Responsibility Pattern (Solution) 得到新的 Context (Resulting Context) 先來看一下 Chain of Responsibility Pattern 的 UML:...","categories": ["Design Pattern"], + "tags": ["Chain of Responsibility Pattern"], + "url": "/blog/design%20pattern/design-pattern-18-chain-of-responsibility-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (19) - Command Pattern (命令模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們需要一個音樂播放器控制系統,需求如下: 使用者可以透過遙控器控制音樂播放器執行「播放」、「暫停」和「停止」操作。 支援撤銷 (Undo) 功能,例如撤銷暫停會恢復播放。 按鈕行為應保持靈活,方便未來擴充新功能,例如「下一首」或「重播」。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,我們可能面臨以下挑戰: 高耦合性 (High Coupling): 客戶端需要直接操作每個具體設備的功能,導致耦合度過高,不利於系統擴展。 缺乏靈活性 (Lack of Flexibility): 如果需要新增設備或操作,客戶端需要修改大量程式碼,增加了維護成本。 撤銷/重做困難 (Undo/Redo Complexity): 系統沒有統一的方式處理操作歷史,導致撤銷和重做功能難以實現。 套用 Command Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Command Pattern 解決這個問題。 先來看一下...","categories": ["Design Pattern"], + "tags": ["Command Pattern"], + "url": "/blog/design%20pattern/design-pattern-19-command-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (20) - Iterator Pattern (迭代器模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們的任務是設計一個檔案系統搜尋工具,需求如下: 使用者可以選擇不同的檔案搜尋方式,例如 廣度優先搜尋 (BFS) 或 深度優先搜尋 (DFS)。 客戶端不需要關心搜尋邏輯的實現細節,只需使用統一的迭代器介面來遍歷搜尋結果。 系統需要具備擴展性,方便新增其他搜尋法,例如基於檔案大小排序的搜尋。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,我們可能面臨以下挑戰: 高耦合性 (High Coupling): 客戶端需要直接操作每種搜尋方式的實現細節,導致代碼臃腫且難以維護。 缺乏一致性 (Lack of Consistency): 不同搜尋方式的結果訪問方式可能不一致。 違反開放關閉原則 (Violates OCP): 若新增搜尋法或更改現有搜尋邏輯,需要修改客戶端程式碼。 套用 Iterator Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Iterator...","categories": ["Design Pattern"], + "tags": ["Iterator Pattern"], + "url": "/blog/design%20pattern/design-pattern-20-iterator-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (21) - Mediator Pattern (中介者模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們的任務是設計一個 聊天室應用程式,需求如下: 使用者可以透過聊天室傳遞訊息。 每個使用者都不需要直接管理其他使用者的資訊。 新增或移除使用者不應影響其他使用者的運作。 物件導向分析 (OOA) 理解需求後,我們來快速分析: 若使用者彼此直接通信,會導致複雜的相依關係,增加維護成本。 我們需要一個集中管理的角色,來協調使用者之間的訊息傳遞。 察覺 Forces 在未使用設計模式的情況下,我們可能面臨以下挑戰: 高耦合性 (High Coupling): 使用者彼此之間直接通信,導致新增或移除使用者時需修改多處程式碼。 難以擴展 (Hard to Extend): 若要增加新功能(如訊息過濾或廣播機制),需要修改多個使用者的邏輯。 複雜度上升 (Increased Complexity): 使用者之間的關聯數量隨著使用者數量增長呈指數級增加。 套用 Mediator Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Mediator Pattern 解決這個問題。...","categories": ["Design Pattern"], + "tags": ["Mediator Pattern"], + "url": "/blog/design%20pattern/design-pattern-21-mediator-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (22) - Memento Pattern (備忘錄模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們的任務是設計一個文字編輯器,需求如下: 使用者可以輸入文字,並隨時按下 Ctrl+Z 回復上一步。 系統需要保存歷史狀態以供回復。 客戶端不需要了解狀態保存的實現細節,只需使用一個簡單的回復操作即可。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,我們可能面臨以下挑戰: 資料喪失風險 (Data Loss Risk): 如果我們僅保留當前狀態,將無法回復到之前的狀態。 高耦合性 (High Coupling): 客戶端需要直接操作狀態管理邏輯,導致複雜性增加。 難以擴展 (Hard to Extend): 新增功能或改變狀態保存方式時,可能需要修改大量程式碼。 套用 Memento Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Memento Pattern 解決這個問題...","categories": ["Design Pattern"], + "tags": ["Memento Pattern"], + "url": "/blog/design%20pattern/design-pattern-22-memento-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (23) - Observer Pattern (觀察者模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們的任務是設計一個 安全系統主機 (Panel),需求如下: 主機負責監控不同的感測器,例如煙霧探測器或門窗感測器。 當警報觸發時,主機需要通知所有已註冊的設備,例如平板、iOS 和 Android 手機。 設備可以動態地加入或移除通知清單。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,我們可能面臨以下挑戰: 高耦合性 (High Coupling) 如果主機直接與每一個設備互動,程式碼會變得難以維護,每次新增或移除設備都需要修改主機邏輯。 缺乏彈性 (Lack of Flexibility) 新增設備需要修改現有程式碼,違反開放關閉原則 (OCP)。 通知不一致 (Inconsistent Notifications) 當警報觸發時,難以確保每個設備都能正確接收到通知。 套用 Observer Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用...","categories": ["Design Pattern"], + "tags": ["Observer Pattern"], + "url": "/blog/design%20pattern/design-pattern-23-observer-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (24) - State Pattern (狀態模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 我們的任務是設計一個 飲水機,需求如下: 飲水機有三種狀態: 加熱中:提升水溫至熱水。 冷卻中:降低水溫至冷水。 待機中:維持現有水溫。 使用者可透過按鈕切換飲水機的狀態。 飲水機需要根據當前狀態執行正確的行為,例如加熱狀態時加熱水,但不可冷卻。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 在未使用設計模式的情況下,我們可能面臨以下挑戰: 高耦合性 (High Coupling) 狀態邏輯與飲水機核心功能混合在一起,導致程式碼難以維護。 違反單一職責原則 (SRP) 飲水機類別需要同時處理狀態邏輯與主要功能,責任過於繁重。 難以擴展 (Hard to Extend) 新增或修改狀態行為需更改飲水機核心邏輯,違反開放關閉原則 (OCP)。 套用 State Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 State Pattern...","categories": ["Design Pattern"], + "tags": ["State Pattern"], + "url": "/blog/design%20pattern/design-pattern-24-state-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + },{ + "title": "Design Pattern (25) - Strategy Pattern (策略模式)", + "excerpt":"您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。 需求 在設計一個 電商運費計算系統 時,我們需要滿足以下需求: 支援多種運費計算方式,例如: 一般配送:固定運費。 快速配送:依重量計費。 國際配送:根據地區與重量計費。 系統需具備良好的擴展性: 能夠方便地新增新的運費計算方式。 避免使用大量的 if-else 或 switch-case。 使用者應能輕鬆切換運費計算方式。 物件導向分析 (OOA) 理解需求後,讓我們來快速實作物件導向分析吧! 察覺 Forces 如果未套用設計模式,我們可能會遇到以下問題: 難以維護 運費計算邏輯混雜在主程式內,新增或修改一種計算方式可能會影響其他部分。 違反開放關閉原則 (OCP) 每次新增運費計算方式都需修改核心業務邏輯。 違反單一職責原則 (SRP) 主程式同時負責運費計算與核心業務邏輯,責任過於繁重。 套用 Strategy Pattern (Solution) 得到新的 Context (Resulting Context) 做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Strategy...","categories": ["Design Pattern"], + "tags": ["Strategy Pattern"], + "url": "/blog/design%20pattern/design-pattern-25-strategy-pattern/", + "teaser": "/blog/assets/images/design_patterns.jpg" + }] diff --git a/assets/js/lunr/lunr.js b/assets/js/lunr/lunr.js new file mode 100644 index 0000000..6aa370f --- /dev/null +++ b/assets/js/lunr/lunr.js @@ -0,0 +1,3475 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ + +;(function(){ + +/** + * A convenience function for configuring and constructing + * a new lunr Index. + * + * A lunr.Builder instance is created and the pipeline setup + * with a trimmer, stop word filter and stemmer. + * + * This builder object is yielded to the configuration function + * that is passed as a parameter, allowing the list of fields + * and other builder parameters to be customised. + * + * All documents _must_ be added within the passed config function. + * + * @example + * var idx = lunr(function () { + * this.field('title') + * this.field('body') + * this.ref('id') + * + * documents.forEach(function (doc) { + * this.add(doc) + * }, this) + * }) + * + * @see {@link lunr.Builder} + * @see {@link lunr.Pipeline} + * @see {@link lunr.trimmer} + * @see {@link lunr.stopWordFilter} + * @see {@link lunr.stemmer} + * @namespace {function} lunr + */ +var lunr = function (config) { + var builder = new lunr.Builder + + builder.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + builder.searchPipeline.add( + lunr.stemmer + ) + + config.call(builder, builder) + return builder.build() +} + +lunr.version = "2.3.9" +/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A namespace containing utils for the rest of the lunr library + * @namespace lunr.utils + */ +lunr.utils = {} + +/** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf lunr.utils + * @function + */ +lunr.utils.warn = (function (global) { + /* eslint-disable no-console */ + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + /* eslint-enable no-console */ +})(this) + +/** + * Convert an object to a string. + * + * In the case of `null` and `undefined` the function returns + * the empty string, in all other cases the result of calling + * `toString` on the passed object is returned. + * + * @param {Any} obj The object to convert to a string. + * @return {String} string representation of the passed object. + * @memberOf lunr.utils + */ +lunr.utils.asString = function (obj) { + if (obj === void 0 || obj === null) { + return "" + } else { + return obj.toString() + } +} + +/** + * Clones an object. + * + * Will create a copy of an existing object such that any mutations + * on the copy cannot affect the original. + * + * Only shallow objects are supported, passing a nested object to this + * function will cause a TypeError. + * + * Objects with primitives, and arrays of primitives are supported. + * + * @param {Object} obj The object to clone. + * @return {Object} a clone of the passed object. + * @throws {TypeError} when a nested object is passed. + * @memberOf Utils + */ +lunr.utils.clone = function (obj) { + if (obj === null || obj === undefined) { + return obj + } + + var clone = Object.create(null), + keys = Object.keys(obj) + + for (var i = 0; i < keys.length; i++) { + var key = keys[i], + val = obj[key] + + if (Array.isArray(val)) { + clone[key] = val.slice() + continue + } + + if (typeof val === 'string' || + typeof val === 'number' || + typeof val === 'boolean') { + clone[key] = val + continue + } + + throw new TypeError("clone is not deep and does not support nested objects") + } + + return clone +} +lunr.FieldRef = function (docRef, fieldName, stringValue) { + this.docRef = docRef + this.fieldName = fieldName + this._stringValue = stringValue +} + +lunr.FieldRef.joiner = "/" + +lunr.FieldRef.fromString = function (s) { + var n = s.indexOf(lunr.FieldRef.joiner) + + if (n === -1) { + throw "malformed field ref string" + } + + var fieldRef = s.slice(0, n), + docRef = s.slice(n + 1) + + return new lunr.FieldRef (docRef, fieldRef, s) +} + +lunr.FieldRef.prototype.toString = function () { + if (this._stringValue == undefined) { + this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef + } + + return this._stringValue +} +/*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A lunr set. + * + * @constructor + */ +lunr.Set = function (elements) { + this.elements = Object.create(null) + + if (elements) { + this.length = elements.length + + for (var i = 0; i < this.length; i++) { + this.elements[elements[i]] = true + } + } else { + this.length = 0 + } +} + +/** + * A complete set that contains all elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.complete = { + intersect: function (other) { + return other + }, + + union: function () { + return this + }, + + contains: function () { + return true + } +} + +/** + * An empty set that contains no elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.empty = { + intersect: function () { + return this + }, + + union: function (other) { + return other + }, + + contains: function () { + return false + } +} + +/** + * Returns true if this set contains the specified object. + * + * @param {object} object - Object whose presence in this set is to be tested. + * @returns {boolean} - True if this set contains the specified object. + */ +lunr.Set.prototype.contains = function (object) { + return !!this.elements[object] +} + +/** + * Returns a new set containing only the elements that are present in both + * this set and the specified set. + * + * @param {lunr.Set} other - set to intersect with this set. + * @returns {lunr.Set} a new set that is the intersection of this and the specified set. + */ + +lunr.Set.prototype.intersect = function (other) { + var a, b, elements, intersection = [] + + if (other === lunr.Set.complete) { + return this + } + + if (other === lunr.Set.empty) { + return other + } + + if (this.length < other.length) { + a = this + b = other + } else { + a = other + b = this + } + + elements = Object.keys(a.elements) + + for (var i = 0; i < elements.length; i++) { + var element = elements[i] + if (element in b.elements) { + intersection.push(element) + } + } + + return new lunr.Set (intersection) +} + +/** + * Returns a new set combining the elements of this and the specified set. + * + * @param {lunr.Set} other - set to union with this set. + * @return {lunr.Set} a new set that is the union of this and the specified set. + */ + +lunr.Set.prototype.union = function (other) { + if (other === lunr.Set.complete) { + return lunr.Set.complete + } + + if (other === lunr.Set.empty) { + return this + } + + return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements))) +} +/** + * A function to calculate the inverse document frequency for + * a posting. This is shared between the builder and the index + * + * @private + * @param {object} posting - The posting for a given term + * @param {number} documentCount - The total number of documents. + */ +lunr.idf = function (posting, documentCount) { + var documentsWithTerm = 0 + + for (var fieldName in posting) { + if (fieldName == '_index') continue // Ignore the term index, its not a field + documentsWithTerm += Object.keys(posting[fieldName]).length + } + + var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5) + + return Math.log(1 + Math.abs(x)) +} + +/** + * A token wraps a string representation of a token + * as it is passed through the text processing pipeline. + * + * @constructor + * @param {string} [str=''] - The string token being wrapped. + * @param {object} [metadata={}] - Metadata associated with this token. + */ +lunr.Token = function (str, metadata) { + this.str = str || "" + this.metadata = metadata || {} +} + +/** + * Returns the token string that is being wrapped by this object. + * + * @returns {string} + */ +lunr.Token.prototype.toString = function () { + return this.str +} + +/** + * A token update function is used when updating or optionally + * when cloning a token. + * + * @callback lunr.Token~updateFunction + * @param {string} str - The string representation of the token. + * @param {Object} metadata - All metadata associated with this token. + */ + +/** + * Applies the given function to the wrapped string token. + * + * @example + * token.update(function (str, metadata) { + * return str.toUpperCase() + * }) + * + * @param {lunr.Token~updateFunction} fn - A function to apply to the token string. + * @returns {lunr.Token} + */ +lunr.Token.prototype.update = function (fn) { + this.str = fn(this.str, this.metadata) + return this +} + +/** + * Creates a clone of this token. Optionally a function can be + * applied to the cloned token. + * + * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token. + * @returns {lunr.Token} + */ +lunr.Token.prototype.clone = function (fn) { + fn = fn || function (s) { return s } + return new lunr.Token (fn(this.str, this.metadata), this.metadata) +} +/*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A function for splitting a string into tokens ready to be inserted into + * the search index. Uses `lunr.tokenizer.separator` to split strings, change + * the value of this property to change how strings are split into tokens. + * + * This tokenizer will convert its parameter to a string by calling `toString` and + * then will split this string on the character in `lunr.tokenizer.separator`. + * Arrays will have their elements converted to strings and wrapped in a lunr.Token. + * + * Optional metadata can be passed to the tokenizer, this metadata will be cloned and + * added as metadata to every token that is created from the object to be tokenized. + * + * @static + * @param {?(string|object|object[])} obj - The object to convert into tokens + * @param {?object} metadata - Optional metadata to associate with every token + * @returns {lunr.Token[]} + * @see {@link lunr.Pipeline} + */ +lunr.tokenizer = function (obj, metadata) { + if (obj == null || obj == undefined) { + return [] + } + + if (Array.isArray(obj)) { + return obj.map(function (t) { + return new lunr.Token( + lunr.utils.asString(t).toLowerCase(), + lunr.utils.clone(metadata) + ) + }) + } + + var str = obj.toString().toLowerCase(), + len = str.length, + tokens = [] + + for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) { + var char = str.charAt(sliceEnd), + sliceLength = sliceEnd - sliceStart + + if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) { + + if (sliceLength > 0) { + var tokenMetadata = lunr.utils.clone(metadata) || {} + tokenMetadata["position"] = [sliceStart, sliceLength] + tokenMetadata["index"] = tokens.length + + tokens.push( + new lunr.Token ( + str.slice(sliceStart, sliceEnd), + tokenMetadata + ) + ) + } + + sliceStart = sliceEnd + 1 + } + + } + + return tokens +} + +/** + * The separator used to split a string into tokens. Override this property to change the behaviour of + * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. + * + * @static + * @see lunr.tokenizer + */ +lunr.tokenizer.separator = /[\s\-]+/ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { + this._stack = [] +} + +lunr.Pipeline.registeredFunctions = Object.create(null) + +/** + * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token + * string as well as all known metadata. A pipeline function can mutate the token string + * or mutate (or add) metadata for a given token. + * + * A pipeline function can indicate that the passed token should be discarded by returning + * null, undefined or an empty string. This token will not be passed to any downstream pipeline + * functions and will not be added to the index. + * + * Multiple tokens can be returned by returning an array of tokens. Each token will be passed + * to any downstream pipeline functions and all will returned tokens will be added to the index. + * + * Any number of pipeline functions may be chained together using a lunr.Pipeline. + * + * @interface lunr.PipelineFunction + * @param {lunr.Token} token - A token from the document being processed. + * @param {number} i - The index of this token in the complete list of tokens for this document/field. + * @param {lunr.Token[]} tokens - All tokens for this document/field. + * @returns {(?lunr.Token|lunr.Token[])} + */ + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @param {String} label - The label to register this function with + */ +lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @private + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised - The serialised pipeline to load. + * @returns {lunr.Pipeline} + */ +lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error('Cannot load unregistered function: ' + fnName) + } + }) + + return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline. + */ +lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + pos = pos + 1 + this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {lunr.PipelineFunction} fn The function to remove from the pipeline. + */ +lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + if (pos == -1) { + return + } + + this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + */ +lunr.Pipeline.prototype.run = function (tokens) { + var stackLength = this._stack.length + + for (var i = 0; i < stackLength; i++) { + var fn = this._stack[i] + var memo = [] + + for (var j = 0; j < tokens.length; j++) { + var result = fn(tokens[j], j, tokens) + + if (result === null || result === void 0 || result === '') continue + + if (Array.isArray(result)) { + for (var k = 0; k < result.length; k++) { + memo.push(result[k]) + } + } else { + memo.push(result) + } + } + + tokens = memo + } + + return tokens +} + +/** + * Convenience method for passing a string through a pipeline and getting + * strings out. This method takes care of wrapping the passed string in a + * token and mapping the resulting tokens back to strings. + * + * @param {string} str - The string to pass through the pipeline. + * @param {?object} metadata - Optional metadata to associate with the token + * passed to the pipeline. + * @returns {string[]} + */ +lunr.Pipeline.prototype.runString = function (str, metadata) { + var token = new lunr.Token (str, metadata) + + return this.run([token]).map(function (t) { + return t.toString() + }) +} + +/** + * Resets the pipeline by removing any existing processors. + * + */ +lunr.Pipeline.prototype.reset = function () { + this._stack = [] +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + */ +lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) +} +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A vector is used to construct the vector space of documents and queries. These + * vectors support operations to determine the similarity between two documents or + * a document and a query. + * + * Normally no parameters are required for initializing a vector, but in the case of + * loading a previously dumped vector the raw elements can be provided to the constructor. + * + * For performance reasons vectors are implemented with a flat array, where an elements + * index is immediately followed by its value. E.g. [index, value, index, value]. This + * allows the underlying array to be as sparse as possible and still offer decent + * performance when being used for vector calculations. + * + * @constructor + * @param {Number[]} [elements] - The flat list of element index and element value pairs. + */ +lunr.Vector = function (elements) { + this._magnitude = 0 + this.elements = elements || [] +} + + +/** + * Calculates the position within the vector to insert a given index. + * + * This is used internally by insert and upsert. If there are duplicate indexes then + * the position is returned as if the value for that index were to be updated, but it + * is the callers responsibility to check whether there is a duplicate at that index + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @returns {Number} + */ +lunr.Vector.prototype.positionForIndex = function (index) { + // For an empty vector the tuple can be inserted at the beginning + if (this.elements.length == 0) { + return 0 + } + + var start = 0, + end = this.elements.length / 2, + sliceLength = end - start, + pivotPoint = Math.floor(sliceLength / 2), + pivotIndex = this.elements[pivotPoint * 2] + + while (sliceLength > 1) { + if (pivotIndex < index) { + start = pivotPoint + } + + if (pivotIndex > index) { + end = pivotPoint + } + + if (pivotIndex == index) { + break + } + + sliceLength = end - start + pivotPoint = start + Math.floor(sliceLength / 2) + pivotIndex = this.elements[pivotPoint * 2] + } + + if (pivotIndex == index) { + return pivotPoint * 2 + } + + if (pivotIndex > index) { + return pivotPoint * 2 + } + + if (pivotIndex < index) { + return (pivotPoint + 1) * 2 + } +} + +/** + * Inserts an element at an index within the vector. + * + * Does not allow duplicates, will throw an error if there is already an entry + * for this index. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + */ +lunr.Vector.prototype.insert = function (insertIdx, val) { + this.upsert(insertIdx, val, function () { + throw "duplicate index" + }) +} + +/** + * Inserts or updates an existing index within the vector. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + * @param {function} fn - A function that is called for updates, the existing value and the + * requested value are passed as arguments + */ +lunr.Vector.prototype.upsert = function (insertIdx, val, fn) { + this._magnitude = 0 + var position = this.positionForIndex(insertIdx) + + if (this.elements[position] == insertIdx) { + this.elements[position + 1] = fn(this.elements[position + 1], val) + } else { + this.elements.splice(position, 0, insertIdx, val) + } +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + */ +lunr.Vector.prototype.magnitude = function () { + if (this._magnitude) return this._magnitude + + var sumOfSquares = 0, + elementsLength = this.elements.length + + for (var i = 1; i < elementsLength; i += 2) { + var val = this.elements[i] + sumOfSquares += val * val + } + + return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector - The vector to compute the dot product with. + * @returns {Number} + */ +lunr.Vector.prototype.dot = function (otherVector) { + var dotProduct = 0, + a = this.elements, b = otherVector.elements, + aLen = a.length, bLen = b.length, + aVal = 0, bVal = 0, + i = 0, j = 0 + + while (i < aLen && j < bLen) { + aVal = a[i], bVal = b[j] + if (aVal < bVal) { + i += 2 + } else if (aVal > bVal) { + j += 2 + } else if (aVal == bVal) { + dotProduct += a[i + 1] * b[j + 1] + i += 2 + j += 2 + } + } + + return dotProduct +} + +/** + * Calculates the similarity between this vector and another vector. + * + * @param {lunr.Vector} otherVector - The other vector to calculate the + * similarity with. + * @returns {Number} + */ +lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / this.magnitude() || 0 +} + +/** + * Converts the vector to an array of the elements within the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toArray = function () { + var output = new Array (this.elements.length / 2) + + for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) { + output[j] = this.elements[i] + } + + return output +} + +/** + * A JSON serializable representation of the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toJSON = function () { + return this.elements +} +/* eslint-disable */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartarus.org/~martin + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token - The string to stem + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + * @function + */ +lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return function (token) { + return token.update(porterStemmer); + } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.generateStopWordFilter builds a stopWordFilter function from the provided + * list of stop words. + * + * The built in lunr.stopWordFilter is built using this generator and can be used + * to generate custom stopWordFilters for applications or non English languages. + * + * @function + * @param {Array} token The token to pass through the filter + * @returns {lunr.PipelineFunction} + * @see lunr.Pipeline + * @see lunr.stopWordFilter + */ +lunr.generateStopWordFilter = function (stopWords) { + var words = stopWords.reduce(function (memo, stopWord) { + memo[stopWord] = stopWord + return memo + }, {}) + + return function (token) { + if (token && words[token.toString()] !== token.toString()) return token + } +} + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @function + * @implements {lunr.PipelineFunction} + * @params {lunr.Token} token - A token to check for being a stop word. + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + */ +lunr.stopWordFilter = lunr.generateStopWordFilter([ + 'a', + 'able', + 'about', + 'across', + 'after', + 'all', + 'almost', + 'also', + 'am', + 'among', + 'an', + 'and', + 'any', + 'are', + 'as', + 'at', + 'be', + 'because', + 'been', + 'but', + 'by', + 'can', + 'cannot', + 'could', + 'dear', + 'did', + 'do', + 'does', + 'either', + 'else', + 'ever', + 'every', + 'for', + 'from', + 'get', + 'got', + 'had', + 'has', + 'have', + 'he', + 'her', + 'hers', + 'him', + 'his', + 'how', + 'however', + 'i', + 'if', + 'in', + 'into', + 'is', + 'it', + 'its', + 'just', + 'least', + 'let', + 'like', + 'likely', + 'may', + 'me', + 'might', + 'most', + 'must', + 'my', + 'neither', + 'no', + 'nor', + 'not', + 'of', + 'off', + 'often', + 'on', + 'only', + 'or', + 'other', + 'our', + 'own', + 'rather', + 'said', + 'say', + 'says', + 'she', + 'should', + 'since', + 'so', + 'some', + 'than', + 'that', + 'the', + 'their', + 'them', + 'then', + 'there', + 'these', + 'they', + 'this', + 'tis', + 'to', + 'too', + 'twas', + 'us', + 'wants', + 'was', + 'we', + 'were', + 'what', + 'when', + 'where', + 'which', + 'while', + 'who', + 'whom', + 'why', + 'will', + 'with', + 'would', + 'yet', + 'you', + 'your' +]) + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the beginning and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token The token to pass through the filter + * @returns {lunr.Token} + * @see lunr.Pipeline + */ +lunr.trimmer = function (token) { + return token.update(function (s) { + return s.replace(/^\W+/, '').replace(/\W+$/, '') + }) +} + +lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A token set is used to store the unique list of all tokens + * within an index. Token sets are also used to represent an + * incoming query to the index, this query token set and index + * token set are then intersected to find which tokens to look + * up in the inverted index. + * + * A token set can hold multiple tokens, as in the case of the + * index token set, or it can hold a single token as in the + * case of a simple query token set. + * + * Additionally token sets are used to perform wildcard matching. + * Leading, contained and trailing wildcards are supported, and + * from this edit distance matching can also be provided. + * + * Token sets are implemented as a minimal finite state automata, + * where both common prefixes and suffixes are shared between tokens. + * This helps to reduce the space used for storing the token set. + * + * @constructor + */ +lunr.TokenSet = function () { + this.final = false + this.edges = {} + this.id = lunr.TokenSet._nextId + lunr.TokenSet._nextId += 1 +} + +/** + * Keeps track of the next, auto increment, identifier to assign + * to a new tokenSet. + * + * TokenSets require a unique identifier to be correctly minimised. + * + * @private + */ +lunr.TokenSet._nextId = 1 + +/** + * Creates a TokenSet instance from the given sorted array of words. + * + * @param {String[]} arr - A sorted array of strings to create the set from. + * @returns {lunr.TokenSet} + * @throws Will throw an error if the input array is not sorted. + */ +lunr.TokenSet.fromArray = function (arr) { + var builder = new lunr.TokenSet.Builder + + for (var i = 0, len = arr.length; i < len; i++) { + builder.insert(arr[i]) + } + + builder.finish() + return builder.root +} + +/** + * Creates a token set from a query clause. + * + * @private + * @param {Object} clause - A single clause from lunr.Query. + * @param {string} clause.term - The query clause term. + * @param {number} [clause.editDistance] - The optional edit distance for the term. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromClause = function (clause) { + if ('editDistance' in clause) { + return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance) + } else { + return lunr.TokenSet.fromString(clause.term) + } +} + +/** + * Creates a token set representing a single string with a specified + * edit distance. + * + * Insertions, deletions, substitutions and transpositions are each + * treated as an edit distance of 1. + * + * Increasing the allowed edit distance will have a dramatic impact + * on the performance of both creating and intersecting these TokenSets. + * It is advised to keep the edit distance less than 3. + * + * @param {string} str - The string to create the token set from. + * @param {number} editDistance - The allowed edit distance to match. + * @returns {lunr.Vector} + */ +lunr.TokenSet.fromFuzzyString = function (str, editDistance) { + var root = new lunr.TokenSet + + var stack = [{ + node: root, + editsRemaining: editDistance, + str: str + }] + + while (stack.length) { + var frame = stack.pop() + + // no edit + if (frame.str.length > 0) { + var char = frame.str.charAt(0), + noEditNode + + if (char in frame.node.edges) { + noEditNode = frame.node.edges[char] + } else { + noEditNode = new lunr.TokenSet + frame.node.edges[char] = noEditNode + } + + if (frame.str.length == 1) { + noEditNode.final = true + } + + stack.push({ + node: noEditNode, + editsRemaining: frame.editsRemaining, + str: frame.str.slice(1) + }) + } + + if (frame.editsRemaining == 0) { + continue + } + + // insertion + if ("*" in frame.node.edges) { + var insertionNode = frame.node.edges["*"] + } else { + var insertionNode = new lunr.TokenSet + frame.node.edges["*"] = insertionNode + } + + if (frame.str.length == 0) { + insertionNode.final = true + } + + stack.push({ + node: insertionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str + }) + + // deletion + // can only do a deletion if we have enough edits remaining + // and if there are characters left to delete in the string + if (frame.str.length > 1) { + stack.push({ + node: frame.node, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // deletion + // just removing the last character from the str + if (frame.str.length == 1) { + frame.node.final = true + } + + // substitution + // can only do a substitution if we have enough edits remaining + // and if there are characters left to substitute + if (frame.str.length >= 1) { + if ("*" in frame.node.edges) { + var substitutionNode = frame.node.edges["*"] + } else { + var substitutionNode = new lunr.TokenSet + frame.node.edges["*"] = substitutionNode + } + + if (frame.str.length == 1) { + substitutionNode.final = true + } + + stack.push({ + node: substitutionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // transposition + // can only do a transposition if there are edits remaining + // and there are enough characters to transpose + if (frame.str.length > 1) { + var charA = frame.str.charAt(0), + charB = frame.str.charAt(1), + transposeNode + + if (charB in frame.node.edges) { + transposeNode = frame.node.edges[charB] + } else { + transposeNode = new lunr.TokenSet + frame.node.edges[charB] = transposeNode + } + + if (frame.str.length == 1) { + transposeNode.final = true + } + + stack.push({ + node: transposeNode, + editsRemaining: frame.editsRemaining - 1, + str: charA + frame.str.slice(2) + }) + } + } + + return root +} + +/** + * Creates a TokenSet from a string. + * + * The string may contain one or more wildcard characters (*) + * that will allow wildcard matching when intersecting with + * another TokenSet. + * + * @param {string} str - The string to create a TokenSet from. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromString = function (str) { + var node = new lunr.TokenSet, + root = node + + /* + * Iterates through all characters within the passed string + * appending a node for each character. + * + * When a wildcard character is found then a self + * referencing edge is introduced to continually match + * any number of any characters. + */ + for (var i = 0, len = str.length; i < len; i++) { + var char = str[i], + final = (i == len - 1) + + if (char == "*") { + node.edges[char] = node + node.final = final + + } else { + var next = new lunr.TokenSet + next.final = final + + node.edges[char] = next + node = next + } + } + + return root +} + +/** + * Converts this TokenSet into an array of strings + * contained within the TokenSet. + * + * This is not intended to be used on a TokenSet that + * contains wildcards, in these cases the results are + * undefined and are likely to cause an infinite loop. + * + * @returns {string[]} + */ +lunr.TokenSet.prototype.toArray = function () { + var words = [] + + var stack = [{ + prefix: "", + node: this + }] + + while (stack.length) { + var frame = stack.pop(), + edges = Object.keys(frame.node.edges), + len = edges.length + + if (frame.node.final) { + /* In Safari, at this point the prefix is sometimes corrupted, see: + * https://github.com/olivernn/lunr.js/issues/279 Calling any + * String.prototype method forces Safari to "cast" this string to what + * it's supposed to be, fixing the bug. */ + frame.prefix.charAt(0) + words.push(frame.prefix) + } + + for (var i = 0; i < len; i++) { + var edge = edges[i] + + stack.push({ + prefix: frame.prefix.concat(edge), + node: frame.node.edges[edge] + }) + } + } + + return words +} + +/** + * Generates a string representation of a TokenSet. + * + * This is intended to allow TokenSets to be used as keys + * in objects, largely to aid the construction and minimisation + * of a TokenSet. As such it is not designed to be a human + * friendly representation of the TokenSet. + * + * @returns {string} + */ +lunr.TokenSet.prototype.toString = function () { + // NOTE: Using Object.keys here as this.edges is very likely + // to enter 'hash-mode' with many keys being added + // + // avoiding a for-in loop here as it leads to the function + // being de-optimised (at least in V8). From some simple + // benchmarks the performance is comparable, but allowing + // V8 to optimize may mean easy performance wins in the future. + + if (this._str) { + return this._str + } + + var str = this.final ? '1' : '0', + labels = Object.keys(this.edges).sort(), + len = labels.length + + for (var i = 0; i < len; i++) { + var label = labels[i], + node = this.edges[label] + + str = str + label + node.id + } + + return str +} + +/** + * Returns a new TokenSet that is the intersection of + * this TokenSet and the passed TokenSet. + * + * This intersection will take into account any wildcards + * contained within the TokenSet. + * + * @param {lunr.TokenSet} b - An other TokenSet to intersect with. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.prototype.intersect = function (b) { + var output = new lunr.TokenSet, + frame = undefined + + var stack = [{ + qNode: b, + output: output, + node: this + }] + + while (stack.length) { + frame = stack.pop() + + // NOTE: As with the #toString method, we are using + // Object.keys and a for loop instead of a for-in loop + // as both of these objects enter 'hash' mode, causing + // the function to be de-optimised in V8 + var qEdges = Object.keys(frame.qNode.edges), + qLen = qEdges.length, + nEdges = Object.keys(frame.node.edges), + nLen = nEdges.length + + for (var q = 0; q < qLen; q++) { + var qEdge = qEdges[q] + + for (var n = 0; n < nLen; n++) { + var nEdge = nEdges[n] + + if (nEdge == qEdge || qEdge == '*') { + var node = frame.node.edges[nEdge], + qNode = frame.qNode.edges[qEdge], + final = node.final && qNode.final, + next = undefined + + if (nEdge in frame.output.edges) { + // an edge already exists for this character + // no need to create a new node, just set the finality + // bit unless this node is already final + next = frame.output.edges[nEdge] + next.final = next.final || final + + } else { + // no edge exists yet, must create one + // set the finality bit and insert it + // into the output + next = new lunr.TokenSet + next.final = final + frame.output.edges[nEdge] = next + } + + stack.push({ + qNode: qNode, + output: next, + node: node + }) + } + } + } + } + + return output +} +lunr.TokenSet.Builder = function () { + this.previousWord = "" + this.root = new lunr.TokenSet + this.uncheckedNodes = [] + this.minimizedNodes = {} +} + +lunr.TokenSet.Builder.prototype.insert = function (word) { + var node, + commonPrefix = 0 + + if (word < this.previousWord) { + throw new Error ("Out of order word insertion") + } + + for (var i = 0; i < word.length && i < this.previousWord.length; i++) { + if (word[i] != this.previousWord[i]) break + commonPrefix++ + } + + this.minimize(commonPrefix) + + if (this.uncheckedNodes.length == 0) { + node = this.root + } else { + node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child + } + + for (var i = commonPrefix; i < word.length; i++) { + var nextNode = new lunr.TokenSet, + char = word[i] + + node.edges[char] = nextNode + + this.uncheckedNodes.push({ + parent: node, + char: char, + child: nextNode + }) + + node = nextNode + } + + node.final = true + this.previousWord = word +} + +lunr.TokenSet.Builder.prototype.finish = function () { + this.minimize(0) +} + +lunr.TokenSet.Builder.prototype.minimize = function (downTo) { + for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) { + var node = this.uncheckedNodes[i], + childKey = node.child.toString() + + if (childKey in this.minimizedNodes) { + node.parent.edges[node.char] = this.minimizedNodes[childKey] + } else { + // Cache the key for this node since + // we know it can't change anymore + node.child._str = childKey + + this.minimizedNodes[childKey] = node.child + } + + this.uncheckedNodes.pop() + } +} +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * An index contains the built index of all documents and provides a query interface + * to the index. + * + * Usually instances of lunr.Index will not be created using this constructor, instead + * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be + * used to load previously built and serialized indexes. + * + * @constructor + * @param {Object} attrs - The attributes of the built search index. + * @param {Object} attrs.invertedIndex - An index of term/field to document reference. + * @param {Object} attrs.fieldVectors - Field vectors + * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens. + * @param {string[]} attrs.fields - The names of indexed document fields. + * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms. + */ +lunr.Index = function (attrs) { + this.invertedIndex = attrs.invertedIndex + this.fieldVectors = attrs.fieldVectors + this.tokenSet = attrs.tokenSet + this.fields = attrs.fields + this.pipeline = attrs.pipeline +} + +/** + * A result contains details of a document matching a search query. + * @typedef {Object} lunr.Index~Result + * @property {string} ref - The reference of the document this result represents. + * @property {number} score - A number between 0 and 1 representing how similar this document is to the query. + * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match. + */ + +/** + * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple + * query language which itself is parsed into an instance of lunr.Query. + * + * For programmatically building queries it is advised to directly use lunr.Query, the query language + * is best used for human entered text rather than program generated text. + * + * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported + * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello' + * or 'world', though those that contain both will rank higher in the results. + * + * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can + * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding + * wildcards will increase the number of documents that will be found but can also have a negative + * impact on query performance, especially with wildcards at the beginning of a term. + * + * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term + * hello in the title field will match this query. Using a field not present in the index will lead + * to an error being thrown. + * + * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term + * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported + * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2. + * Avoid large values for edit distance to improve query performance. + * + * Each term also supports a presence modifier. By default a term's presence in document is optional, however + * this can be changed to either required or prohibited. For a term's presence to be required in a document the + * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and + * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not + * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'. + * + * To escape special characters the backslash character '\' can be used, this allows searches to include + * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead + * of attempting to apply a boost of 2 to the search term "foo". + * + * @typedef {string} lunr.Index~QueryString + * @example Simple single term query + * hello + * @example Multiple term query + * hello world + * @example term scoped to a field + * title:hello + * @example term with a boost of 10 + * hello^10 + * @example term with an edit distance of 2 + * hello~2 + * @example terms with presence modifiers + * -foo +bar baz + */ + +/** + * Performs a search against the index using lunr query syntax. + * + * Results will be returned sorted by their score, the most relevant results + * will be returned first. For details on how the score is calculated, please see + * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}. + * + * For more programmatic querying use lunr.Index#query. + * + * @param {lunr.Index~QueryString} queryString - A string containing a lunr query. + * @throws {lunr.QueryParseError} If the passed query string cannot be parsed. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.search = function (queryString) { + return this.query(function (query) { + var parser = new lunr.QueryParser(queryString, query) + parser.parse() + }) +} + +/** + * A query builder callback provides a query object to be used to express + * the query to perform on the index. + * + * @callback lunr.Index~queryBuilder + * @param {lunr.Query} query - The query object to build up. + * @this lunr.Query + */ + +/** + * Performs a query against the index using the yielded lunr.Query object. + * + * If performing programmatic queries against the index, this method is preferred + * over lunr.Index#search so as to avoid the additional query parsing overhead. + * + * A query object is yielded to the supplied function which should be used to + * express the query to be run against the index. + * + * Note that although this function takes a callback parameter it is _not_ an + * asynchronous operation, the callback is just yielded a query object to be + * customized. + * + * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.query = function (fn) { + // for each query clause + // * process terms + // * expand terms from token set + // * find matching documents and metadata + // * get document vectors + // * score documents + + var query = new lunr.Query(this.fields), + matchingFields = Object.create(null), + queryVectors = Object.create(null), + termFieldCache = Object.create(null), + requiredMatches = Object.create(null), + prohibitedMatches = Object.create(null) + + /* + * To support field level boosts a query vector is created per + * field. An empty vector is eagerly created to support negated + * queries. + */ + for (var i = 0; i < this.fields.length; i++) { + queryVectors[this.fields[i]] = new lunr.Vector + } + + fn.call(query, query) + + for (var i = 0; i < query.clauses.length; i++) { + /* + * Unless the pipeline has been disabled for this term, which is + * the case for terms with wildcards, we need to pass the clause + * term through the search pipeline. A pipeline returns an array + * of processed terms. Pipeline functions may expand the passed + * term, which means we may end up performing multiple index lookups + * for a single query term. + */ + var clause = query.clauses[i], + terms = null, + clauseMatches = lunr.Set.empty + + if (clause.usePipeline) { + terms = this.pipeline.runString(clause.term, { + fields: clause.fields + }) + } else { + terms = [clause.term] + } + + for (var m = 0; m < terms.length; m++) { + var term = terms[m] + + /* + * Each term returned from the pipeline needs to use the same query + * clause object, e.g. the same boost and or edit distance. The + * simplest way to do this is to re-use the clause object but mutate + * its term property. + */ + clause.term = term + + /* + * From the term in the clause we create a token set which will then + * be used to intersect the indexes token set to get a list of terms + * to lookup in the inverted index + */ + var termTokenSet = lunr.TokenSet.fromClause(clause), + expandedTerms = this.tokenSet.intersect(termTokenSet).toArray() + + /* + * If a term marked as required does not exist in the tokenSet it is + * impossible for the search to return any matches. We set all the field + * scoped required matches set to empty and stop examining any further + * clauses. + */ + if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = lunr.Set.empty + } + + break + } + + for (var j = 0; j < expandedTerms.length; j++) { + /* + * For each term get the posting and termIndex, this is required for + * building the query vector. + */ + var expandedTerm = expandedTerms[j], + posting = this.invertedIndex[expandedTerm], + termIndex = posting._index + + for (var k = 0; k < clause.fields.length; k++) { + /* + * For each field that this query term is scoped by (by default + * all fields are in scope) we need to get all the document refs + * that have this term in that field. + * + * The posting is the entry in the invertedIndex for the matching + * term from above. + */ + var field = clause.fields[k], + fieldPosting = posting[field], + matchingDocumentRefs = Object.keys(fieldPosting), + termField = expandedTerm + "/" + field, + matchingDocumentsSet = new lunr.Set(matchingDocumentRefs) + + /* + * if the presence of this term is required ensure that the matching + * documents are added to the set of required matches for this clause. + * + */ + if (clause.presence == lunr.Query.presence.REQUIRED) { + clauseMatches = clauseMatches.union(matchingDocumentsSet) + + if (requiredMatches[field] === undefined) { + requiredMatches[field] = lunr.Set.complete + } + } + + /* + * if the presence of this term is prohibited ensure that the matching + * documents are added to the set of prohibited matches for this field, + * creating that set if it does not yet exist. + */ + if (clause.presence == lunr.Query.presence.PROHIBITED) { + if (prohibitedMatches[field] === undefined) { + prohibitedMatches[field] = lunr.Set.empty + } + + prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet) + + /* + * Prohibited matches should not be part of the query vector used for + * similarity scoring and no metadata should be extracted so we continue + * to the next field + */ + continue + } + + /* + * The query field vector is populated using the termIndex found for + * the term and a unit value with the appropriate boost applied. + * Using upsert because there could already be an entry in the vector + * for the term we are working with. In that case we just add the scores + * together. + */ + queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b }) + + /** + * If we've already seen this term, field combo then we've already collected + * the matching documents and metadata, no need to go through all that again + */ + if (termFieldCache[termField]) { + continue + } + + for (var l = 0; l < matchingDocumentRefs.length; l++) { + /* + * All metadata for this term/field/document triple + * are then extracted and collected into an instance + * of lunr.MatchData ready to be returned in the query + * results + */ + var matchingDocumentRef = matchingDocumentRefs[l], + matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field), + metadata = fieldPosting[matchingDocumentRef], + fieldMatch + + if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) { + matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata) + } else { + fieldMatch.add(expandedTerm, field, metadata) + } + + } + + termFieldCache[termField] = true + } + } + } + + /** + * If the presence was required we need to update the requiredMatches field sets. + * We do this after all fields for the term have collected their matches because + * the clause terms presence is required in _any_ of the fields not _all_ of the + * fields. + */ + if (clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = requiredMatches[field].intersect(clauseMatches) + } + } + } + + /** + * Need to combine the field scoped required and prohibited + * matching documents into a global set of required and prohibited + * matches + */ + var allRequiredMatches = lunr.Set.complete, + allProhibitedMatches = lunr.Set.empty + + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i] + + if (requiredMatches[field]) { + allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field]) + } + + if (prohibitedMatches[field]) { + allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field]) + } + } + + var matchingFieldRefs = Object.keys(matchingFields), + results = [], + matches = Object.create(null) + + /* + * If the query is negated (contains only prohibited terms) + * we need to get _all_ fieldRefs currently existing in the + * index. This is only done when we know that the query is + * entirely prohibited terms to avoid any cost of getting all + * fieldRefs unnecessarily. + * + * Additionally, blank MatchData must be created to correctly + * populate the results. + */ + if (query.isNegated()) { + matchingFieldRefs = Object.keys(this.fieldVectors) + + for (var i = 0; i < matchingFieldRefs.length; i++) { + var matchingFieldRef = matchingFieldRefs[i] + var fieldRef = lunr.FieldRef.fromString(matchingFieldRef) + matchingFields[matchingFieldRef] = new lunr.MatchData + } + } + + for (var i = 0; i < matchingFieldRefs.length; i++) { + /* + * Currently we have document fields that match the query, but we + * need to return documents. The matchData and scores are combined + * from multiple fields belonging to the same document. + * + * Scores are calculated by field, using the query vectors created + * above, and combined into a final document score using addition. + */ + var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]), + docRef = fieldRef.docRef + + if (!allRequiredMatches.contains(docRef)) { + continue + } + + if (allProhibitedMatches.contains(docRef)) { + continue + } + + var fieldVector = this.fieldVectors[fieldRef], + score = queryVectors[fieldRef.fieldName].similarity(fieldVector), + docMatch + + if ((docMatch = matches[docRef]) !== undefined) { + docMatch.score += score + docMatch.matchData.combine(matchingFields[fieldRef]) + } else { + var match = { + ref: docRef, + score: score, + matchData: matchingFields[fieldRef] + } + matches[docRef] = match + results.push(match) + } + } + + /* + * Sort the results objects by score, highest first. + */ + return results.sort(function (a, b) { + return b.score - a.score + }) +} + +/** + * Prepares the index for JSON serialization. + * + * The schema for this JSON blob will be described in a + * separate JSON schema file. + * + * @returns {Object} + */ +lunr.Index.prototype.toJSON = function () { + var invertedIndex = Object.keys(this.invertedIndex) + .sort() + .map(function (term) { + return [term, this.invertedIndex[term]] + }, this) + + var fieldVectors = Object.keys(this.fieldVectors) + .map(function (ref) { + return [ref, this.fieldVectors[ref].toJSON()] + }, this) + + return { + version: lunr.version, + fields: this.fields, + fieldVectors: fieldVectors, + invertedIndex: invertedIndex, + pipeline: this.pipeline.toJSON() + } +} + +/** + * Loads a previously serialized lunr.Index + * + * @param {Object} serializedIndex - A previously serialized lunr.Index + * @returns {lunr.Index} + */ +lunr.Index.load = function (serializedIndex) { + var attrs = {}, + fieldVectors = {}, + serializedVectors = serializedIndex.fieldVectors, + invertedIndex = Object.create(null), + serializedInvertedIndex = serializedIndex.invertedIndex, + tokenSetBuilder = new lunr.TokenSet.Builder, + pipeline = lunr.Pipeline.load(serializedIndex.pipeline) + + if (serializedIndex.version != lunr.version) { + lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'") + } + + for (var i = 0; i < serializedVectors.length; i++) { + var tuple = serializedVectors[i], + ref = tuple[0], + elements = tuple[1] + + fieldVectors[ref] = new lunr.Vector(elements) + } + + for (var i = 0; i < serializedInvertedIndex.length; i++) { + var tuple = serializedInvertedIndex[i], + term = tuple[0], + posting = tuple[1] + + tokenSetBuilder.insert(term) + invertedIndex[term] = posting + } + + tokenSetBuilder.finish() + + attrs.fields = serializedIndex.fields + + attrs.fieldVectors = fieldVectors + attrs.invertedIndex = invertedIndex + attrs.tokenSet = tokenSetBuilder.root + attrs.pipeline = pipeline + + return new lunr.Index(attrs) +} +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Builder performs indexing on a set of documents and + * returns instances of lunr.Index ready for querying. + * + * All configuration of the index is done via the builder, the + * fields to index, the document reference, the text processing + * pipeline and document scoring parameters are all set on the + * builder before indexing. + * + * @constructor + * @property {string} _ref - Internal reference to the document reference field. + * @property {string[]} _fields - Internal reference to the document fields to index. + * @property {object} invertedIndex - The inverted index maps terms to document fields. + * @property {object} documentTermFrequencies - Keeps track of document term frequencies. + * @property {object} documentLengths - Keeps track of the length of documents added to the index. + * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing. + * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing. + * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index. + * @property {number} documentCount - Keeps track of the total number of documents indexed. + * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75. + * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2. + * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space. + * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index. + */ +lunr.Builder = function () { + this._ref = "id" + this._fields = Object.create(null) + this._documents = Object.create(null) + this.invertedIndex = Object.create(null) + this.fieldTermFrequencies = {} + this.fieldLengths = {} + this.tokenizer = lunr.tokenizer + this.pipeline = new lunr.Pipeline + this.searchPipeline = new lunr.Pipeline + this.documentCount = 0 + this._b = 0.75 + this._k1 = 1.2 + this.termIndex = 0 + this.metadataWhitelist = [] +} + +/** + * Sets the document field used as the document reference. Every document must have this field. + * The type of this field in the document should be a string, if it is not a string it will be + * coerced into a string by calling toString. + * + * The default ref is 'id'. + * + * The ref should _not_ be changed during indexing, it should be set before any documents are + * added to the index. Changing it during indexing can lead to inconsistent results. + * + * @param {string} ref - The name of the reference field in the document. + */ +lunr.Builder.prototype.ref = function (ref) { + this._ref = ref +} + +/** + * A function that is used to extract a field from a document. + * + * Lunr expects a field to be at the top level of a document, if however the field + * is deeply nested within a document an extractor function can be used to extract + * the right field for indexing. + * + * @callback fieldExtractor + * @param {object} doc - The document being added to the index. + * @returns {?(string|object|object[])} obj - The object that will be indexed for this field. + * @example Extracting a nested field + * function (doc) { return doc.nested.field } + */ + +/** + * Adds a field to the list of document fields that will be indexed. Every document being + * indexed should have this field. Null values for this field in indexed documents will + * not cause errors but will limit the chance of that document being retrieved by searches. + * + * All fields should be added before adding documents to the index. Adding fields after + * a document has been indexed will have no effect on already indexed documents. + * + * Fields can be boosted at build time. This allows terms within that field to have more + * importance when ranking search results. Use a field boost to specify that matches within + * one field are more important than other fields. + * + * @param {string} fieldName - The name of a field to index in all documents. + * @param {object} attributes - Optional attributes associated with this field. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this field. + * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document. + * @throws {RangeError} fieldName cannot contain unsupported characters '/' + */ +lunr.Builder.prototype.field = function (fieldName, attributes) { + if (/\//.test(fieldName)) { + throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'") + } + + this._fields[fieldName] = attributes || {} +} + +/** + * A parameter to tune the amount of field length normalisation that is applied when + * calculating relevance scores. A value of 0 will completely disable any normalisation + * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b + * will be clamped to the range 0 - 1. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.b = function (number) { + if (number < 0) { + this._b = 0 + } else if (number > 1) { + this._b = 1 + } else { + this._b = number + } +} + +/** + * A parameter that controls the speed at which a rise in term frequency results in term + * frequency saturation. The default value is 1.2. Setting this to a higher value will give + * slower saturation levels, a lower value will result in quicker saturation. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.k1 = function (number) { + this._k1 = number +} + +/** + * Adds a document to the index. + * + * Before adding fields to the index the index should have been fully setup, with the document + * ref and all fields to index already having been specified. + * + * The document must have a field name as specified by the ref (by default this is 'id') and + * it should have all fields defined for indexing, though null or undefined values will not + * cause errors. + * + * Entire documents can be boosted at build time. Applying a boost to a document indicates that + * this document should rank higher in search results than other documents. + * + * @param {object} doc - The document to add to the index. + * @param {object} attributes - Optional attributes associated with this document. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this document. + */ +lunr.Builder.prototype.add = function (doc, attributes) { + var docRef = doc[this._ref], + fields = Object.keys(this._fields) + + this._documents[docRef] = attributes || {} + this.documentCount += 1 + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i], + extractor = this._fields[fieldName].extractor, + field = extractor ? extractor(doc) : doc[fieldName], + tokens = this.tokenizer(field, { + fields: [fieldName] + }), + terms = this.pipeline.run(tokens), + fieldRef = new lunr.FieldRef (docRef, fieldName), + fieldTerms = Object.create(null) + + this.fieldTermFrequencies[fieldRef] = fieldTerms + this.fieldLengths[fieldRef] = 0 + + // store the length of this field for this document + this.fieldLengths[fieldRef] += terms.length + + // calculate term frequencies for this field + for (var j = 0; j < terms.length; j++) { + var term = terms[j] + + if (fieldTerms[term] == undefined) { + fieldTerms[term] = 0 + } + + fieldTerms[term] += 1 + + // add to inverted index + // create an initial posting if one doesn't exist + if (this.invertedIndex[term] == undefined) { + var posting = Object.create(null) + posting["_index"] = this.termIndex + this.termIndex += 1 + + for (var k = 0; k < fields.length; k++) { + posting[fields[k]] = Object.create(null) + } + + this.invertedIndex[term] = posting + } + + // add an entry for this term/fieldName/docRef to the invertedIndex + if (this.invertedIndex[term][fieldName][docRef] == undefined) { + this.invertedIndex[term][fieldName][docRef] = Object.create(null) + } + + // store all whitelisted metadata about this token in the + // inverted index + for (var l = 0; l < this.metadataWhitelist.length; l++) { + var metadataKey = this.metadataWhitelist[l], + metadata = term.metadata[metadataKey] + + if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) { + this.invertedIndex[term][fieldName][docRef][metadataKey] = [] + } + + this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata) + } + } + + } +} + +/** + * Calculates the average document length for this index + * + * @private + */ +lunr.Builder.prototype.calculateAverageFieldLengths = function () { + + var fieldRefs = Object.keys(this.fieldLengths), + numberOfFields = fieldRefs.length, + accumulator = {}, + documentsWithField = {} + + for (var i = 0; i < numberOfFields; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + field = fieldRef.fieldName + + documentsWithField[field] || (documentsWithField[field] = 0) + documentsWithField[field] += 1 + + accumulator[field] || (accumulator[field] = 0) + accumulator[field] += this.fieldLengths[fieldRef] + } + + var fields = Object.keys(this._fields) + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i] + accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName] + } + + this.averageFieldLength = accumulator +} + +/** + * Builds a vector space model of every document using lunr.Vector + * + * @private + */ +lunr.Builder.prototype.createFieldVectors = function () { + var fieldVectors = {}, + fieldRefs = Object.keys(this.fieldTermFrequencies), + fieldRefsLength = fieldRefs.length, + termIdfCache = Object.create(null) + + for (var i = 0; i < fieldRefsLength; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + fieldName = fieldRef.fieldName, + fieldLength = this.fieldLengths[fieldRef], + fieldVector = new lunr.Vector, + termFrequencies = this.fieldTermFrequencies[fieldRef], + terms = Object.keys(termFrequencies), + termsLength = terms.length + + + var fieldBoost = this._fields[fieldName].boost || 1, + docBoost = this._documents[fieldRef.docRef].boost || 1 + + for (var j = 0; j < termsLength; j++) { + var term = terms[j], + tf = termFrequencies[term], + termIndex = this.invertedIndex[term]._index, + idf, score, scoreWithPrecision + + if (termIdfCache[term] === undefined) { + idf = lunr.idf(this.invertedIndex[term], this.documentCount) + termIdfCache[term] = idf + } else { + idf = termIdfCache[term] + } + + score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf) + score *= fieldBoost + score *= docBoost + scoreWithPrecision = Math.round(score * 1000) / 1000 + // Converts 1.23456789 to 1.234. + // Reducing the precision so that the vectors take up less + // space when serialised. Doing it now so that they behave + // the same before and after serialisation. Also, this is + // the fastest approach to reducing a number's precision in + // JavaScript. + + fieldVector.insert(termIndex, scoreWithPrecision) + } + + fieldVectors[fieldRef] = fieldVector + } + + this.fieldVectors = fieldVectors +} + +/** + * Creates a token set of all tokens in the index using lunr.TokenSet + * + * @private + */ +lunr.Builder.prototype.createTokenSet = function () { + this.tokenSet = lunr.TokenSet.fromArray( + Object.keys(this.invertedIndex).sort() + ) +} + +/** + * Builds the index, creating an instance of lunr.Index. + * + * This completes the indexing process and should only be called + * once all documents have been added to the index. + * + * @returns {lunr.Index} + */ +lunr.Builder.prototype.build = function () { + this.calculateAverageFieldLengths() + this.createFieldVectors() + this.createTokenSet() + + return new lunr.Index({ + invertedIndex: this.invertedIndex, + fieldVectors: this.fieldVectors, + tokenSet: this.tokenSet, + fields: Object.keys(this._fields), + pipeline: this.searchPipeline + }) +} + +/** + * Applies a plugin to the index builder. + * + * A plugin is a function that is called with the index builder as its context. + * Plugins can be used to customise or extend the behaviour of the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied when building the index. + * + * The plugin function will be called with the index builder as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index builder as its context. + * + * @param {Function} plugin The plugin to apply. + */ +lunr.Builder.prototype.use = function (fn) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + fn.apply(this, args) +} +/** + * Contains and collects metadata about a matching document. + * A single instance of lunr.MatchData is returned as part of every + * lunr.Index~Result. + * + * @constructor + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + * @property {object} metadata - A cloned collection of metadata associated with this document. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData = function (term, field, metadata) { + var clonedMetadata = Object.create(null), + metadataKeys = Object.keys(metadata || {}) + + // Cloning the metadata to prevent the original + // being mutated during match data combination. + // Metadata is kept in an array within the inverted + // index so cloning the data can be done with + // Array#slice + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + clonedMetadata[key] = metadata[key].slice() + } + + this.metadata = Object.create(null) + + if (term !== undefined) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = clonedMetadata + } +} + +/** + * An instance of lunr.MatchData will be created for every term that matches a + * document. However only one instance is required in a lunr.Index~Result. This + * method combines metadata from another instance of lunr.MatchData with this + * objects metadata. + * + * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData.prototype.combine = function (otherMatchData) { + var terms = Object.keys(otherMatchData.metadata) + + for (var i = 0; i < terms.length; i++) { + var term = terms[i], + fields = Object.keys(otherMatchData.metadata[term]) + + if (this.metadata[term] == undefined) { + this.metadata[term] = Object.create(null) + } + + for (var j = 0; j < fields.length; j++) { + var field = fields[j], + keys = Object.keys(otherMatchData.metadata[term][field]) + + if (this.metadata[term][field] == undefined) { + this.metadata[term][field] = Object.create(null) + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k] + + if (this.metadata[term][field][key] == undefined) { + this.metadata[term][field][key] = otherMatchData.metadata[term][field][key] + } else { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key]) + } + + } + } + } +} + +/** + * Add metadata for a term/field pair to this instance of match data. + * + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + */ +lunr.MatchData.prototype.add = function (term, field, metadata) { + if (!(term in this.metadata)) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = metadata + return + } + + if (!(field in this.metadata[term])) { + this.metadata[term][field] = metadata + return + } + + var metadataKeys = Object.keys(metadata) + + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + + if (key in this.metadata[term][field]) { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key]) + } else { + this.metadata[term][field][key] = metadata[key] + } + } +} +/** + * A lunr.Query provides a programmatic way of defining queries to be performed + * against a {@link lunr.Index}. + * + * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method + * so the query object is pre-initialized with the right index fields. + * + * @constructor + * @property {lunr.Query~Clause[]} clauses - An array of query clauses. + * @property {string[]} allFields - An array of all available fields in a lunr.Index. + */ +lunr.Query = function (allFields) { + this.clauses = [] + this.allFields = allFields +} + +/** + * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause. + * + * This allows wildcards to be added to the beginning and end of a term without having to manually do any string + * concatenation. + * + * The wildcard constants can be bitwise combined to select both leading and trailing wildcards. + * + * @constant + * @default + * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour + * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists + * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with trailing wildcard + * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING }) + * @example query term with leading and trailing wildcard + * query.term('foo', { + * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING + * }) + */ + +lunr.Query.wildcard = new String ("*") +lunr.Query.wildcard.NONE = 0 +lunr.Query.wildcard.LEADING = 1 +lunr.Query.wildcard.TRAILING = 2 + +/** + * Constants for indicating what kind of presence a term must have in matching documents. + * + * @constant + * @enum {number} + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with required presence + * query.term('foo', { presence: lunr.Query.presence.REQUIRED }) + */ +lunr.Query.presence = { + /** + * Term's presence in a document is optional, this is the default value. + */ + OPTIONAL: 1, + + /** + * Term's presence in a document is required, documents that do not contain + * this term will not be returned. + */ + REQUIRED: 2, + + /** + * Term's presence in a document is prohibited, documents that do contain + * this term will not be returned. + */ + PROHIBITED: 3 +} + +/** + * A single clause in a {@link lunr.Query} contains a term and details on how to + * match that term against a {@link lunr.Index}. + * + * @typedef {Object} lunr.Query~Clause + * @property {string[]} fields - The fields in an index this clause should be matched against. + * @property {number} [boost=1] - Any boost that should be applied when matching this clause. + * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be. + * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline. + * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended. + * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents. + */ + +/** + * Adds a {@link lunr.Query~Clause} to this query. + * + * Unless the clause contains the fields to be matched all fields will be matched. In addition + * a default boost of 1 is applied to the clause. + * + * @param {lunr.Query~Clause} clause - The clause to add to this query. + * @see lunr.Query~Clause + * @returns {lunr.Query} + */ +lunr.Query.prototype.clause = function (clause) { + if (!('fields' in clause)) { + clause.fields = this.allFields + } + + if (!('boost' in clause)) { + clause.boost = 1 + } + + if (!('usePipeline' in clause)) { + clause.usePipeline = true + } + + if (!('wildcard' in clause)) { + clause.wildcard = lunr.Query.wildcard.NONE + } + + if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) { + clause.term = "*" + clause.term + } + + if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) { + clause.term = "" + clause.term + "*" + } + + if (!('presence' in clause)) { + clause.presence = lunr.Query.presence.OPTIONAL + } + + this.clauses.push(clause) + + return this +} + +/** + * A negated query is one in which every clause has a presence of + * prohibited. These queries require some special processing to return + * the expected results. + * + * @returns boolean + */ +lunr.Query.prototype.isNegated = function () { + for (var i = 0; i < this.clauses.length; i++) { + if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) { + return false + } + } + + return true +} + +/** + * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause} + * to the list of clauses that make up this query. + * + * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion + * to a token or token-like string should be done before calling this method. + * + * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an + * array, each term in the array will share the same options. + * + * @param {object|object[]} term - The term(s) to add to the query. + * @param {object} [options] - Any additional properties to add to the query clause. + * @returns {lunr.Query} + * @see lunr.Query#clause + * @see lunr.Query~Clause + * @example adding a single term to a query + * query.term("foo") + * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard + * query.term("foo", { + * fields: ["title"], + * boost: 10, + * wildcard: lunr.Query.wildcard.TRAILING + * }) + * @example using lunr.tokenizer to convert a string to tokens before using them as terms + * query.term(lunr.tokenizer("foo bar")) + */ +lunr.Query.prototype.term = function (term, options) { + if (Array.isArray(term)) { + term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this) + return this + } + + var clause = options || {} + clause.term = term.toString() + + this.clause(clause) + + return this +} +lunr.QueryParseError = function (message, start, end) { + this.name = "QueryParseError" + this.message = message + this.start = start + this.end = end +} + +lunr.QueryParseError.prototype = new Error +lunr.QueryLexer = function (str) { + this.lexemes = [] + this.str = str + this.length = str.length + this.pos = 0 + this.start = 0 + this.escapeCharPositions = [] +} + +lunr.QueryLexer.prototype.run = function () { + var state = lunr.QueryLexer.lexText + + while (state) { + state = state(this) + } +} + +lunr.QueryLexer.prototype.sliceString = function () { + var subSlices = [], + sliceStart = this.start, + sliceEnd = this.pos + + for (var i = 0; i < this.escapeCharPositions.length; i++) { + sliceEnd = this.escapeCharPositions[i] + subSlices.push(this.str.slice(sliceStart, sliceEnd)) + sliceStart = sliceEnd + 1 + } + + subSlices.push(this.str.slice(sliceStart, this.pos)) + this.escapeCharPositions.length = 0 + + return subSlices.join('') +} + +lunr.QueryLexer.prototype.emit = function (type) { + this.lexemes.push({ + type: type, + str: this.sliceString(), + start: this.start, + end: this.pos + }) + + this.start = this.pos +} + +lunr.QueryLexer.prototype.escapeCharacter = function () { + this.escapeCharPositions.push(this.pos - 1) + this.pos += 1 +} + +lunr.QueryLexer.prototype.next = function () { + if (this.pos >= this.length) { + return lunr.QueryLexer.EOS + } + + var char = this.str.charAt(this.pos) + this.pos += 1 + return char +} + +lunr.QueryLexer.prototype.width = function () { + return this.pos - this.start +} + +lunr.QueryLexer.prototype.ignore = function () { + if (this.start == this.pos) { + this.pos += 1 + } + + this.start = this.pos +} + +lunr.QueryLexer.prototype.backup = function () { + this.pos -= 1 +} + +lunr.QueryLexer.prototype.acceptDigitRun = function () { + var char, charCode + + do { + char = this.next() + charCode = char.charCodeAt(0) + } while (charCode > 47 && charCode < 58) + + if (char != lunr.QueryLexer.EOS) { + this.backup() + } +} + +lunr.QueryLexer.prototype.more = function () { + return this.pos < this.length +} + +lunr.QueryLexer.EOS = 'EOS' +lunr.QueryLexer.FIELD = 'FIELD' +lunr.QueryLexer.TERM = 'TERM' +lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE' +lunr.QueryLexer.BOOST = 'BOOST' +lunr.QueryLexer.PRESENCE = 'PRESENCE' + +lunr.QueryLexer.lexField = function (lexer) { + lexer.backup() + lexer.emit(lunr.QueryLexer.FIELD) + lexer.ignore() + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexTerm = function (lexer) { + if (lexer.width() > 1) { + lexer.backup() + lexer.emit(lunr.QueryLexer.TERM) + } + + lexer.ignore() + + if (lexer.more()) { + return lunr.QueryLexer.lexText + } +} + +lunr.QueryLexer.lexEditDistance = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.EDIT_DISTANCE) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexBoost = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.BOOST) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexEOS = function (lexer) { + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } +} + +// This matches the separator used when tokenising fields +// within a document. These should match otherwise it is +// not possible to search for some tokens within a document. +// +// It is possible for the user to change the separator on the +// tokenizer so it _might_ clash with any other of the special +// characters already used within the search string, e.g. :. +// +// This means that it is possible to change the separator in +// such a way that makes some words unsearchable using a search +// string. +lunr.QueryLexer.termSeparator = lunr.tokenizer.separator + +lunr.QueryLexer.lexText = function (lexer) { + while (true) { + var char = lexer.next() + + if (char == lunr.QueryLexer.EOS) { + return lunr.QueryLexer.lexEOS + } + + // Escape character is '\' + if (char.charCodeAt(0) == 92) { + lexer.escapeCharacter() + continue + } + + if (char == ":") { + return lunr.QueryLexer.lexField + } + + if (char == "~") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexEditDistance + } + + if (char == "^") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexBoost + } + + // "+" indicates term presence is required + // checking for length to ensure that only + // leading "+" are considered + if (char == "+" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + // "-" indicates term presence is prohibited + // checking for length to ensure that only + // leading "-" are considered + if (char == "-" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + if (char.match(lunr.QueryLexer.termSeparator)) { + return lunr.QueryLexer.lexTerm + } + } +} + +lunr.QueryParser = function (str, query) { + this.lexer = new lunr.QueryLexer (str) + this.query = query + this.currentClause = {} + this.lexemeIdx = 0 +} + +lunr.QueryParser.prototype.parse = function () { + this.lexer.run() + this.lexemes = this.lexer.lexemes + + var state = lunr.QueryParser.parseClause + + while (state) { + state = state(this) + } + + return this.query +} + +lunr.QueryParser.prototype.peekLexeme = function () { + return this.lexemes[this.lexemeIdx] +} + +lunr.QueryParser.prototype.consumeLexeme = function () { + var lexeme = this.peekLexeme() + this.lexemeIdx += 1 + return lexeme +} + +lunr.QueryParser.prototype.nextClause = function () { + var completedClause = this.currentClause + this.query.clause(completedClause) + this.currentClause = {} +} + +lunr.QueryParser.parseClause = function (parser) { + var lexeme = parser.peekLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.type) { + case lunr.QueryLexer.PRESENCE: + return lunr.QueryParser.parsePresence + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expected either a field or a term, found " + lexeme.type + + if (lexeme.str.length >= 1) { + errorMessage += " with value '" + lexeme.str + "'" + } + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } +} + +lunr.QueryParser.parsePresence = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.str) { + case "-": + parser.currentClause.presence = lunr.Query.presence.PROHIBITED + break + case "+": + parser.currentClause.presence = lunr.Query.presence.REQUIRED + break + default: + var errorMessage = "unrecognised presence operator'" + lexeme.str + "'" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term or field, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseField = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + if (parser.query.allFields.indexOf(lexeme.str) == -1) { + var possibleFields = parser.query.allFields.map(function (f) { return "'" + f + "'" }).join(', '), + errorMessage = "unrecognised field '" + lexeme.str + "', possible fields: " + possibleFields + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.fields = [lexeme.str] + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseTerm = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + parser.currentClause.term = lexeme.str.toLowerCase() + + if (lexeme.str.indexOf("*") != -1) { + parser.currentClause.usePipeline = false + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseEditDistance = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var editDistance = parseInt(lexeme.str, 10) + + if (isNaN(editDistance)) { + var errorMessage = "edit distance must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.editDistance = editDistance + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseBoost = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var boost = parseInt(lexeme.str, 10) + + if (isNaN(boost)) { + var errorMessage = "boost must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.boost = boost + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like enviroments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})(); diff --git a/assets/js/lunr/lunr.min.js b/assets/js/lunr/lunr.min.js new file mode 100644 index 0000000..cdc94cd --- /dev/null +++ b/assets/js/lunr/lunr.min.js @@ -0,0 +1,6 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ +!function(){var e=function(t){var r=new e.Builder;return r.pipeline.add(e.trimmer,e.stopWordFilter,e.stemmer),r.searchPipeline.add(e.stemmer),t.call(r,r),r.build()};e.version="2.3.9",e.utils={},e.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),e.utils.asString=function(e){return void 0===e||null===e?"":e.toString()},e.utils.clone=function(e){if(null===e||void 0===e)return e;for(var t=Object.create(null),r=Object.keys(e),i=0;i0){var c=e.utils.clone(r)||{};c.position=[a,l],c.index=s.length,s.push(new e.Token(i.slice(a,o),c))}a=o+1}}return s},e.tokenizer.separator=/[\s\-]+/,e.Pipeline=function(){this._stack=[]},e.Pipeline.registeredFunctions=Object.create(null),e.Pipeline.registerFunction=function(t,r){r in this.registeredFunctions&&e.utils.warn("Overwriting existing registered function: "+r),t.label=r,e.Pipeline.registeredFunctions[t.label]=t},e.Pipeline.warnIfFunctionNotRegistered=function(t){var r=t.label&&t.label in this.registeredFunctions;r||e.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",t)},e.Pipeline.load=function(t){var r=new e.Pipeline;return t.forEach(function(t){var i=e.Pipeline.registeredFunctions[t];if(!i)throw new Error("Cannot load unregistered function: "+t);r.add(i)}),r},e.Pipeline.prototype.add=function(){var t=Array.prototype.slice.call(arguments);t.forEach(function(t){e.Pipeline.warnIfFunctionNotRegistered(t),this._stack.push(t)},this)},e.Pipeline.prototype.after=function(t,r){e.Pipeline.warnIfFunctionNotRegistered(r);var i=this._stack.indexOf(t);if(i==-1)throw new Error("Cannot find existingFn");i+=1,this._stack.splice(i,0,r)},e.Pipeline.prototype.before=function(t,r){e.Pipeline.warnIfFunctionNotRegistered(r);var i=this._stack.indexOf(t);if(i==-1)throw new Error("Cannot find existingFn");this._stack.splice(i,0,r)},e.Pipeline.prototype.remove=function(e){var t=this._stack.indexOf(e);t!=-1&&this._stack.splice(t,1)},e.Pipeline.prototype.run=function(e){for(var t=this._stack.length,r=0;r1&&(se&&(r=n),s!=e);)i=r-t,n=t+Math.floor(i/2),s=this.elements[2*n];return s==e?2*n:s>e?2*n:sa?l+=2:o==a&&(t+=r[u+1]*i[l+1],u+=2,l+=2);return t},e.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},e.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),t=1,r=0;t0){var o,a=s.str.charAt(0);a in s.node.edges?o=s.node.edges[a]:(o=new e.TokenSet,s.node.edges[a]=o),1==s.str.length&&(o["final"]=!0),n.push({node:o,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(0!=s.editsRemaining){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new e.TokenSet;s.node.edges["*"]=u}if(0==s.str.length&&(u["final"]=!0),n.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&n.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),1==s.str.length&&(s.node["final"]=!0),s.str.length>=1){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new e.TokenSet;s.node.edges["*"]=l}1==s.str.length&&(l["final"]=!0),n.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var c,h=s.str.charAt(0),d=s.str.charAt(1);d in s.node.edges?c=s.node.edges[d]:(c=new e.TokenSet,s.node.edges[d]=c),1==s.str.length&&(c["final"]=!0),n.push({node:c,editsRemaining:s.editsRemaining-1,str:h+s.str.slice(2)})}}}return i},e.TokenSet.fromString=function(t){for(var r=new e.TokenSet,i=r,n=0,s=t.length;n=e;t--){var r=this.uncheckedNodes[t],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r["char"]]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}},e.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},e.Index.prototype.search=function(t){return this.query(function(r){var i=new e.QueryParser(t,r);i.parse()})},e.Index.prototype.query=function(t){for(var r=new e.Query(this.fields),i=Object.create(null),n=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},e.Builder.prototype.k1=function(e){this._k1=e},e.Builder.prototype.add=function(t,r){var i=t[this._ref],n=Object.keys(this._fields);this._documents[i]=r||{},this.documentCount+=1;for(var s=0;s=this.length)return e.QueryLexer.EOS;var t=this.str.charAt(this.pos);return this.pos+=1,t},e.QueryLexer.prototype.width=function(){return this.pos-this.start},e.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},e.QueryLexer.prototype.backup=function(){this.pos-=1},e.QueryLexer.prototype.acceptDigitRun=function(){var t,r;do t=this.next(),r=t.charCodeAt(0);while(r>47&&r<58);t!=e.QueryLexer.EOS&&this.backup()},e.QueryLexer.prototype.more=function(){return this.pos1&&(t.backup(),t.emit(e.QueryLexer.TERM)),t.ignore(),t.more())return e.QueryLexer.lexText},e.QueryLexer.lexEditDistance=function(t){return t.ignore(),t.acceptDigitRun(),t.emit(e.QueryLexer.EDIT_DISTANCE),e.QueryLexer.lexText},e.QueryLexer.lexBoost=function(t){return t.ignore(),t.acceptDigitRun(),t.emit(e.QueryLexer.BOOST),e.QueryLexer.lexText},e.QueryLexer.lexEOS=function(t){t.width()>0&&t.emit(e.QueryLexer.TERM)},e.QueryLexer.termSeparator=e.tokenizer.separator,e.QueryLexer.lexText=function(t){for(;;){var r=t.next();if(r==e.QueryLexer.EOS)return e.QueryLexer.lexEOS;if(92!=r.charCodeAt(0)){if(":"==r)return e.QueryLexer.lexField;if("~"==r)return t.backup(),t.width()>0&&t.emit(e.QueryLexer.TERM),e.QueryLexer.lexEditDistance;if("^"==r)return t.backup(),t.width()>0&&t.emit(e.QueryLexer.TERM),e.QueryLexer.lexBoost;if("+"==r&&1===t.width())return t.emit(e.QueryLexer.PRESENCE),e.QueryLexer.lexText;if("-"==r&&1===t.width())return t.emit(e.QueryLexer.PRESENCE),e.QueryLexer.lexText;if(r.match(e.QueryLexer.termSeparator))return e.QueryLexer.lexTerm}else t.escapeCharacter()}},e.QueryParser=function(t,r){this.lexer=new e.QueryLexer(t),this.query=r,this.currentClause={},this.lexemeIdx=0},e.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var t=e.QueryParser.parseClause;t;)t=t(this);return this.query},e.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},e.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},e.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},e.QueryParser.parseClause=function(t){var r=t.peekLexeme();if(void 0!=r)switch(r.type){case e.QueryLexer.PRESENCE:return e.QueryParser.parsePresence;case e.QueryLexer.FIELD:return e.QueryParser.parseField;case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var i="expected either a field or a term, found "+r.type;throw r.str.length>=1&&(i+=" with value '"+r.str+"'"),new e.QueryParseError(i,r.start,r.end)}},e.QueryParser.parsePresence=function(t){var r=t.consumeLexeme();if(void 0!=r){switch(r.str){case"-":t.currentClause.presence=e.Query.presence.PROHIBITED;break;case"+":t.currentClause.presence=e.Query.presence.REQUIRED;break;default:var i="unrecognised presence operator'"+r.str+"'";throw new e.QueryParseError(i,r.start,r.end)}var n=t.peekLexeme();if(void 0==n){var i="expecting term or field, found nothing";throw new e.QueryParseError(i,r.start,r.end)}switch(n.type){case e.QueryLexer.FIELD:return e.QueryParser.parseField;case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var i="expecting term or field, found '"+n.type+"'";throw new e.QueryParseError(i,n.start,n.end)}}},e.QueryParser.parseField=function(t){var r=t.consumeLexeme();if(void 0!=r){if(t.query.allFields.indexOf(r.str)==-1){var i=t.query.allFields.map(function(e){return"'"+e+"'"}).join(", "),n="unrecognised field '"+r.str+"', possible fields: "+i;throw new e.QueryParseError(n,r.start,r.end)}t.currentClause.fields=[r.str];var s=t.peekLexeme();if(void 0==s){var n="expecting term, found nothing";throw new e.QueryParseError(n,r.start,r.end)}switch(s.type){case e.QueryLexer.TERM:return e.QueryParser.parseTerm;default:var n="expecting term, found '"+s.type+"'";throw new e.QueryParseError(n,s.start,s.end)}}},e.QueryParser.parseTerm=function(t){var r=t.consumeLexeme();if(void 0!=r){t.currentClause.term=r.str.toLowerCase(),r.str.indexOf("*")!=-1&&(t.currentClause.usePipeline=!1);var i=t.peekLexeme();if(void 0==i)return void t.nextClause();switch(i.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var n="Unexpected lexeme type '"+i.type+"'";throw new e.QueryParseError(n,i.start,i.end)}}},e.QueryParser.parseEditDistance=function(t){var r=t.consumeLexeme();if(void 0!=r){var i=parseInt(r.str,10);if(isNaN(i)){var n="edit distance must be numeric";throw new e.QueryParseError(n,r.start,r.end)}t.currentClause.editDistance=i;var s=t.peekLexeme();if(void 0==s)return void t.nextClause();switch(s.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var n="Unexpected lexeme type '"+s.type+"'";throw new e.QueryParseError(n,s.start,s.end)}}},e.QueryParser.parseBoost=function(t){var r=t.consumeLexeme();if(void 0!=r){var i=parseInt(r.str,10);if(isNaN(i)){var n="boost must be numeric";throw new e.QueryParseError(n,r.start,r.end)}t.currentClause.boost=i;var s=t.peekLexeme();if(void 0==s)return void t.nextClause();switch(s.type){case e.QueryLexer.TERM:return t.nextClause(),e.QueryParser.parseTerm;case e.QueryLexer.FIELD:return t.nextClause(),e.QueryParser.parseField;case e.QueryLexer.EDIT_DISTANCE:return e.QueryParser.parseEditDistance;case e.QueryLexer.BOOST:return e.QueryParser.parseBoost;case e.QueryLexer.PRESENCE:return t.nextClause(),e.QueryParser.parsePresence;default:var n="Unexpected lexeme type '"+s.type+"'";throw new e.QueryParseError(n,s.start,s.end)}}},function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():e.lunr=t()}(this,function(){return e})}(); diff --git a/assets/js/main.min.js b/assets/js/main.min.js new file mode 100644 index 0000000..8b2983e --- /dev/null +++ b/assets/js/main.min.js @@ -0,0 +1,6 @@ +/*! + * Minimal Mistakes Jekyll Theme 4.24.0 by Michael Rose + * Copyright 2013-2021 Michael Rose - mademistakes.com | @mmistakes + * Licensed under MIT + */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";function m(e){return null!=e&&e===e.window}var t=[],n=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,o=t.indexOf,r={},i=r.toString,v=r.hasOwnProperty,a=v.toString,l=a.call(Object),y={},b=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},T=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function x(e,t,n){var r,o,i=(n=n||T).createElement("script");if(i.text=e,t)for(r in c)(o=t[r]||t.getAttribute&&t.getAttribute(r))&&i.setAttribute(r,o);n.head.appendChild(i).parentNode.removeChild(i)}function h(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?r[i.call(e)]||"object":typeof e}var f="3.5.1",E=function(e,t){return new E.fn.init(e,t)};function d(e){var t=!!e&&"length"in e&&e.length,n=h(e);return!b(e)&&!m(e)&&("array"===n||0===t||"number"==typeof t&&0>10|55296,1023&e|56320))}function r(){C()}var e,d,x,i,o,p,h,m,w,u,l,C,T,a,E,g,s,c,v,S="sizzle"+ +new Date,y=n.document,k=0,b=0,A=ue(),N=ue(),j=ue(),I=ue(),L=function(e,t){return e===t&&(l=!0),0},D={}.hasOwnProperty,t=[],O=t.pop,H=t.push,P=t.push,q=t.slice,M=function(e,t){for(var n=0,r=e.length;n+~]|"+$+")"+$+"*"),Q=new RegExp($+"|>"),Y=new RegExp(F),V=new RegExp("^"+R+"$"),G={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+B),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+$+"*(even|odd|(([+-]|)(\\d*)n|)"+$+"*(?:([+-]|)"+$+"*(\\d+)|))"+$+"*\\)|)","i"),bool:new RegExp("^(?:"+_+")$","i"),needsContext:new RegExp("^"+$+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+$+"*((?:-\\d)?\\d*)"+$+"*\\)|)(?=[^-]|$)","i")},K=/HTML$/i,Z=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,ee=/^[^{]+\{\s*\[native \w/,te=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ne=/[+~]/,re=new RegExp("\\\\[\\da-fA-F]{1,6}"+$+"?|\\\\([^\\r\\n\\f])","g"),oe=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"�":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},ae=ye(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{P.apply(t=q.call(y.childNodes),y.childNodes),t[y.childNodes.length].nodeType}catch(e){P={apply:t.length?function(e,t){H.apply(e,q.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function se(t,e,n,r){var o,i,a,s,u,l,c=e&&e.ownerDocument,f=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==f&&9!==f&&11!==f)return n;if(!r&&(C(e),e=e||T,E)){if(11!==f&&(s=te.exec(t)))if(l=s[1]){if(9===f){if(!(i=e.getElementById(l)))return n;if(i.id===l)return n.push(i),n}else if(c&&(i=c.getElementById(l))&&v(e,i)&&i.id===l)return n.push(i),n}else{if(s[2])return P.apply(n,e.getElementsByTagName(t)),n;if((l=s[3])&&d.getElementsByClassName&&e.getElementsByClassName)return P.apply(n,e.getElementsByClassName(l)),n}if(d.qsa&&!I[t+" "]&&(!g||!g.test(t))&&(1!==f||"object"!==e.nodeName.toLowerCase())){if(l=t,c=e,1===f&&(Q.test(t)||X.test(t))){for((c=ne.test(t)&&me(e.parentNode)||e)===e&&d.scope||((a=e.getAttribute("id"))?a=a.replace(oe,ie):e.setAttribute("id",a=S)),o=(u=p(t)).length;o--;)u[o]=(a?"#"+a:":scope")+" "+ve(u[o]);l=u.join(",")}try{return P.apply(n,c.querySelectorAll(l)),n}catch(e){I(t,!0)}finally{a===S&&e.removeAttribute("id")}}}return m(t.replace(W,"$1"),e,n,r)}function ue(){var n=[];function r(e,t){return n.push(e+" ")>x.cacheLength&&delete r[n.shift()],r[e+" "]=t}return r}function le(e){return e[S]=!0,e}function ce(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){for(var n=e.split("|"),r=n.length;r--;)x.attrHandle[n[r]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function pe(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function he(a){return le(function(i){return i=+i,le(function(e,t){for(var n,r=a([],e.length,i),o=r.length;o--;)e[n=r[o]]&&(e[n]=!(t[n]=e[n]))})})}function me(e){return e&&void 0!==e.getElementsByTagName&&e}for(e in d=se.support={},o=se.isXML=function(e){var t=e.namespaceURI,e=(e.ownerDocument||e).documentElement;return!K.test(t||e&&e.nodeName||"HTML")},C=se.setDocument=function(e){var t,e=e?e.ownerDocument||e:y;return e!=T&&9===e.nodeType&&e.documentElement&&(a=(T=e).documentElement,E=!o(T),y!=T&&(t=T.defaultView)&&t.top!==t&&(t.addEventListener?t.addEventListener("unload",r,!1):t.attachEvent&&t.attachEvent("onunload",r)),d.scope=ce(function(e){return a.appendChild(e).appendChild(T.createElement("div")),void 0!==e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(T.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=ee.test(T.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!T.getElementsByName||!T.getElementsByName(S).length}),d.getById?(x.filter.ID=function(e){var t=e.replace(re,f);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if(void 0!==t.getElementById&&E){e=t.getElementById(e);return e?[e]:[]}}):(x.filter.ID=function(e){var t=e.replace(re,f);return function(e){e=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return e&&e.value===t}},x.find.ID=function(e,t){if(void 0!==t.getElementById&&E){var n,r,o,i=t.getElementById(e);if(i){if((n=i.getAttributeNode("id"))&&n.value===e)return[i];for(o=t.getElementsByName(e),r=0;i=o[r++];)if((n=i.getAttributeNode("id"))&&n.value===e)return[i]}return[]}}),x.find.TAG=d.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if("*"!==e)return i;for(;n=i[o++];)1===n.nodeType&&r.push(n);return r},x.find.CLASS=d.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],g=[],(d.qsa=ee.test(T.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&g.push("[*^$]="+$+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||g.push("\\["+$+"*(?:value|"+_+")"),e.querySelectorAll("[id~="+S+"-]").length||g.push("~="),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||g.push("\\["+$+"*name"+$+"*="+$+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||g.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||g.push(".#.+[+~]"),e.querySelectorAll("\\\f"),g.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=T.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&g.push("name"+$+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&g.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(d.matchesSelector=ee.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),g=g.length&&new RegExp(g.join("|")),s=s.length&&new RegExp(s.join("|")),t=ee.test(a.compareDocumentPosition),v=t||ee.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,t=t&&t.parentNode;return e===t||!(!t||1!==t.nodeType||!(n.contains?n.contains(t):e.compareDocumentPosition&&16&e.compareDocumentPosition(t)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},L=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==T||e.ownerDocument==y&&v(y,e)?-1:t==T||t.ownerDocument==y&&v(y,t)?1:u?M(u,e)-M(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],s=[t];if(!o||!i)return e==T?-1:t==T?1:o?-1:i?1:u?M(u,e)-M(u,t):0;if(o===i)return de(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?de(a[r],s[r]):a[r]==y?-1:s[r]==y?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(C(e),d.matchesSelector&&E&&!I[t+" "]&&(!s||!s.test(t))&&(!g||!g.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){I(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(re,f),e[3]=(e[3]||e[4]||e[5]||"").replace(re,f),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&Y.test(n)&&(t=p(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(re,f).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=A[e+" "];return t||(t=new RegExp("(^|"+$+")"+e+"("+$+"|$)"))&&A(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(t,n,r){return function(e){e=se.attr(e,t);return null==e?"!="===n:!n||(e+="","="===n?e===r:"!="===n?e!==r:"^="===n?r&&0===e.indexOf(r):"*="===n?r&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return b(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){if(!e)return this;if(n=n||L,"string"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):b(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this);if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:I.exec(e))||!r[1]&&t)return(!t||t.jquery?t||n:this.constructor(t)).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:T,!0)),N.test(r[1])&&E.isPlainObject(t))for(var r in t)b(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(e=T.getElementById(r[2]))&&(this[0]=e,this.length=1),this}).prototype=E.fn;var L=E(T),D=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function H(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,de=/^$|^module$|\/(?:java|ecma)script/i;f=T.createDocumentFragment().appendChild(T.createElement("div")),(p=T.createElement("input")).setAttribute("type","radio"),p.setAttribute("checked","checked"),p.setAttribute("name","t"),f.appendChild(p),y.checkClone=f.cloneNode(!0).cloneNode(!0).lastChild.checked,f.innerHTML="",y.noCloneChecked=!!f.cloneNode(!0).lastChild.defaultValue,f.innerHTML="",y.option=!!f.lastChild;var pe={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function he(e,t){var n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[];return void 0===t||t&&A(e,t)?E.merge([e],n):n}function me(e,t){for(var n=0,r=e.length;n",""]);var ge=/<|&#?\w+;/;function ve(e,t,n,r,o){for(var i,a,s,u,l,c=t.createDocumentFragment(),f=[],d=0,p=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Le(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function De(e,t){var n,r,o,i;if(1===t.nodeType){if(V.hasData(e)&&(i=V.get(e).events))for(o in V.remove(t,"handle events"),i)for(n=0,r=i[o].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",o=function(e){r.remove(),o=null,e&&t("error"===e.type?404:200,e.type)}),T.head.appendChild(r[0])},abort:function(){o&&o()}}});var Gt=[],Kt=/(=)\?(?=&|$)|\?\?/;E.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||E.expando+"_"+jt.guid++;return this[e]=!0,e}}),E.ajaxPrefilter("json jsonp",function(e,t,n){var r,o,i,a=!1!==e.jsonp&&(Kt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Kt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=b(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Kt,"$1"+r):!1!==e.jsonp&&(e.url+=(It.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return i||E.error(r+" was not called"),i[0]},e.dataTypes[0]="json",o=C[r],C[r]=function(){i=arguments},n.always(function(){void 0===o?E(C).removeProp(r):C[r]=o,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),i&&b(o)&&o(i[0]),i=o=void 0}),"script"}),y.createHTMLDocument=((f=T.implementation.createHTMLDocument("").body).innerHTML="
",2===f.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=T.implementation.createHTMLDocument("")).createElement("base")).href=T.location.href,t.head.appendChild(r)):t=T),r=!n&&[],(n=N.exec(e))?[t.createElement(n[1])]:(n=ve([e],t,r),r&&r.length&&E(r).remove(),E.merge([],n.childNodes)));var r},E.fn.load=function(e,t,n){var r,o,i,a=this,s=e.indexOf(" ");return-1").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,i||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,o,i,a,s=E.css(e,"position"),u=E(e),l={};"static"===s&&(e.style.position="relative"),i=u.offset(),r=E.css(e,"top"),a=E.css(e,"left"),a=("absolute"===s||"fixed"===s)&&-1<(r+a).indexOf("auto")?(o=(s=u.position()).top,s.left):(o=parseFloat(r)||0,parseFloat(a)||0),null!=(t=b(t)?t.call(e,n,E.extend({},i)):t).top&&(l.top=t.top-i.top+o),null!=t.left&&(l.left=t.left-i.left+a),"using"in t?t.using.call(e,l):("number"==typeof l.top&&(l.top+="px"),"number"==typeof l.left&&(l.left+="px"),u.css(l))}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n=this[0];return n?n.getClientRects().length?(e=n.getBoundingClientRect(),n=n.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],o={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{for(t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position");)e=e.parentNode;e&&e!==r&&1===e.nodeType&&((o=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),o.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-o.top-E.css(r,"marginTop",!0),left:t.left-o.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&"static"===E.css(e,"position");)e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,o){var i="pageYOffset"===o;E.fn[t]=function(e){return F(this,function(e,t,n){var r;return m(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n?r?r[o]:e[t]:void(r?r.scrollTo(i?r.pageXOffset:n,i?n:r.pageYOffset):e[t]=n)},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=Ge(y.pixelPosition,function(e,t){if(t)return t=Ve(e,n),We.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,i){E.fn[i]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),o=r||(!0===e||!0===t?"margin":"border");return F(this,function(e,t,n){var r;return m(e)?0===i.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,o):E.style(e,t,n,o)},s,n?e:void 0,n)}})}),E.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0x

',t.appendChild(n.childNodes[1])),e&&i.extend(o,e),this.each(function(){var e=['iframe[src*="player.vimeo.com"]','iframe[src*="youtube.com"]','iframe[src*="youtube-nocookie.com"]','iframe[src*="kickstarter.com"][src*="video.html"]',"object","embed"];o.customSelector&&e.push(o.customSelector);var r=".fitvidsignore";o.ignore&&(r=r+", "+o.ignore);e=i(this).find(e.join(","));(e=(e=e.not("object object")).not(r)).each(function(e){var t,n=i(this);0').parent(".fluid-width-video-wrapper").css("padding-top",100*t+"%"),n.removeAttr("height").removeAttr("width"))})})}}(window.jQuery||window.Zepto),$(function(){var n,r,e,o,t=$("nav.greedy-nav .greedy-nav__toggle"),i=$("nav.greedy-nav .visible-links"),a=$("nav.greedy-nav .hidden-links"),s=$("nav.greedy-nav"),u=$("nav.greedy-nav .site-logo"),l=$("nav.greedy-nav .site-logo img"),c=$("nav.greedy-nav .site-title"),f=$("nav.greedy-nav button.search__toggle");function d(){function t(e,t){r+=t,n+=1,o.push(r)}r=n=0,e=1e3,o=[],i.children().outerWidth(t),a.children().each(function(){var e;(e=(e=$(this)).clone()).css("visibility","hidden"),i.append(e),t(0,e.outerWidth()),e.remove()})}d();var p,h,m,g,v=$(window).width(),y=v<768?0:v<1024?1:v<1280?2:3;function b(){var e=(v=$(window).width())<768?0:v<1024?1:v<1280?2:3;e!==y&&d(),y=e,h=i.children().length,p=s.innerWidth()-(0!==u.length?u.outerWidth(!0):0)-c.outerWidth(!0)-(0!==f.length?f.outerWidth(!0):0)-(h!==o.length?t.outerWidth(!0):0),m=o[h-1],po[h]&&(a.children().first().appendTo(i),h+=1,b()),t.attr("count",n-h),h===n?t.addClass("hidden"):t.removeClass("hidden")}$(window).resize(function(){b()}),t.on("click",function(){a.toggleClass("hidden"),$(this).toggleClass("close"),clearTimeout(g)}),a.on("mouseleave",function(){g=setTimeout(function(){a.addClass("hidden")},e)}).on("mouseenter",function(){clearTimeout(g)}),0===l.length||l[0].complete||0!==l[0].naturalWidth?b():l.one("load error",b)}),function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?e(require("jquery")):e(window.jQuery||window.Zepto)}(function(l){function e(){}function c(e,t){h.ev.on("mfp"+e+x,t)}function f(e,t,n,r){var o=document.createElement("div");return o.className="mfp-"+e,n&&(o.innerHTML=n),r?t&&t.appendChild(o):(o=l(o),t&&o.appendTo(t)),o}function d(e,t){h.ev.triggerHandler("mfp"+e,t),h.st.callbacks&&(e=e.charAt(0).toLowerCase()+e.slice(1),h.st.callbacks[e]&&h.st.callbacks[e].apply(h,l.isArray(t)?t:[t]))}function p(e){return e===t&&h.currTemplate.closeBtn||(h.currTemplate.closeBtn=l(h.st.closeMarkup.replace("%title%",h.st.tClose)),t=e),h.currTemplate.closeBtn}function i(){l.magnificPopup.instance||((h=new e).init(),l.magnificPopup.instance=h)}var h,r,m,o,g,t,u="Close",v="BeforeClose",y="MarkupParse",b="Open",x=".mfp",w="mfp-ready",n="mfp-removing",a="mfp-prevent-close",s=!!window.jQuery,C=l(window);e.prototype={constructor:e,init:function(){var e=navigator.appVersion;h.isLowIE=h.isIE8=document.all&&!document.addEventListener,h.isAndroid=/android/gi.test(e),h.isIOS=/iphone|ipad|ipod/gi.test(e),h.supportsTransition=function(){var e=document.createElement("p").style,t=["ms","O","Moz","Webkit"];if(void 0!==e.transition)return!0;for(;t.length;)if(t.pop()+"Transition"in e)return!0;return!1}(),h.probablyMobile=h.isAndroid||h.isIOS||/(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent),m=l(document),h.popupsCache={}},open:function(e){if(!1===e.isObj){h.items=e.items.toArray(),h.index=0;for(var t,n=e.items,r=0;r(e||C.height())},_setFocus:function(){(h.st.focus?h.content.find(h.st.focus).eq(0):h.wrap).focus()},_onFocusIn:function(e){if(e.target!==h.wrap[0]&&!l.contains(h.wrap[0],e.target))return h._setFocus(),!1},_parseMarkup:function(o,e,t){var i;t.data&&(e=l.extend(t.data,e)),d(y,[o,e,t]),l.each(e,function(e,t){return void 0===t||!1===t||void(1<(i=e.split("_")).length?0<(n=o.find(x+"-"+i[0])).length&&("replaceWith"===(r=i[1])?n[0]!==t[0]&&n.replaceWith(t):"img"===r?n.is("img")?n.attr("src",t):n.replaceWith(l("").attr("src",t).attr("class",n.attr("class"))):n.attr(i[1],t)):o.find(x+"-"+e).html(t));var n,r})},_getScrollbarSize:function(){var e;return void 0===h.scrollbarSize&&((e=document.createElement("div")).style.cssText="width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;",document.body.appendChild(e),h.scrollbarSize=e.offsetWidth-e.clientWidth,document.body.removeChild(e)),h.scrollbarSize}},l.magnificPopup={instance:null,proto:e.prototype,modules:[],open:function(e,t){return i(),(e=e?l.extend(!0,{},e):{}).isObj=!0,e.index=t||0,this.instance.open(e)},close:function(){return l.magnificPopup.instance&&l.magnificPopup.instance.close()},registerModule:function(e,t){t.options&&(l.magnificPopup.defaults[e]=t.options),l.extend(this.proto,t.proto),this.modules.push(e)},defaults:{disableOn:0,key:null,midClick:!1,mainClass:"",preloader:!0,focus:"",closeOnContentClick:!1,closeOnBgClick:!0,closeBtnInside:!0,showCloseBtn:!0,enableEscapeKey:!0,modal:!1,alignTop:!1,removalDelay:0,prependTo:null,fixedContentPos:"auto",fixedBgPos:"auto",overflowY:"auto",closeMarkup:'',tClose:"Close (Esc)",tLoading:"Loading...",autoFocusLast:!0}},l.fn.magnificPopup=function(e){i();var t,n,r,o=l(this);return"string"==typeof e?"open"===e?(t=s?o.data("magnificPopup"):o[0].magnificPopup,n=parseInt(arguments[1],10)||0,r=t.items?t.items[n]:(r=o,(r=t.delegate?o.find(t.delegate):r).eq(n)),h._openClick({mfpEl:r},o,t)):h.isOpen&&h[e].apply(h,Array.prototype.slice.call(arguments,1)):(e=l.extend(!0,{},e),s?o.data("magnificPopup",e):o[0].magnificPopup=e,h.addGroup(o,e)),o};function T(){k&&(S.after(k.addClass(E)).detach(),k=null)}var E,S,k,A="inline";l.magnificPopup.registerModule(A,{options:{hiddenClass:"hide",markup:"",tNotFound:"Content not found"},proto:{initInline:function(){h.types.push(A),c(u+"."+A,function(){T()})},getInline:function(e,t){if(T(),e.src){var n,r=h.st.inline,o=l(e.src);return o.length?((n=o[0].parentNode)&&n.tagName&&(S||(E=r.hiddenClass,S=f(E),E="mfp-"+E),k=o.after(S).detach().removeClass(E)),h.updateStatus("ready")):(h.updateStatus("error",r.tNotFound),o=l("
")),e.inlineElement=o}return h.updateStatus("ready"),h._parseMarkup(t,{},e),t}}});function N(){I&&l(document.body).removeClass(I)}function j(){N(),h.req&&h.req.abort()}var I,L="ajax";l.magnificPopup.registerModule(L,{options:{settings:null,cursor:"mfp-ajax-cur",tError:'The content could not be loaded.'},proto:{initAjax:function(){h.types.push(L),I=h.st.ajax.cursor,c(u+"."+L,j),c("BeforeChange."+L,j)},getAjax:function(r){I&&l(document.body).addClass(I),h.updateStatus("loading");var e=l.extend({url:r.src,success:function(e,t,n){n={data:e,xhr:n};d("ParseAjax",n),h.appendContent(l(n.data),L),r.finished=!0,N(),h._setFocus(),setTimeout(function(){h.wrap.addClass(w)},16),h.updateStatus("ready"),d("AjaxContentAdded")},error:function(){N(),r.finished=r.loadError=!0,h.updateStatus("error",h.st.ajax.tError.replace("%url%",r.src))}},h.st.ajax.settings);return h.req=l.ajax(e),""}}});var D;l.magnificPopup.registerModule("image",{options:{markup:'
',cursor:"mfp-zoom-out-cur",titleSrc:"title",verticalFit:!0,tError:'The image could not be loaded.'},proto:{initImage:function(){var e=h.st.image,t=".image";h.types.push("image"),c(b+t,function(){"image"===h.currItem.type&&e.cursor&&l(document.body).addClass(e.cursor)}),c(u+t,function(){e.cursor&&l(document.body).removeClass(e.cursor),C.off("resize"+x)}),c("Resize"+t,h.resizeImage),h.isLowIE&&c("AfterChange",h.resizeImage)},resizeImage:function(){var e,t=h.currItem;t&&t.img&&h.st.image.verticalFit&&(e=0,h.isLowIE&&(e=parseInt(t.img.css("padding-top"),10)+parseInt(t.img.css("padding-bottom"),10)),t.img.css("max-height",h.wH-e))},_onImageHasSize:function(e){e.img&&(e.hasSize=!0,D&&clearInterval(D),e.isCheckingImgSize=!1,d("ImageHasSize",e),e.imgHidden&&(h.content&&h.content.removeClass("mfp-loading"),e.imgHidden=!1))},findImageSize:function(t){var n=0,r=t.img[0],o=function(e){D&&clearInterval(D),D=setInterval(function(){0
',srcAction:"iframe_src",patterns:{youtube:{index:"youtube.com",id:"v=",src:"//www.youtube.com/embed/%id%?autoplay=1"},vimeo:{index:"vimeo.com/",id:"/",src:"//player.vimeo.com/video/%id%?autoplay=1"},gmaps:{index:"//maps.google.",src:"%id%&output=embed"}}},proto:{initIframe:function(){h.types.push(P),c("BeforeChange",function(e,t,n){t!==n&&(t===P?H():n===P&&H(!0))}),c(u+"."+P,function(){H()})},getIframe:function(e,t){var n=e.src,r=h.st.iframe;l.each(r.patterns,function(){if(-1',preload:[0,2],navigateByImgClick:!0,arrows:!0,tPrev:"Previous (Left arrow key)",tNext:"Next (Right arrow key)",tCounter:"%curr% of %total%"},proto:{initGallery:function(){var i=h.st.gallery,e=".mfp-gallery";if(h.direction=!0,!i||!i.enabled)return!1;g+=" mfp-gallery",c(b+e,function(){i.navigateByImgClick&&h.wrap.on("click"+e,".mfp-img",function(){if(1=h.index,h.index=e,h.updateItemHTML()},preloadNearbyImages:function(){for(var e=h.st.gallery.preload,t=Math.min(e[0],h.items.length),n=Math.min(e[1],h.items.length),r=1;r<=(h.direction?n:t);r++)h._preloadItem(h.index+r);for(r=1;r<=(h.direction?t:n);r++)h._preloadItem(h.index-r)},_preloadItem:function(e){var t;e=q(e),h.items[e].preloaded||((t=h.items[e]).parsed||(t=h.parseEl(e)),d("LazyLoad",t),"image"===t.type&&(t.img=l('').on("load.mfploader",function(){t.hasSize=!0}).on("error.mfploader",function(){t.hasSize=!0,t.loadError=!0,d("LazyLoadError",t)}).attr("src",t.src)),t.preloaded=!0)}}});var _="retina";l.magnificPopup.registerModule(_,{options:{replaceSrc:function(e){return e.src.replace(/\.\w+$/,function(e){return"@2x"+e})},ratio:1},proto:{initRetina:function(){var n,r;1t.durationMax?t.durationMax:t.durationMin&&e=u)return b.cancelScroll(!0),e=t,n=g,0===(t=r)&&document.body.focus(),n||(t.focus(),document.activeElement!==t&&(t.setAttribute("tabindex","-1"),t.focus(),t.style.outline="none"),x.scrollTo(0,e)),E("scrollStop",m,r,o),!(y=f=null)},h=function(e){var t,n,r;l+=e-(f=f||e),d=i+s*(n=d=1<(d=0===c?0:l/c)?1:d,"easeInQuad"===(t=m).easing&&(r=n*n),"easeOutQuad"===t.easing&&(r=n*(2-n)),"easeInOutQuad"===t.easing&&(r=n<.5?2*n*n:(4-2*n)*n-1),"easeInCubic"===t.easing&&(r=n*n*n),"easeOutCubic"===t.easing&&(r=--n*n*n+1),"easeInOutCubic"===t.easing&&(r=n<.5?4*n*n*n:(n-1)*(2*n-2)*(2*n-2)+1),"easeInQuart"===t.easing&&(r=n*n*n*n),"easeOutQuart"===t.easing&&(r=1- --n*n*n*n),"easeInOutQuart"===t.easing&&(r=n<.5?8*n*n*n*n:1-8*--n*n*n*n),"easeInQuint"===t.easing&&(r=n*n*n*n*n),"easeOutQuint"===t.easing&&(r=1+--n*n*n*n*n),"easeInOutQuint"===t.easing&&(r=n<.5?16*n*n*n*n*n:1+16*--n*n*n*n*n),(r=t.customEasing?t.customEasing(n):r)||n),x.scrollTo(0,Math.floor(d)),p(d,a)||(y=x.requestAnimationFrame(h),f=e)},0===x.pageYOffset&&x.scrollTo(0,0),t=r,e=m,g||history.pushState&&e.updateURL&&history.pushState({smoothScroll:JSON.stringify(e),anchor:t.id},document.title,t===document.documentElement?"#top":"#"+t.id),"matchMedia"in x&&x.matchMedia("(prefers-reduced-motion)").matches?x.scrollTo(0,Math.floor(a)):(E("scrollStart",m,r,o),b.cancelScroll(!0),x.requestAnimationFrame(h)))};function t(e){if(!e.defaultPrevented&&!(0!==e.button||e.metaKey||e.ctrlKey||e.shiftKey)&&"closest"in e.target&&(o=e.target.closest(r))&&"a"===o.tagName.toLowerCase()&&!e.target.closest(v.ignore)&&o.hostname===x.location.hostname&&o.pathname===x.location.pathname&&/#/.test(o.href)){var t,n;try{n=a(decodeURIComponent(o.hash))}catch(e){n=a(o.hash)}if("#"===n){if(!v.topOnEmptyHash)return;t=document.documentElement}else t=document.querySelector(n);(t=t||"#top"!==n?t:document.documentElement)&&(e.preventDefault(),n=v,history.replaceState&&n.updateURL&&!history.state&&(e=(e=x.location.hash)||"",history.replaceState({smoothScroll:JSON.stringify(n),anchor:e||x.pageYOffset},document.title,e||x.location.href)),b.animateScroll(t,o))}}function i(e){var t;null!==history.state&&history.state.smoothScroll&&history.state.smoothScroll===JSON.stringify(v)&&("string"==typeof(t=history.state.anchor)&&t&&!(t=document.querySelector(a(history.state.anchor)))||b.animateScroll(t,null,{updateURL:!1}))}b.destroy=function(){v&&(document.removeEventListener("click",t,!1),x.removeEventListener("popstate",i,!1),b.cancelScroll(),y=n=o=v=null)};return function(){if(!("querySelector"in document&&"addEventListener"in x&&"requestAnimationFrame"in x&&"closest"in x.Element.prototype))throw"Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.";b.destroy(),v=w(S,e||{}),n=v.header?document.querySelector(v.header):null,document.addEventListener("click",t,!1),v.updateURL&&v.popstate&&x.addEventListener("popstate",i,!1)}(),b}}),function(e,t){"function"==typeof define&&define.amd?define([],function(){return t(e)}):"object"==typeof exports?module.exports=t(e):e.Gumshoe=t(e)}("undefined"!=typeof global?global:"undefined"!=typeof window?window:this,function(c){"use strict";function f(e,t,n){n.settings.events&&(n=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:n}),t.dispatchEvent(n))}function n(e){var t=0;if(e.offsetParent)for(;e;)t+=e.offsetTop,e=e.offsetParent;return 0<=t?t:0}function d(e){e&&e.sort(function(e,t){return n(e.content)=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)}function p(e,t){var n,r,o=e[e.length-1];if(n=o,r=t,!(!s()||!a(n.content,r,!0)))return o;for(var i=e.length-1;0<=i;i--)if(a(e[i].content,t))return e[i]}function h(e,t){var n;!e||(n=e.nav.closest("li"))&&(n.classList.remove(t.navClass),e.content.classList.remove(t.contentClass),r(n,t),f("gumshoeDeactivate",n,{link:e.nav,content:e.content,settings:t}))}var m={navClass:"active",contentClass:"active",nested:!1,nestedClass:"active",offset:0,reflow:!1,events:!0},r=function(e,t){!t.nested||(e=e.parentNode.closest("li"))&&(e.classList.remove(t.nestedClass),r(e,t))},g=function(e,t){!t.nested||(e=e.parentNode.closest("li"))&&(e.classList.add(t.nestedClass),g(e,t))};return function(e,t){var n,o,i,r,a,s={setup:function(){n=document.querySelectorAll(e),o=[],Array.prototype.forEach.call(n,function(e){var t=document.getElementById(decodeURIComponent(e.hash.substr(1)));t&&o.push({nav:e,content:t})}),d(o)}};s.detect=function(){var e,t,n,r=p(o,a);r?i&&r.content===i.content||(h(i,a),t=a,!(e=r)||(n=e.nav.closest("li"))&&(n.classList.add(t.navClass),e.content.classList.add(t.contentClass),g(n,t),f("gumshoeActivate",n,{link:e.nav,content:e.content,settings:t})),i=r):i&&(h(i,a),i=null)};function u(e){r&&c.cancelAnimationFrame(r),r=c.requestAnimationFrame(s.detect)}function l(e){r&&c.cancelAnimationFrame(r),r=c.requestAnimationFrame(function(){d(o),s.detect()})}s.destroy=function(){i&&h(i,a),c.removeEventListener("scroll",u,!1),a.reflow&&c.removeEventListener("resize",l,!1),a=r=i=n=o=null};return a=function(){var n={};return Array.prototype.forEach.call(arguments,function(e){for(var t in e){if(!e.hasOwnProperty(t))return;n[t]=e[t]}}),n}(m,t||{}),s.setup(),s.detect(),c.addEventListener("scroll",u,!1),a.reflow&&c.addEventListener("resize",l,!1),s}}),$(document).ready(function(){$("#main").fitVids();function e(){(0===$(".author__urls-wrapper button").length?1024<$(window).width():!$(".author__urls-wrapper button").is(":visible"))?$(".sidebar").addClass("sticky"):$(".sidebar").removeClass("sticky")}e(),$(window).resize(function(){e()}),$(".author__urls-wrapper button").on("click",function(){$(".author__urls").toggleClass("is--visible"),$(".author__urls-wrapper button").toggleClass("open")}),$(document).keyup(function(e){27===e.keyCode&&$(".initial-content").hasClass("is--hidden")&&($(".search-content").toggleClass("is--visible"),$(".initial-content").toggleClass("is--hidden"))}),$(".search__toggle").on("click",function(){$(".search-content").toggleClass("is--visible"),$(".initial-content").toggleClass("is--hidden"),setTimeout(function(){$(".search-content input").focus()},400)});new SmoothScroll('a[href*="#"]',{offset:20,speed:400,speedAsDuration:!0,durationMax:500});0<$("nav.toc").length&&new Gumshoe("nav.toc a",{navClass:"active",contentClass:"active",nested:!1,nestedClass:"active",offset:20,reflow:!0,events:!0}),$("a[href$='.jpg'],a[href$='.jpeg'],a[href$='.JPG'],a[href$='.png'],a[href$='.gif'],a[href$='.webp']").addClass("image-popup"),$(".image-popup").magnificPopup({type:"image",tLoading:"Loading image #%curr%...",gallery:{enabled:!0,navigateByImgClick:!0,preload:[0,1]},image:{tError:'Image #%curr% could not be loaded.'},removalDelay:500,mainClass:"mfp-zoom-in",callbacks:{beforeOpen:function(){this.st.image.markup=this.st.image.markup.replace("mfp-figure","mfp-figure mfp-with-anim")}},closeOnContentClick:!0,midClick:!0}),$(".page__content").find("h1, h2, h3, h4, h5, h6").each(function(){var e,t=$(this).attr("id");t&&((e=document.createElement("a")).className="header-link",e.href="#"+t,e.innerHTML='Permalink',e.title="Permalink",$(this).append(e))})}); \ No newline at end of file diff --git a/assets/js/plugins/gumshoe.js b/assets/js/plugins/gumshoe.js new file mode 100644 index 0000000..713b6eb --- /dev/null +++ b/assets/js/plugins/gumshoe.js @@ -0,0 +1,484 @@ +/*! + * gumshoejs v5.1.1 + * A simple, framework-agnostic scrollspy script. + * (c) 2019 Chris Ferdinandi + * MIT License + * http://github.com/cferdinandi/gumshoe + */ + +(function (root, factory) { + if ( typeof define === 'function' && define.amd ) { + define([], (function () { + return factory(root); + })); + } else if ( typeof exports === 'object' ) { + module.exports = factory(root); + } else { + root.Gumshoe = factory(root); + } +})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this, (function (window) { + + 'use strict'; + + // + // Defaults + // + + var defaults = { + + // Active classes + navClass: 'active', + contentClass: 'active', + + // Nested navigation + nested: false, + nestedClass: 'active', + + // Offset & reflow + offset: 0, + reflow: false, + + // Event support + events: true + + }; + + + // + // Methods + // + + /** + * Merge two or more objects together. + * @param {Object} objects The objects to merge together + * @returns {Object} Merged values of defaults and options + */ + var extend = function () { + var merged = {}; + Array.prototype.forEach.call(arguments, (function (obj) { + for (var key in obj) { + if (!obj.hasOwnProperty(key)) return; + merged[key] = obj[key]; + } + })); + return merged; + }; + + /** + * Emit a custom event + * @param {String} type The event type + * @param {Node} elem The element to attach the event to + * @param {Object} detail Any details to pass along with the event + */ + var emitEvent = function (type, elem, detail) { + + // Make sure events are enabled + if (!detail.settings.events) return; + + // Create a new event + var event = new CustomEvent(type, { + bubbles: true, + cancelable: true, + detail: detail + }); + + // Dispatch the event + elem.dispatchEvent(event); + + }; + + /** + * Get an element's distance from the top of the Document. + * @param {Node} elem The element + * @return {Number} Distance from the top in pixels + */ + var getOffsetTop = function (elem) { + var location = 0; + if (elem.offsetParent) { + while (elem) { + location += elem.offsetTop; + elem = elem.offsetParent; + } + } + return location >= 0 ? location : 0; + }; + + /** + * Sort content from first to last in the DOM + * @param {Array} contents The content areas + */ + var sortContents = function (contents) { + if(contents) { + contents.sort((function (item1, item2) { + var offset1 = getOffsetTop(item1.content); + var offset2 = getOffsetTop(item2.content); + if (offset1 < offset2) return -1; + return 1; + })); + } + }; + + /** + * Get the offset to use for calculating position + * @param {Object} settings The settings for this instantiation + * @return {Float} The number of pixels to offset the calculations + */ + var getOffset = function (settings) { + + // if the offset is a function run it + if (typeof settings.offset === 'function') { + return parseFloat(settings.offset()); + } + + // Otherwise, return it as-is + return parseFloat(settings.offset); + + }; + + /** + * Get the document element's height + * @private + * @returns {Number} + */ + var getDocumentHeight = function () { + return Math.max( + document.body.scrollHeight, document.documentElement.scrollHeight, + document.body.offsetHeight, document.documentElement.offsetHeight, + document.body.clientHeight, document.documentElement.clientHeight + ); + }; + + /** + * Determine if an element is in view + * @param {Node} elem The element + * @param {Object} settings The settings for this instantiation + * @param {Boolean} bottom If true, check if element is above bottom of viewport instead + * @return {Boolean} Returns true if element is in the viewport + */ + var isInView = function (elem, settings, bottom) { + var bounds = elem.getBoundingClientRect(); + var offset = getOffset(settings); + if (bottom) { + return parseInt(bounds.bottom, 10) < (window.innerHeight || document.documentElement.clientHeight); + } + return parseInt(bounds.top, 10) <= offset; + }; + + /** + * Check if at the bottom of the viewport + * @return {Boolean} If true, page is at the bottom of the viewport + */ + var isAtBottom = function () { + if (window.innerHeight + window.pageYOffset >= getDocumentHeight()) return true; + return false; + }; + + /** + * Check if the last item should be used (even if not at the top of the page) + * @param {Object} item The last item + * @param {Object} settings The settings for this instantiation + * @return {Boolean} If true, use the last item + */ + var useLastItem = function (item, settings) { + if (isAtBottom() && isInView(item.content, settings, true)) return true; + return false; + }; + + /** + * Get the active content + * @param {Array} contents The content areas + * @param {Object} settings The settings for this instantiation + * @return {Object} The content area and matching navigation link + */ + var getActive = function (contents, settings) { + var last = contents[contents.length-1]; + if (useLastItem(last, settings)) return last; + for (var i = contents.length - 1; i >= 0; i--) { + if (isInView(contents[i].content, settings)) return contents[i]; + } + }; + + /** + * Deactivate parent navs in a nested navigation + * @param {Node} nav The starting navigation element + * @param {Object} settings The settings for this instantiation + */ + var deactivateNested = function (nav, settings) { + + // If nesting isn't activated, bail + if (!settings.nested) return; + + // Get the parent navigation + var li = nav.parentNode.closest('li'); + if (!li) return; + + // Remove the active class + li.classList.remove(settings.nestedClass); + + // Apply recursively to any parent navigation elements + deactivateNested(li, settings); + + }; + + /** + * Deactivate a nav and content area + * @param {Object} items The nav item and content to deactivate + * @param {Object} settings The settings for this instantiation + */ + var deactivate = function (items, settings) { + + // Make sure their are items to deactivate + if (!items) return; + + // Get the parent list item + var li = items.nav.closest('li'); + if (!li) return; + + // Remove the active class from the nav and content + li.classList.remove(settings.navClass); + items.content.classList.remove(settings.contentClass); + + // Deactivate any parent navs in a nested navigation + deactivateNested(li, settings); + + // Emit a custom event + emitEvent('gumshoeDeactivate', li, { + link: items.nav, + content: items.content, + settings: settings + }); + + }; + + + /** + * Activate parent navs in a nested navigation + * @param {Node} nav The starting navigation element + * @param {Object} settings The settings for this instantiation + */ + var activateNested = function (nav, settings) { + + // If nesting isn't activated, bail + if (!settings.nested) return; + + // Get the parent navigation + var li = nav.parentNode.closest('li'); + if (!li) return; + + // Add the active class + li.classList.add(settings.nestedClass); + + // Apply recursively to any parent navigation elements + activateNested(li, settings); + + }; + + /** + * Activate a nav and content area + * @param {Object} items The nav item and content to activate + * @param {Object} settings The settings for this instantiation + */ + var activate = function (items, settings) { + + // Make sure their are items to activate + if (!items) return; + + // Get the parent list item + var li = items.nav.closest('li'); + if (!li) return; + + // Add the active class to the nav and content + li.classList.add(settings.navClass); + items.content.classList.add(settings.contentClass); + + // Activate any parent navs in a nested navigation + activateNested(li, settings); + + // Emit a custom event + emitEvent('gumshoeActivate', li, { + link: items.nav, + content: items.content, + settings: settings + }); + + }; + + /** + * Create the Constructor object + * @param {String} selector The selector to use for navigation items + * @param {Object} options User options and settings + */ + var Constructor = function (selector, options) { + + // + // Variables + // + + var publicAPIs = {}; + var navItems, contents, current, timeout, settings; + + + // + // Methods + // + + /** + * Set variables from DOM elements + */ + publicAPIs.setup = function () { + + // Get all nav items + navItems = document.querySelectorAll(selector); + + // Create contents array + contents = []; + + // Loop through each item, get it's matching content, and push to the array + Array.prototype.forEach.call(navItems, (function (item) { + + // Get the content for the nav item + var content = document.getElementById(decodeURIComponent(item.hash.substr(1))); + if (!content) return; + + // Push to the contents array + contents.push({ + nav: item, + content: content + }); + + })); + + // Sort contents by the order they appear in the DOM + sortContents(contents); + + }; + + /** + * Detect which content is currently active + */ + publicAPIs.detect = function () { + + // Get the active content + var active = getActive(contents, settings); + + // if there's no active content, deactivate and bail + if (!active) { + if (current) { + deactivate(current, settings); + current = null; + } + return; + } + + // If the active content is the one currently active, do nothing + if (current && active.content === current.content) return; + + // Deactivate the current content and activate the new content + deactivate(current, settings); + activate(active, settings); + + // Update the currently active content + current = active; + + }; + + /** + * Detect the active content on scroll + * Debounced for performance + */ + var scrollHandler = function (event) { + + // If there's a timer, cancel it + if (timeout) { + window.cancelAnimationFrame(timeout); + } + + // Setup debounce callback + timeout = window.requestAnimationFrame(publicAPIs.detect); + + }; + + /** + * Update content sorting on resize + * Debounced for performance + */ + var resizeHandler = function (event) { + + // If there's a timer, cancel it + if (timeout) { + window.cancelAnimationFrame(timeout); + } + + // Setup debounce callback + timeout = window.requestAnimationFrame((function () { + sortContents(contents); + publicAPIs.detect(); + })); + + }; + + /** + * Destroy the current instantiation + */ + publicAPIs.destroy = function () { + + // Undo DOM changes + if (current) { + deactivate(current, settings); + } + + // Remove event listeners + window.removeEventListener('scroll', scrollHandler, false); + if (settings.reflow) { + window.removeEventListener('resize', resizeHandler, false); + } + + // Reset variables + contents = null; + navItems = null; + current = null; + timeout = null; + settings = null; + + }; + + /** + * Initialize the current instantiation + */ + var init = function () { + + // Merge user options into defaults + settings = extend(defaults, options || {}); + + // Setup variables based on the current DOM + publicAPIs.setup(); + + // Find the currently active content + publicAPIs.detect(); + + // Setup event listeners + window.addEventListener('scroll', scrollHandler, false); + if (settings.reflow) { + window.addEventListener('resize', resizeHandler, false); + } + + }; + + + // + // Initialize and return the public APIs + // + + init(); + return publicAPIs; + + }; + + + // + // Return the Constructor + // + + return Constructor; + +})); \ No newline at end of file diff --git a/assets/js/plugins/jquery.ba-throttle-debounce.js b/assets/js/plugins/jquery.ba-throttle-debounce.js new file mode 100644 index 0000000..fa30bdf --- /dev/null +++ b/assets/js/plugins/jquery.ba-throttle-debounce.js @@ -0,0 +1,252 @@ +/*! + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ + +// Script: jQuery throttle / debounce: Sometimes, less is more! +// +// *Version: 1.1, Last updated: 3/7/2010* +// +// Project Home - http://benalman.com/projects/jquery-throttle-debounce-plugin/ +// GitHub - http://github.com/cowboy/jquery-throttle-debounce/ +// Source - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.js +// (Minified) - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.min.js (0.7kb) +// +// About: License +// +// Copyright (c) 2010 "Cowboy" Ben Alman, +// Dual licensed under the MIT and GPL licenses. +// http://benalman.com/about/license/ +// +// About: Examples +// +// These working examples, complete with fully commented code, illustrate a few +// ways in which this plugin can be used. +// +// Throttle - http://benalman.com/code/projects/jquery-throttle-debounce/examples/throttle/ +// Debounce - http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/ +// +// About: Support and Testing +// +// Information about what version or versions of jQuery this plugin has been +// tested with, what browsers it has been tested in, and where the unit tests +// reside (so you can test it yourself). +// +// jQuery Versions - none, 1.3.2, 1.4.2 +// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome 4-5, Opera 9.6-10.1. +// Unit Tests - http://benalman.com/code/projects/jquery-throttle-debounce/unit/ +// +// About: Release History +// +// 1.1 - (3/7/2010) Fixed a bug in where trailing callbacks +// executed later than they should. Reworked a fair amount of internal +// logic as well. +// 1.0 - (3/6/2010) Initial release as a stand-alone project. Migrated over +// from jquery-misc repo v0.4 to jquery-throttle repo v1.0, added the +// no_trailing throttle parameter and debounce functionality. +// +// Topic: Note for non-jQuery users +// +// jQuery isn't actually required for this plugin, because nothing internal +// uses any jQuery methods or properties. jQuery is just used as a namespace +// under which these methods can exist. +// +// Since jQuery isn't actually required for this plugin, if jQuery doesn't exist +// when this plugin is loaded, the method described below will be created in +// the `Cowboy` namespace. Usage will be exactly the same, but instead of +// $.method() or jQuery.method(), you'll need to use Cowboy.method(). + +(function(window,undefined){ + '$:nomunge'; // Used by YUI compressor. + + // Since jQuery really isn't required for this plugin, use `jQuery` as the + // namespace only if it already exists, otherwise use the `Cowboy` namespace, + // creating it if necessary. + var $ = window.jQuery || window.Cowboy || ( window.Cowboy = {} ), + + // Internal method reference. + jq_throttle; + + // Method: jQuery.throttle + // + // Throttle execution of a function. Especially useful for rate limiting + // execution of handlers on events like resize and scroll. If you want to + // rate-limit execution of a function to a single time, see the + // method. + // + // In this visualization, | is a throttled-function call and X is the actual + // callback execution: + // + // > Throttled with `no_trailing` specified as false or unspecified: + // > ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + // > X X X X X X X X X X X X + // > + // > Throttled with `no_trailing` specified as true: + // > ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + // > X X X X X X X X X X + // + // Usage: + // + // > var throttled = jQuery.throttle( delay, [ no_trailing, ] callback ); + // > + // > jQuery('selector').bind( 'someevent', throttled ); + // > jQuery('selector').unbind( 'someevent', throttled ); + // + // This also works in jQuery 1.4+: + // + // > jQuery('selector').bind( 'someevent', jQuery.throttle( delay, [ no_trailing, ] callback ) ); + // > jQuery('selector').unbind( 'someevent', callback ); + // + // Arguments: + // + // delay - (Number) A zero-or-greater delay in milliseconds. For event + // callbacks, values around 100 or 250 (or even higher) are most useful. + // no_trailing - (Boolean) Optional, defaults to false. If no_trailing is + // true, callback will only execute every `delay` milliseconds while the + // throttled-function is being called. If no_trailing is false or + // unspecified, callback will be executed one final time after the last + // throttled-function call. (After the throttled-function has not been + // called for `delay` milliseconds, the internal counter is reset) + // callback - (Function) A function to be executed after delay milliseconds. + // The `this` context and all arguments are passed through, as-is, to + // `callback` when the throttled-function is executed. + // + // Returns: + // + // (Function) A new, throttled, function. + + $.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) { + // After wrapper has stopped being called, this timeout ensures that + // `callback` is executed at the proper times in `throttle` and `end` + // debounce modes. + var timeout_id, + + // Keep track of the last time `callback` was executed. + last_exec = 0; + + // `no_trailing` defaults to falsy. + if ( typeof no_trailing !== 'boolean' ) { + debounce_mode = callback; + callback = no_trailing; + no_trailing = undefined; + } + + // The `wrapper` function encapsulates all of the throttling / debouncing + // functionality and when executed will limit the rate at which `callback` + // is executed. + function wrapper() { + var that = this, + elapsed = +new Date() - last_exec, + args = arguments; + + // Execute `callback` and update the `last_exec` timestamp. + function exec() { + last_exec = +new Date(); + callback.apply( that, args ); + }; + + // If `debounce_mode` is true (at_begin) this is used to clear the flag + // to allow future `callback` executions. + function clear() { + timeout_id = undefined; + }; + + if ( debounce_mode && !timeout_id ) { + // Since `wrapper` is being called for the first time and + // `debounce_mode` is true (at_begin), execute `callback`. + exec(); + } + + // Clear any existing timeout. + timeout_id && clearTimeout( timeout_id ); + + if ( debounce_mode === undefined && elapsed > delay ) { + // In throttle mode, if `delay` time has been exceeded, execute + // `callback`. + exec(); + + } else if ( no_trailing !== true ) { + // In trailing throttle mode, since `delay` time has not been + // exceeded, schedule `callback` to execute `delay` ms after most + // recent execution. + // + // If `debounce_mode` is true (at_begin), schedule `clear` to execute + // after `delay` ms. + // + // If `debounce_mode` is false (at end), schedule `callback` to + // execute after `delay` ms. + timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay ); + } + }; + + // Set the guid of `wrapper` function to the same of original callback, so + // it can be removed in jQuery 1.4+ .unbind or .die by using the original + // callback as a reference. + if ( $.guid ) { + wrapper.guid = callback.guid = callback.guid || $.guid++; + } + + // Return the wrapper function. + return wrapper; + }; + + // Method: jQuery.debounce + // + // Debounce execution of a function. Debouncing, unlike throttling, + // guarantees that a function is only executed a single time, either at the + // very beginning of a series of calls, or at the very end. If you want to + // simply rate-limit execution of a function, see the + // method. + // + // In this visualization, | is a debounced-function call and X is the actual + // callback execution: + // + // > Debounced with `at_begin` specified as false or unspecified: + // > ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + // > X X + // > + // > Debounced with `at_begin` specified as true: + // > ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + // > X X + // + // Usage: + // + // > var debounced = jQuery.debounce( delay, [ at_begin, ] callback ); + // > + // > jQuery('selector').bind( 'someevent', debounced ); + // > jQuery('selector').unbind( 'someevent', debounced ); + // + // This also works in jQuery 1.4+: + // + // > jQuery('selector').bind( 'someevent', jQuery.debounce( delay, [ at_begin, ] callback ) ); + // > jQuery('selector').unbind( 'someevent', callback ); + // + // Arguments: + // + // delay - (Number) A zero-or-greater delay in milliseconds. For event + // callbacks, values around 100 or 250 (or even higher) are most useful. + // at_begin - (Boolean) Optional, defaults to false. If at_begin is false or + // unspecified, callback will only be executed `delay` milliseconds after + // the last debounced-function call. If at_begin is true, callback will be + // executed only at the first debounced-function call. (After the + // throttled-function has not been called for `delay` milliseconds, the + // internal counter is reset) + // callback - (Function) A function to be executed after delay milliseconds. + // The `this` context and all arguments are passed through, as-is, to + // `callback` when the debounced-function is executed. + // + // Returns: + // + // (Function) A new, debounced, function. + + $.debounce = function( delay, at_begin, callback ) { + return callback === undefined + ? jq_throttle( delay, at_begin, false ) + : jq_throttle( delay, callback, at_begin !== false ); + }; + +})(this); diff --git a/assets/js/plugins/jquery.fitvids.js b/assets/js/plugins/jquery.fitvids.js new file mode 100644 index 0000000..5c2f85c --- /dev/null +++ b/assets/js/plugins/jquery.fitvids.js @@ -0,0 +1,82 @@ +/*jshint browser:true */ +/*! +* FitVids 1.1 +* +* Copyright 2013, Chris Coyier - http://css-tricks.com + Dave Rupert - http://daverupert.com +* Credit to Thierry Koblentz - http://www.alistapart.com/articles/creating-intrinsic-ratios-for-video/ +* Released under the WTFPL license - http://sam.zoy.org/wtfpl/ +* +*/ + +;(function( $ ){ + + 'use strict'; + + $.fn.fitVids = function( options ) { + var settings = { + customSelector: null, + ignore: null + }; + + if(!document.getElementById('fit-vids-style')) { + // appendStyles: https://github.com/toddmotto/fluidvids/blob/master/dist/fluidvids.js + var head = document.head || document.getElementsByTagName('head')[0]; + var css = '.fluid-width-video-wrapper{width:100%;position:relative;padding:0;}.fluid-width-video-wrapper iframe,.fluid-width-video-wrapper object,.fluid-width-video-wrapper embed {position:absolute;top:0;left:0;width:100%;height:100%;}'; + var div = document.createElement("div"); + div.innerHTML = '

x

'; + head.appendChild(div.childNodes[1]); + } + + if ( options ) { + $.extend( settings, options ); + } + + return this.each(function(){ + var selectors = [ + 'iframe[src*="player.vimeo.com"]', + 'iframe[src*="youtube.com"]', + 'iframe[src*="youtube-nocookie.com"]', + 'iframe[src*="kickstarter.com"][src*="video.html"]', + 'object', + 'embed' + ]; + + if (settings.customSelector) { + selectors.push(settings.customSelector); + } + + var ignoreList = '.fitvidsignore'; + + if(settings.ignore) { + ignoreList = ignoreList + ', ' + settings.ignore; + } + + var $allVideos = $(this).find(selectors.join(',')); + $allVideos = $allVideos.not('object object'); // SwfObj conflict patch + $allVideos = $allVideos.not(ignoreList); // Disable FitVids on this video. + + $allVideos.each(function(count){ + var $this = $(this); + if($this.parents(ignoreList).length > 0) { + return; // Disable FitVids on this video. + } + if (this.tagName.toLowerCase() === 'embed' && $this.parent('object').length || $this.parent('.fluid-width-video-wrapper').length) { return; } + if ((!$this.css('height') && !$this.css('width')) && (isNaN($this.attr('height')) || isNaN($this.attr('width')))) + { + $this.attr('height', 9); + $this.attr('width', 16); + } + var height = ( this.tagName.toLowerCase() === 'object' || ($this.attr('height') && !isNaN(parseInt($this.attr('height'), 10))) ) ? parseInt($this.attr('height'), 10) : $this.height(), + width = !isNaN(parseInt($this.attr('width'), 10)) ? parseInt($this.attr('width'), 10) : $this.width(), + aspectRatio = height / width; + if(!$this.attr('id')){ + var videoID = 'fitvid' + count; + $this.attr('id', videoID); + } + $this.wrap('
').parent('.fluid-width-video-wrapper').css('padding-top', (aspectRatio * 100)+'%'); + $this.removeAttr('height').removeAttr('width'); + }); + }); + }; +// Works with either jQuery or Zepto +})( window.jQuery || window.Zepto ); \ No newline at end of file diff --git a/assets/js/plugins/jquery.greedy-navigation.js b/assets/js/plugins/jquery.greedy-navigation.js new file mode 100644 index 0000000..d8f3237 --- /dev/null +++ b/assets/js/plugins/jquery.greedy-navigation.js @@ -0,0 +1,127 @@ +/* +GreedyNav.js - http://lukejacksonn.com/actuate +Licensed under the MIT license - http://opensource.org/licenses/MIT +Copyright (c) 2015 Luke Jackson +*/ + +$(function() { + + var $btn = $("nav.greedy-nav .greedy-nav__toggle"); + var $vlinks = $("nav.greedy-nav .visible-links"); + var $hlinks = $("nav.greedy-nav .hidden-links"); + var $nav = $("nav.greedy-nav"); + var $logo = $('nav.greedy-nav .site-logo'); + var $logoImg = $('nav.greedy-nav .site-logo img'); + var $title = $("nav.greedy-nav .site-title"); + var $search = $('nav.greedy-nav button.search__toggle'); + + var numOfItems, totalSpace, closingTime, breakWidths; + + // This function measures both hidden and visible links and sets the navbar breakpoints + // This is called the first time the script runs and everytime the "check()" function detects a change of window width that reached a different CSS width breakpoint, which affects the size of navbar Items + // Please note that "CSS width breakpoints" (which are only 4) !== "navbar breakpoints" (which are as many as the number of items on the navbar) + function measureLinks(){ + numOfItems = 0; + totalSpace = 0; + closingTime = 1000; + breakWidths = []; + + // Adds the width of a navItem in order to create breakpoints for the navbar + function addWidth(i, w) { + totalSpace += w; + numOfItems += 1; + breakWidths.push(totalSpace); + } + + // Measures the width of hidden links by making a temporary clone of them and positioning under visible links + function hiddenWidth(obj){ + var clone = obj.clone(); + clone.css("visibility","hidden"); + $vlinks.append(clone); + addWidth(0, clone.outerWidth()); + clone.remove(); + } + // Measure both visible and hidden links widths + $vlinks.children().outerWidth(addWidth); + $hlinks.children().each(function(){hiddenWidth($(this))}); + } + // Get initial state + measureLinks(); + + var winWidth = $( window ).width(); + // Set the last measured CSS width breakpoint: 0: <768px, 1: <1024px, 2: < 1280px, 3: >= 1280px. + var lastBreakpoint = winWidth < 768 ? 0 : winWidth < 1024 ? 1 : winWidth < 1280 ? 2 : 3; + + var availableSpace, numOfVisibleItems, requiredSpace, timer; + + function check() { + + winWidth = $( window ).width(); + // Set the current CSS width breakpoint: 0: <768px, 1: <1024px, 2: < 1280px, 3: >= 1280px. + var curBreakpoint = winWidth < 768 ? 0 : winWidth < 1024 ? 1 : winWidth < 1280 ? 2 : 3; + // If current breakpoint is different from last measured breakpoint, measureLinks again + if(curBreakpoint !== lastBreakpoint) measureLinks(); + // Set the last measured CSS width breakpoint with the current breakpoint + lastBreakpoint = curBreakpoint; + + // Get instant state + numOfVisibleItems = $vlinks.children().length; + // Decrease the width of visible elements from the nav innerWidth to find out the available space for navItems + availableSpace = /* nav */ $nav.innerWidth() + - /* logo */ ($logo.length !== 0 ? $logo.outerWidth(true) : 0) + - /* title */ $title.outerWidth(true) + - /* search */ ($search.length !== 0 ? $search.outerWidth(true) : 0) + - /* toggle */ (numOfVisibleItems !== breakWidths.length ? $btn.outerWidth(true) : 0); + requiredSpace = breakWidths[numOfVisibleItems - 1]; + + // There is not enought space + if (requiredSpace > availableSpace) { + $vlinks.children().last().prependTo($hlinks); + numOfVisibleItems -= 1; + check(); + // There is more than enough space. If only one element is hidden, add the toggle width to the available space + } else if (availableSpace + (numOfVisibleItems === breakWidths.length - 1?$btn.outerWidth(true):0) > breakWidths[numOfVisibleItems]) { + $hlinks.children().first().appendTo($vlinks); + numOfVisibleItems += 1; + check(); + } + // Update the button accordingly + $btn.attr("count", numOfItems - numOfVisibleItems); + if (numOfVisibleItems === numOfItems) { + $btn.addClass('hidden'); + } else $btn.removeClass('hidden'); + } + + // Window listeners + $(window).resize(function() { + check(); + }); + + $btn.on('click', function() { + $hlinks.toggleClass('hidden'); + $(this).toggleClass('close'); + clearTimeout(timer); + }); + + $hlinks.on('mouseleave', function() { + // Mouse has left, start the timer + timer = setTimeout(function() { + $hlinks.addClass('hidden'); + }, closingTime); + }).on('mouseenter', function() { + // Mouse is back, cancel the timer + clearTimeout(timer); + }) + + // check if page has a logo + if($logoImg.length !== 0){ + // check if logo is not loaded + if(!($logoImg[0].complete || $logoImg[0].naturalWidth !== 0)){ + // if logo is not loaded wait for logo to load or fail to check + $logoImg.one("load error", check); + // if logo is already loaded just check + } else check(); + // if page does not have a logo just check + } else check(); + +}); diff --git a/assets/js/plugins/jquery.magnific-popup.js b/assets/js/plugins/jquery.magnific-popup.js new file mode 100644 index 0000000..7d1d197 --- /dev/null +++ b/assets/js/plugins/jquery.magnific-popup.js @@ -0,0 +1,1860 @@ +/*! Magnific Popup - v1.1.0 - 2016-02-20 +* http://dimsemenov.com/plugins/magnific-popup/ +* Copyright (c) 2016 Dmitry Semenov; */ +;(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery || window.Zepto); + } + }(function($) { + + /*>>core*/ + /** + * + * Magnific Popup Core JS file + * + */ + + + /** + * Private static constants + */ + var CLOSE_EVENT = 'Close', + BEFORE_CLOSE_EVENT = 'BeforeClose', + AFTER_CLOSE_EVENT = 'AfterClose', + BEFORE_APPEND_EVENT = 'BeforeAppend', + MARKUP_PARSE_EVENT = 'MarkupParse', + OPEN_EVENT = 'Open', + CHANGE_EVENT = 'Change', + NS = 'mfp', + EVENT_NS = '.' + NS, + READY_CLASS = 'mfp-ready', + REMOVING_CLASS = 'mfp-removing', + PREVENT_CLOSE_CLASS = 'mfp-prevent-close'; + + + /** + * Private vars + */ + /*jshint -W079 */ + var mfp, // As we have only one instance of MagnificPopup object, we define it locally to not to use 'this' + MagnificPopup = function(){}, + _isJQ = !!(window.jQuery), + _prevStatus, + _window = $(window), + _document, + _prevContentType, + _wrapClasses, + _currPopupType; + + + /** + * Private functions + */ + var _mfpOn = function(name, f) { + mfp.ev.on(NS + name + EVENT_NS, f); + }, + _getEl = function(className, appendTo, html, raw) { + var el = document.createElement('div'); + el.className = 'mfp-'+className; + if(html) { + el.innerHTML = html; + } + if(!raw) { + el = $(el); + if(appendTo) { + el.appendTo(appendTo); + } + } else if(appendTo) { + appendTo.appendChild(el); + } + return el; + }, + _mfpTrigger = function(e, data) { + mfp.ev.triggerHandler(NS + e, data); + + if(mfp.st.callbacks) { + // converts "mfpEventName" to "eventName" callback and triggers it if it's present + e = e.charAt(0).toLowerCase() + e.slice(1); + if(mfp.st.callbacks[e]) { + mfp.st.callbacks[e].apply(mfp, $.isArray(data) ? data : [data]); + } + } + }, + _getCloseBtn = function(type) { + if(type !== _currPopupType || !mfp.currTemplate.closeBtn) { + mfp.currTemplate.closeBtn = $( mfp.st.closeMarkup.replace('%title%', mfp.st.tClose ) ); + _currPopupType = type; + } + return mfp.currTemplate.closeBtn; + }, + // Initialize Magnific Popup only when called at least once + _checkInstance = function() { + if(!$.magnificPopup.instance) { + /*jshint -W020 */ + mfp = new MagnificPopup(); + mfp.init(); + $.magnificPopup.instance = mfp; + } + }, + // CSS transition detection, http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr + supportsTransitions = function() { + var s = document.createElement('p').style, // 's' for style. better to create an element if body yet to exist + v = ['ms','O','Moz','Webkit']; // 'v' for vendor + + if( s['transition'] !== undefined ) { + return true; + } + + while( v.length ) { + if( v.pop() + 'Transition' in s ) { + return true; + } + } + + return false; + }; + + + + /** + * Public functions + */ + MagnificPopup.prototype = { + + constructor: MagnificPopup, + + /** + * Initializes Magnific Popup plugin. + * This function is triggered only once when $.fn.magnificPopup or $.magnificPopup is executed + */ + init: function() { + var appVersion = navigator.appVersion; + mfp.isLowIE = mfp.isIE8 = document.all && !document.addEventListener; + mfp.isAndroid = (/android/gi).test(appVersion); + mfp.isIOS = (/iphone|ipad|ipod/gi).test(appVersion); + mfp.supportsTransition = supportsTransitions(); + + // We disable fixed positioned lightbox on devices that don't handle it nicely. + // If you know a better way of detecting this - let me know. + mfp.probablyMobile = (mfp.isAndroid || mfp.isIOS || /(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent) ); + _document = $(document); + + mfp.popupsCache = {}; + }, + + /** + * Opens popup + * @param data [description] + */ + open: function(data) { + + var i; + + if(data.isObj === false) { + // convert jQuery collection to array to avoid conflicts later + mfp.items = data.items.toArray(); + + mfp.index = 0; + var items = data.items, + item; + for(i = 0; i < items.length; i++) { + item = items[i]; + if(item.parsed) { + item = item.el[0]; + } + if(item === data.el[0]) { + mfp.index = i; + break; + } + } + } else { + mfp.items = $.isArray(data.items) ? data.items : [data.items]; + mfp.index = data.index || 0; + } + + // if popup is already opened - we just update the content + if(mfp.isOpen) { + mfp.updateItemHTML(); + return; + } + + mfp.types = []; + _wrapClasses = ''; + if(data.mainEl && data.mainEl.length) { + mfp.ev = data.mainEl.eq(0); + } else { + mfp.ev = _document; + } + + if(data.key) { + if(!mfp.popupsCache[data.key]) { + mfp.popupsCache[data.key] = {}; + } + mfp.currTemplate = mfp.popupsCache[data.key]; + } else { + mfp.currTemplate = {}; + } + + + + mfp.st = $.extend(true, {}, $.magnificPopup.defaults, data ); + mfp.fixedContentPos = mfp.st.fixedContentPos === 'auto' ? !mfp.probablyMobile : mfp.st.fixedContentPos; + + if(mfp.st.modal) { + mfp.st.closeOnContentClick = false; + mfp.st.closeOnBgClick = false; + mfp.st.showCloseBtn = false; + mfp.st.enableEscapeKey = false; + } + + + // Building markup + // main containers are created only once + if(!mfp.bgOverlay) { + + // Dark overlay + mfp.bgOverlay = _getEl('bg').on('click'+EVENT_NS, function() { + mfp.close(); + }); + + mfp.wrap = _getEl('wrap').attr('tabindex', -1).on('click'+EVENT_NS, function(e) { + if(mfp._checkIfClose(e.target)) { + mfp.close(); + } + }); + + mfp.container = _getEl('container', mfp.wrap); + } + + mfp.contentContainer = _getEl('content'); + if(mfp.st.preloader) { + mfp.preloader = _getEl('preloader', mfp.container, mfp.st.tLoading); + } + + + // Initializing modules + var modules = $.magnificPopup.modules; + for(i = 0; i < modules.length; i++) { + var n = modules[i]; + n = n.charAt(0).toUpperCase() + n.slice(1); + mfp['init'+n].call(mfp); + } + _mfpTrigger('BeforeOpen'); + + + if(mfp.st.showCloseBtn) { + // Close button + if(!mfp.st.closeBtnInside) { + mfp.wrap.append( _getCloseBtn() ); + } else { + _mfpOn(MARKUP_PARSE_EVENT, function(e, template, values, item) { + values.close_replaceWith = _getCloseBtn(item.type); + }); + _wrapClasses += ' mfp-close-btn-in'; + } + } + + if(mfp.st.alignTop) { + _wrapClasses += ' mfp-align-top'; + } + + + + if(mfp.fixedContentPos) { + mfp.wrap.css({ + overflow: mfp.st.overflowY, + overflowX: 'hidden', + overflowY: mfp.st.overflowY + }); + } else { + mfp.wrap.css({ + top: _window.scrollTop(), + position: 'absolute' + }); + } + if( mfp.st.fixedBgPos === false || (mfp.st.fixedBgPos === 'auto' && !mfp.fixedContentPos) ) { + mfp.bgOverlay.css({ + height: _document.height(), + position: 'absolute' + }); + } + + + + if(mfp.st.enableEscapeKey) { + // Close on ESC key + _document.on('keyup' + EVENT_NS, function(e) { + if(e.keyCode === 27) { + mfp.close(); + } + }); + } + + _window.on('resize' + EVENT_NS, function() { + mfp.updateSize(); + }); + + + if(!mfp.st.closeOnContentClick) { + _wrapClasses += ' mfp-auto-cursor'; + } + + if(_wrapClasses) + mfp.wrap.addClass(_wrapClasses); + + + // this triggers recalculation of layout, so we get it once to not to trigger twice + var windowHeight = mfp.wH = _window.height(); + + + var windowStyles = {}; + + if( mfp.fixedContentPos ) { + if(mfp._hasScrollBar(windowHeight)){ + var s = mfp._getScrollbarSize(); + if(s) { + windowStyles.marginRight = s; + } + } + } + + if(mfp.fixedContentPos) { + if(!mfp.isIE7) { + windowStyles.overflow = 'hidden'; + } else { + // ie7 double-scroll bug + $('body, html').css('overflow', 'hidden'); + } + } + + + + var classesToadd = mfp.st.mainClass; + if(mfp.isIE7) { + classesToadd += ' mfp-ie7'; + } + if(classesToadd) { + mfp._addClassToMFP( classesToadd ); + } + + // add content + mfp.updateItemHTML(); + + _mfpTrigger('BuildControls'); + + // remove scrollbar, add margin e.t.c + $('html').css(windowStyles); + + // add everything to DOM + mfp.bgOverlay.add(mfp.wrap).prependTo( mfp.st.prependTo || $(document.body) ); + + // Save last focused element + mfp._lastFocusedEl = document.activeElement; + + // Wait for next cycle to allow CSS transition + setTimeout(function() { + + if(mfp.content) { + mfp._addClassToMFP(READY_CLASS); + mfp._setFocus(); + } else { + // if content is not defined (not loaded e.t.c) we add class only for BG + mfp.bgOverlay.addClass(READY_CLASS); + } + + // Trap the focus in popup + _document.on('focusin' + EVENT_NS, mfp._onFocusIn); + + }, 16); + + mfp.isOpen = true; + mfp.updateSize(windowHeight); + _mfpTrigger(OPEN_EVENT); + + return data; + }, + + /** + * Closes the popup + */ + close: function() { + if(!mfp.isOpen) return; + _mfpTrigger(BEFORE_CLOSE_EVENT); + + mfp.isOpen = false; + // for CSS3 animation + if(mfp.st.removalDelay && !mfp.isLowIE && mfp.supportsTransition ) { + mfp._addClassToMFP(REMOVING_CLASS); + setTimeout(function() { + mfp._close(); + }, mfp.st.removalDelay); + } else { + mfp._close(); + } + }, + + /** + * Helper for close() function + */ + _close: function() { + _mfpTrigger(CLOSE_EVENT); + + var classesToRemove = REMOVING_CLASS + ' ' + READY_CLASS + ' '; + + mfp.bgOverlay.detach(); + mfp.wrap.detach(); + mfp.container.empty(); + + if(mfp.st.mainClass) { + classesToRemove += mfp.st.mainClass + ' '; + } + + mfp._removeClassFromMFP(classesToRemove); + + if(mfp.fixedContentPos) { + var windowStyles = {marginRight: ''}; + if(mfp.isIE7) { + $('body, html').css('overflow', ''); + } else { + windowStyles.overflow = ''; + } + $('html').css(windowStyles); + } + + _document.off('keyup' + EVENT_NS + ' focusin' + EVENT_NS); + mfp.ev.off(EVENT_NS); + + // clean up DOM elements that aren't removed + mfp.wrap.attr('class', 'mfp-wrap').removeAttr('style'); + mfp.bgOverlay.attr('class', 'mfp-bg'); + mfp.container.attr('class', 'mfp-container'); + + // remove close button from target element + if(mfp.st.showCloseBtn && + (!mfp.st.closeBtnInside || mfp.currTemplate[mfp.currItem.type] === true)) { + if(mfp.currTemplate.closeBtn) + mfp.currTemplate.closeBtn.detach(); + } + + + if(mfp.st.autoFocusLast && mfp._lastFocusedEl) { + $(mfp._lastFocusedEl).focus(); // put tab focus back + } + mfp.currItem = null; + mfp.content = null; + mfp.currTemplate = null; + mfp.prevHeight = 0; + + _mfpTrigger(AFTER_CLOSE_EVENT); + }, + + updateSize: function(winHeight) { + + if(mfp.isIOS) { + // fixes iOS nav bars https://github.com/dimsemenov/Magnific-Popup/issues/2 + var zoomLevel = document.documentElement.clientWidth / window.innerWidth; + var height = window.innerHeight * zoomLevel; + mfp.wrap.css('height', height); + mfp.wH = height; + } else { + mfp.wH = winHeight || _window.height(); + } + // Fixes #84: popup incorrectly positioned with position:relative on body + if(!mfp.fixedContentPos) { + mfp.wrap.css('height', mfp.wH); + } + + _mfpTrigger('Resize'); + + }, + + /** + * Set content of popup based on current index + */ + updateItemHTML: function() { + var item = mfp.items[mfp.index]; + + // Detach and perform modifications + mfp.contentContainer.detach(); + + if(mfp.content) + mfp.content.detach(); + + if(!item.parsed) { + item = mfp.parseEl( mfp.index ); + } + + var type = item.type; + + _mfpTrigger('BeforeChange', [mfp.currItem ? mfp.currItem.type : '', type]); + // BeforeChange event works like so: + // _mfpOn('BeforeChange', function(e, prevType, newType) { }); + + mfp.currItem = item; + + if(!mfp.currTemplate[type]) { + var markup = mfp.st[type] ? mfp.st[type].markup : false; + + // allows to modify markup + _mfpTrigger('FirstMarkupParse', markup); + + if(markup) { + mfp.currTemplate[type] = $(markup); + } else { + // if there is no markup found we just define that template is parsed + mfp.currTemplate[type] = true; + } + } + + if(_prevContentType && _prevContentType !== item.type) { + mfp.container.removeClass('mfp-'+_prevContentType+'-holder'); + } + + var newContent = mfp['get' + type.charAt(0).toUpperCase() + type.slice(1)](item, mfp.currTemplate[type]); + mfp.appendContent(newContent, type); + + item.preloaded = true; + + _mfpTrigger(CHANGE_EVENT, item); + _prevContentType = item.type; + + // Append container back after its content changed + mfp.container.prepend(mfp.contentContainer); + + _mfpTrigger('AfterChange'); + }, + + + /** + * Set HTML content of popup + */ + appendContent: function(newContent, type) { + mfp.content = newContent; + + if(newContent) { + if(mfp.st.showCloseBtn && mfp.st.closeBtnInside && + mfp.currTemplate[type] === true) { + // if there is no markup, we just append close button element inside + if(!mfp.content.find('.mfp-close').length) { + mfp.content.append(_getCloseBtn()); + } + } else { + mfp.content = newContent; + } + } else { + mfp.content = ''; + } + + _mfpTrigger(BEFORE_APPEND_EVENT); + mfp.container.addClass('mfp-'+type+'-holder'); + + mfp.contentContainer.append(mfp.content); + }, + + + /** + * Creates Magnific Popup data object based on given data + * @param {int} index Index of item to parse + */ + parseEl: function(index) { + var item = mfp.items[index], + type; + + if(item.tagName) { + item = { el: $(item) }; + } else { + type = item.type; + item = { data: item, src: item.src }; + } + + if(item.el) { + var types = mfp.types; + + // check for 'mfp-TYPE' class + for(var i = 0; i < types.length; i++) { + if( item.el.hasClass('mfp-'+types[i]) ) { + type = types[i]; + break; + } + } + + item.src = item.el.attr('data-mfp-src'); + if(!item.src) { + item.src = item.el.attr('href'); + } + } + + item.type = type || mfp.st.type || 'inline'; + item.index = index; + item.parsed = true; + mfp.items[index] = item; + _mfpTrigger('ElementParse', item); + + return mfp.items[index]; + }, + + + /** + * Initializes single popup or a group of popups + */ + addGroup: function(el, options) { + var eHandler = function(e) { + e.mfpEl = this; + mfp._openClick(e, el, options); + }; + + if(!options) { + options = {}; + } + + var eName = 'click.magnificPopup'; + options.mainEl = el; + + if(options.items) { + options.isObj = true; + el.off(eName).on(eName, eHandler); + } else { + options.isObj = false; + if(options.delegate) { + el.off(eName).on(eName, options.delegate , eHandler); + } else { + options.items = el; + el.off(eName).on(eName, eHandler); + } + } + }, + _openClick: function(e, el, options) { + var midClick = options.midClick !== undefined ? options.midClick : $.magnificPopup.defaults.midClick; + + + if(!midClick && ( e.which === 2 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey ) ) { + return; + } + + var disableOn = options.disableOn !== undefined ? options.disableOn : $.magnificPopup.defaults.disableOn; + + if(disableOn) { + if($.isFunction(disableOn)) { + if( !disableOn.call(mfp) ) { + return true; + } + } else { // else it's number + if( _window.width() < disableOn ) { + return true; + } + } + } + + if(e.type) { + e.preventDefault(); + + // This will prevent popup from closing if element is inside and popup is already opened + if(mfp.isOpen) { + e.stopPropagation(); + } + } + + options.el = $(e.mfpEl); + if(options.delegate) { + options.items = el.find(options.delegate); + } + mfp.open(options); + }, + + + /** + * Updates text on preloader + */ + updateStatus: function(status, text) { + + if(mfp.preloader) { + if(_prevStatus !== status) { + mfp.container.removeClass('mfp-s-'+_prevStatus); + } + + if(!text && status === 'loading') { + text = mfp.st.tLoading; + } + + var data = { + status: status, + text: text + }; + // allows to modify status + _mfpTrigger('UpdateStatus', data); + + status = data.status; + text = data.text; + + mfp.preloader.html(text); + + mfp.preloader.find('a').on('click', function(e) { + e.stopImmediatePropagation(); + }); + + mfp.container.addClass('mfp-s-'+status); + _prevStatus = status; + } + }, + + + /* + "Private" helpers that aren't private at all + */ + // Check to close popup or not + // "target" is an element that was clicked + _checkIfClose: function(target) { + + if($(target).hasClass(PREVENT_CLOSE_CLASS)) { + return; + } + + var closeOnContent = mfp.st.closeOnContentClick; + var closeOnBg = mfp.st.closeOnBgClick; + + if(closeOnContent && closeOnBg) { + return true; + } else { + + // We close the popup if click is on close button or on preloader. Or if there is no content. + if(!mfp.content || $(target).hasClass('mfp-close') || (mfp.preloader && target === mfp.preloader[0]) ) { + return true; + } + + // if click is outside the content + if( (target !== mfp.content[0] && !$.contains(mfp.content[0], target)) ) { + if(closeOnBg) { + // last check, if the clicked element is in DOM, (in case it's removed onclick) + if( $.contains(document, target) ) { + return true; + } + } + } else if(closeOnContent) { + return true; + } + + } + return false; + }, + _addClassToMFP: function(cName) { + mfp.bgOverlay.addClass(cName); + mfp.wrap.addClass(cName); + }, + _removeClassFromMFP: function(cName) { + this.bgOverlay.removeClass(cName); + mfp.wrap.removeClass(cName); + }, + _hasScrollBar: function(winHeight) { + return ( (mfp.isIE7 ? _document.height() : document.body.scrollHeight) > (winHeight || _window.height()) ); + }, + _setFocus: function() { + (mfp.st.focus ? mfp.content.find(mfp.st.focus).eq(0) : mfp.wrap).focus(); + }, + _onFocusIn: function(e) { + if( e.target !== mfp.wrap[0] && !$.contains(mfp.wrap[0], e.target) ) { + mfp._setFocus(); + return false; + } + }, + _parseMarkup: function(template, values, item) { + var arr; + if(item.data) { + values = $.extend(item.data, values); + } + _mfpTrigger(MARKUP_PARSE_EVENT, [template, values, item] ); + + $.each(values, function(key, value) { + if(value === undefined || value === false) { + return true; + } + arr = key.split('_'); + if(arr.length > 1) { + var el = template.find(EVENT_NS + '-'+arr[0]); + + if(el.length > 0) { + var attr = arr[1]; + if(attr === 'replaceWith') { + if(el[0] !== value[0]) { + el.replaceWith(value); + } + } else if(attr === 'img') { + if(el.is('img')) { + el.attr('src', value); + } else { + el.replaceWith( $('').attr('src', value).attr('class', el.attr('class')) ); + } + } else { + el.attr(arr[1], value); + } + } + + } else { + template.find(EVENT_NS + '-'+key).html(value); + } + }); + }, + + _getScrollbarSize: function() { + // thx David + if(mfp.scrollbarSize === undefined) { + var scrollDiv = document.createElement("div"); + scrollDiv.style.cssText = 'width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;'; + document.body.appendChild(scrollDiv); + mfp.scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + } + return mfp.scrollbarSize; + } + + }; /* MagnificPopup core prototype end */ + + + + + /** + * Public static functions + */ + $.magnificPopup = { + instance: null, + proto: MagnificPopup.prototype, + modules: [], + + open: function(options, index) { + _checkInstance(); + + if(!options) { + options = {}; + } else { + options = $.extend(true, {}, options); + } + + options.isObj = true; + options.index = index || 0; + return this.instance.open(options); + }, + + close: function() { + return $.magnificPopup.instance && $.magnificPopup.instance.close(); + }, + + registerModule: function(name, module) { + if(module.options) { + $.magnificPopup.defaults[name] = module.options; + } + $.extend(this.proto, module.proto); + this.modules.push(name); + }, + + defaults: { + + // Info about options is in docs: + // http://dimsemenov.com/plugins/magnific-popup/documentation.html#options + + disableOn: 0, + + key: null, + + midClick: false, + + mainClass: '', + + preloader: true, + + focus: '', // CSS selector of input to focus after popup is opened + + closeOnContentClick: false, + + closeOnBgClick: true, + + closeBtnInside: true, + + showCloseBtn: true, + + enableEscapeKey: true, + + modal: false, + + alignTop: false, + + removalDelay: 0, + + prependTo: null, + + fixedContentPos: 'auto', + + fixedBgPos: 'auto', + + overflowY: 'auto', + + closeMarkup: '', + + tClose: 'Close (Esc)', + + tLoading: 'Loading...', + + autoFocusLast: true + + } + }; + + + + $.fn.magnificPopup = function(options) { + _checkInstance(); + + var jqEl = $(this); + + // We call some API method of first param is a string + if (typeof options === "string" ) { + + if(options === 'open') { + var items, + itemOpts = _isJQ ? jqEl.data('magnificPopup') : jqEl[0].magnificPopup, + index = parseInt(arguments[1], 10) || 0; + + if(itemOpts.items) { + items = itemOpts.items[index]; + } else { + items = jqEl; + if(itemOpts.delegate) { + items = items.find(itemOpts.delegate); + } + items = items.eq( index ); + } + mfp._openClick({mfpEl:items}, jqEl, itemOpts); + } else { + if(mfp.isOpen) + mfp[options].apply(mfp, Array.prototype.slice.call(arguments, 1)); + } + + } else { + // clone options obj + options = $.extend(true, {}, options); + + /* + * As Zepto doesn't support .data() method for objects + * and it works only in normal browsers + * we assign "options" object directly to the DOM element. FTW! + */ + if(_isJQ) { + jqEl.data('magnificPopup', options); + } else { + jqEl[0].magnificPopup = options; + } + + mfp.addGroup(jqEl, options); + + } + return jqEl; + }; + + /*>>core*/ + + /*>>inline*/ + + var INLINE_NS = 'inline', + _hiddenClass, + _inlinePlaceholder, + _lastInlineElement, + _putInlineElementsBack = function() { + if(_lastInlineElement) { + _inlinePlaceholder.after( _lastInlineElement.addClass(_hiddenClass) ).detach(); + _lastInlineElement = null; + } + }; + + $.magnificPopup.registerModule(INLINE_NS, { + options: { + hiddenClass: 'hide', // will be appended with `mfp-` prefix + markup: '', + tNotFound: 'Content not found' + }, + proto: { + + initInline: function() { + mfp.types.push(INLINE_NS); + + _mfpOn(CLOSE_EVENT+'.'+INLINE_NS, function() { + _putInlineElementsBack(); + }); + }, + + getInline: function(item, template) { + + _putInlineElementsBack(); + + if(item.src) { + var inlineSt = mfp.st.inline, + el = $(item.src); + + if(el.length) { + + // If target element has parent - we replace it with placeholder and put it back after popup is closed + var parent = el[0].parentNode; + if(parent && parent.tagName) { + if(!_inlinePlaceholder) { + _hiddenClass = inlineSt.hiddenClass; + _inlinePlaceholder = _getEl(_hiddenClass); + _hiddenClass = 'mfp-'+_hiddenClass; + } + // replace target inline element with placeholder + _lastInlineElement = el.after(_inlinePlaceholder).detach().removeClass(_hiddenClass); + } + + mfp.updateStatus('ready'); + } else { + mfp.updateStatus('error', inlineSt.tNotFound); + el = $('
'); + } + + item.inlineElement = el; + return el; + } + + mfp.updateStatus('ready'); + mfp._parseMarkup(template, {}, item); + return template; + } + } + }); + + /*>>inline*/ + + /*>>ajax*/ + var AJAX_NS = 'ajax', + _ajaxCur, + _removeAjaxCursor = function() { + if(_ajaxCur) { + $(document.body).removeClass(_ajaxCur); + } + }, + _destroyAjaxRequest = function() { + _removeAjaxCursor(); + if(mfp.req) { + mfp.req.abort(); + } + }; + + $.magnificPopup.registerModule(AJAX_NS, { + + options: { + settings: null, + cursor: 'mfp-ajax-cur', + tError: 'The content could not be loaded.' + }, + + proto: { + initAjax: function() { + mfp.types.push(AJAX_NS); + _ajaxCur = mfp.st.ajax.cursor; + + _mfpOn(CLOSE_EVENT+'.'+AJAX_NS, _destroyAjaxRequest); + _mfpOn('BeforeChange.' + AJAX_NS, _destroyAjaxRequest); + }, + getAjax: function(item) { + + if(_ajaxCur) { + $(document.body).addClass(_ajaxCur); + } + + mfp.updateStatus('loading'); + + var opts = $.extend({ + url: item.src, + success: function(data, textStatus, jqXHR) { + var temp = { + data:data, + xhr:jqXHR + }; + + _mfpTrigger('ParseAjax', temp); + + mfp.appendContent( $(temp.data), AJAX_NS ); + + item.finished = true; + + _removeAjaxCursor(); + + mfp._setFocus(); + + setTimeout(function() { + mfp.wrap.addClass(READY_CLASS); + }, 16); + + mfp.updateStatus('ready'); + + _mfpTrigger('AjaxContentAdded'); + }, + error: function() { + _removeAjaxCursor(); + item.finished = item.loadError = true; + mfp.updateStatus('error', mfp.st.ajax.tError.replace('%url%', item.src)); + } + }, mfp.st.ajax.settings); + + mfp.req = $.ajax(opts); + + return ''; + } + } + }); + + /*>>ajax*/ + + /*>>image*/ + var _imgInterval, + _getTitle = function(item) { + if(item.data && item.data.title !== undefined) + return item.data.title; + + var src = mfp.st.image.titleSrc; + + if(src) { + if($.isFunction(src)) { + return src.call(mfp, item); + } else if(item.el) { + return item.el.attr(src) || ''; + } + } + return ''; + }; + + $.magnificPopup.registerModule('image', { + + options: { + markup: '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
', + cursor: 'mfp-zoom-out-cur', + titleSrc: 'title', + verticalFit: true, + tError: 'The image could not be loaded.' + }, + + proto: { + initImage: function() { + var imgSt = mfp.st.image, + ns = '.image'; + + mfp.types.push('image'); + + _mfpOn(OPEN_EVENT+ns, function() { + if(mfp.currItem.type === 'image' && imgSt.cursor) { + $(document.body).addClass(imgSt.cursor); + } + }); + + _mfpOn(CLOSE_EVENT+ns, function() { + if(imgSt.cursor) { + $(document.body).removeClass(imgSt.cursor); + } + _window.off('resize' + EVENT_NS); + }); + + _mfpOn('Resize'+ns, mfp.resizeImage); + if(mfp.isLowIE) { + _mfpOn('AfterChange', mfp.resizeImage); + } + }, + resizeImage: function() { + var item = mfp.currItem; + if(!item || !item.img) return; + + if(mfp.st.image.verticalFit) { + var decr = 0; + // fix box-sizing in ie7/8 + if(mfp.isLowIE) { + decr = parseInt(item.img.css('padding-top'), 10) + parseInt(item.img.css('padding-bottom'),10); + } + item.img.css('max-height', mfp.wH-decr); + } + }, + _onImageHasSize: function(item) { + if(item.img) { + + item.hasSize = true; + + if(_imgInterval) { + clearInterval(_imgInterval); + } + + item.isCheckingImgSize = false; + + _mfpTrigger('ImageHasSize', item); + + if(item.imgHidden) { + if(mfp.content) + mfp.content.removeClass('mfp-loading'); + + item.imgHidden = false; + } + + } + }, + + /** + * Function that loops until the image has size to display elements that rely on it asap + */ + findImageSize: function(item) { + + var counter = 0, + img = item.img[0], + mfpSetInterval = function(delay) { + + if(_imgInterval) { + clearInterval(_imgInterval); + } + // decelerating interval that checks for size of an image + _imgInterval = setInterval(function() { + if(img.naturalWidth > 0) { + mfp._onImageHasSize(item); + return; + } + + if(counter > 200) { + clearInterval(_imgInterval); + } + + counter++; + if(counter === 3) { + mfpSetInterval(10); + } else if(counter === 40) { + mfpSetInterval(50); + } else if(counter === 100) { + mfpSetInterval(500); + } + }, delay); + }; + + mfpSetInterval(1); + }, + + getImage: function(item, template) { + + var guard = 0, + + // image load complete handler + onLoadComplete = function() { + if(item) { + if (item.img[0].complete) { + item.img.off('.mfploader'); + + if(item === mfp.currItem){ + mfp._onImageHasSize(item); + + mfp.updateStatus('ready'); + } + + item.hasSize = true; + item.loaded = true; + + _mfpTrigger('ImageLoadComplete'); + + } + else { + // if image complete check fails 200 times (20 sec), we assume that there was an error. + guard++; + if(guard < 200) { + setTimeout(onLoadComplete,100); + } else { + onLoadError(); + } + } + } + }, + + // image error handler + onLoadError = function() { + if(item) { + item.img.off('.mfploader'); + if(item === mfp.currItem){ + mfp._onImageHasSize(item); + mfp.updateStatus('error', imgSt.tError.replace('%url%', item.src) ); + } + + item.hasSize = true; + item.loaded = true; + item.loadError = true; + } + }, + imgSt = mfp.st.image; + + + var el = template.find('.mfp-img'); + if(el.length) { + var img = document.createElement('img'); + img.className = 'mfp-img'; + if(item.el && item.el.find('img').length) { + img.alt = item.el.find('img').attr('alt'); + } + item.img = $(img).on('load.mfploader', onLoadComplete).on('error.mfploader', onLoadError); + img.src = item.src; + + // without clone() "error" event is not firing when IMG is replaced by new IMG + // TODO: find a way to avoid such cloning + if(el.is('img')) { + item.img = item.img.clone(); + } + + img = item.img[0]; + if(img.naturalWidth > 0) { + item.hasSize = true; + } else if(!img.width) { + item.hasSize = false; + } + } + + mfp._parseMarkup(template, { + title: _getTitle(item), + img_replaceWith: item.img + }, item); + + mfp.resizeImage(); + + if(item.hasSize) { + if(_imgInterval) clearInterval(_imgInterval); + + if(item.loadError) { + template.addClass('mfp-loading'); + mfp.updateStatus('error', imgSt.tError.replace('%url%', item.src) ); + } else { + template.removeClass('mfp-loading'); + mfp.updateStatus('ready'); + } + return template; + } + + mfp.updateStatus('loading'); + item.loading = true; + + if(!item.hasSize) { + item.imgHidden = true; + template.addClass('mfp-loading'); + mfp.findImageSize(item); + } + + return template; + } + } + }); + + /*>>image*/ + + /*>>zoom*/ + var hasMozTransform, + getHasMozTransform = function() { + if(hasMozTransform === undefined) { + hasMozTransform = document.createElement('p').style.MozTransform !== undefined; + } + return hasMozTransform; + }; + + $.magnificPopup.registerModule('zoom', { + + options: { + enabled: false, + easing: 'ease-in-out', + duration: 300, + opener: function(element) { + return element.is('img') ? element : element.find('img'); + } + }, + + proto: { + + initZoom: function() { + var zoomSt = mfp.st.zoom, + ns = '.zoom', + image; + + if(!zoomSt.enabled || !mfp.supportsTransition) { + return; + } + + var duration = zoomSt.duration, + getElToAnimate = function(image) { + var newImg = image.clone().removeAttr('style').removeAttr('class').addClass('mfp-animated-image'), + transition = 'all '+(zoomSt.duration/1000)+'s ' + zoomSt.easing, + cssObj = { + position: 'fixed', + zIndex: 9999, + left: 0, + top: 0, + '-webkit-backface-visibility': 'hidden' + }, + t = 'transition'; + + cssObj['-webkit-'+t] = cssObj['-moz-'+t] = cssObj['-o-'+t] = cssObj[t] = transition; + + newImg.css(cssObj); + return newImg; + }, + showMainContent = function() { + mfp.content.css('visibility', 'visible'); + }, + openTimeout, + animatedImg; + + _mfpOn('BuildControls'+ns, function() { + if(mfp._allowZoom()) { + + clearTimeout(openTimeout); + mfp.content.css('visibility', 'hidden'); + + // Basically, all code below does is clones existing image, puts in on top of the current one and animated it + + image = mfp._getItemToZoom(); + + if(!image) { + showMainContent(); + return; + } + + animatedImg = getElToAnimate(image); + + animatedImg.css( mfp._getOffset() ); + + mfp.wrap.append(animatedImg); + + openTimeout = setTimeout(function() { + animatedImg.css( mfp._getOffset( true ) ); + openTimeout = setTimeout(function() { + + showMainContent(); + + setTimeout(function() { + animatedImg.remove(); + image = animatedImg = null; + _mfpTrigger('ZoomAnimationEnded'); + }, 16); // avoid blink when switching images + + }, duration); // this timeout equals animation duration + + }, 16); // by adding this timeout we avoid short glitch at the beginning of animation + + + // Lots of timeouts... + } + }); + _mfpOn(BEFORE_CLOSE_EVENT+ns, function() { + if(mfp._allowZoom()) { + + clearTimeout(openTimeout); + + mfp.st.removalDelay = duration; + + if(!image) { + image = mfp._getItemToZoom(); + if(!image) { + return; + } + animatedImg = getElToAnimate(image); + } + + animatedImg.css( mfp._getOffset(true) ); + mfp.wrap.append(animatedImg); + mfp.content.css('visibility', 'hidden'); + + setTimeout(function() { + animatedImg.css( mfp._getOffset() ); + }, 16); + } + + }); + + _mfpOn(CLOSE_EVENT+ns, function() { + if(mfp._allowZoom()) { + showMainContent(); + if(animatedImg) { + animatedImg.remove(); + } + image = null; + } + }); + }, + + _allowZoom: function() { + return mfp.currItem.type === 'image'; + }, + + _getItemToZoom: function() { + if(mfp.currItem.hasSize) { + return mfp.currItem.img; + } else { + return false; + } + }, + + // Get element postion relative to viewport + _getOffset: function(isLarge) { + var el; + if(isLarge) { + el = mfp.currItem.img; + } else { + el = mfp.st.zoom.opener(mfp.currItem.el || mfp.currItem); + } + + var offset = el.offset(); + var paddingTop = parseInt(el.css('padding-top'),10); + var paddingBottom = parseInt(el.css('padding-bottom'),10); + offset.top -= ( $(window).scrollTop() - paddingTop ); + + + /* + + Animating left + top + width/height looks glitchy in Firefox, but perfect in Chrome. And vice-versa. + + */ + var obj = { + width: el.width(), + // fix Zepto height+padding issue + height: (_isJQ ? el.innerHeight() : el[0].offsetHeight) - paddingBottom - paddingTop + }; + + // I hate to do this, but there is no another option + if( getHasMozTransform() ) { + obj['-moz-transform'] = obj['transform'] = 'translate(' + offset.left + 'px,' + offset.top + 'px)'; + } else { + obj.left = offset.left; + obj.top = offset.top; + } + return obj; + } + + } + }); + + + + /*>>zoom*/ + + /*>>iframe*/ + + var IFRAME_NS = 'iframe', + _emptyPage = '//about:blank', + + _fixIframeBugs = function(isShowing) { + if(mfp.currTemplate[IFRAME_NS]) { + var el = mfp.currTemplate[IFRAME_NS].find('iframe'); + if(el.length) { + // reset src after the popup is closed to avoid "video keeps playing after popup is closed" bug + if(!isShowing) { + el[0].src = _emptyPage; + } + + // IE8 black screen bug fix + if(mfp.isIE8) { + el.css('display', isShowing ? 'block' : 'none'); + } + } + } + }; + + $.magnificPopup.registerModule(IFRAME_NS, { + + options: { + markup: '
'+ + '
'+ + ''+ + '
', + + srcAction: 'iframe_src', + + // we don't care and support only one default type of URL by default + patterns: { + youtube: { + index: 'youtube.com', + id: 'v=', + src: '//www.youtube.com/embed/%id%?autoplay=1' + }, + vimeo: { + index: 'vimeo.com/', + id: '/', + src: '//player.vimeo.com/video/%id%?autoplay=1' + }, + gmaps: { + index: '//maps.google.', + src: '%id%&output=embed' + } + } + }, + + proto: { + initIframe: function() { + mfp.types.push(IFRAME_NS); + + _mfpOn('BeforeChange', function(e, prevType, newType) { + if(prevType !== newType) { + if(prevType === IFRAME_NS) { + _fixIframeBugs(); // iframe if removed + } else if(newType === IFRAME_NS) { + _fixIframeBugs(true); // iframe is showing + } + }// else { + // iframe source is switched, don't do anything + //} + }); + + _mfpOn(CLOSE_EVENT + '.' + IFRAME_NS, function() { + _fixIframeBugs(); + }); + }, + + getIframe: function(item, template) { + var embedSrc = item.src; + var iframeSt = mfp.st.iframe; + + $.each(iframeSt.patterns, function() { + if(embedSrc.indexOf( this.index ) > -1) { + if(this.id) { + if(typeof this.id === 'string') { + embedSrc = embedSrc.substr(embedSrc.lastIndexOf(this.id)+this.id.length, embedSrc.length); + } else { + embedSrc = this.id.call( this, embedSrc ); + } + } + embedSrc = this.src.replace('%id%', embedSrc ); + return false; // break; + } + }); + + var dataObj = {}; + if(iframeSt.srcAction) { + dataObj[iframeSt.srcAction] = embedSrc; + } + mfp._parseMarkup(template, dataObj, item); + + mfp.updateStatus('ready'); + + return template; + } + } + }); + + + + /*>>iframe*/ + + /*>>gallery*/ + /** + * Get looped index depending on number of slides + */ + var _getLoopedId = function(index) { + var numSlides = mfp.items.length; + if(index > numSlides - 1) { + return index - numSlides; + } else if(index < 0) { + return numSlides + index; + } + return index; + }, + _replaceCurrTotal = function(text, curr, total) { + return text.replace(/%curr%/gi, curr + 1).replace(/%total%/gi, total); + }; + + $.magnificPopup.registerModule('gallery', { + + options: { + enabled: false, + arrowMarkup: '', + preload: [0,2], + navigateByImgClick: true, + arrows: true, + + tPrev: 'Previous (Left arrow key)', + tNext: 'Next (Right arrow key)', + tCounter: '%curr% of %total%' + }, + + proto: { + initGallery: function() { + + var gSt = mfp.st.gallery, + ns = '.mfp-gallery'; + + mfp.direction = true; // true - next, false - prev + + if(!gSt || !gSt.enabled ) return false; + + _wrapClasses += ' mfp-gallery'; + + _mfpOn(OPEN_EVENT+ns, function() { + + if(gSt.navigateByImgClick) { + mfp.wrap.on('click'+ns, '.mfp-img', function() { + if(mfp.items.length > 1) { + mfp.next(); + return false; + } + }); + } + + _document.on('keydown'+ns, function(e) { + if (e.keyCode === 37) { + mfp.prev(); + } else if (e.keyCode === 39) { + mfp.next(); + } + }); + }); + + _mfpOn('UpdateStatus'+ns, function(e, data) { + if(data.text) { + data.text = _replaceCurrTotal(data.text, mfp.currItem.index, mfp.items.length); + } + }); + + _mfpOn(MARKUP_PARSE_EVENT+ns, function(e, element, values, item) { + var l = mfp.items.length; + values.counter = l > 1 ? _replaceCurrTotal(gSt.tCounter, item.index, l) : ''; + }); + + _mfpOn('BuildControls' + ns, function() { + if(mfp.items.length > 1 && gSt.arrows && !mfp.arrowLeft) { + var markup = gSt.arrowMarkup, + arrowLeft = mfp.arrowLeft = $( markup.replace(/%title%/gi, gSt.tPrev).replace(/%dir%/gi, 'left') ).addClass(PREVENT_CLOSE_CLASS), + arrowRight = mfp.arrowRight = $( markup.replace(/%title%/gi, gSt.tNext).replace(/%dir%/gi, 'right') ).addClass(PREVENT_CLOSE_CLASS); + + arrowLeft.click(function() { + mfp.prev(); + }); + arrowRight.click(function() { + mfp.next(); + }); + + mfp.container.append(arrowLeft.add(arrowRight)); + } + }); + + _mfpOn(CHANGE_EVENT+ns, function() { + if(mfp._preloadTimeout) clearTimeout(mfp._preloadTimeout); + + mfp._preloadTimeout = setTimeout(function() { + mfp.preloadNearbyImages(); + mfp._preloadTimeout = null; + }, 16); + }); + + + _mfpOn(CLOSE_EVENT+ns, function() { + _document.off(ns); + mfp.wrap.off('click'+ns); + mfp.arrowRight = mfp.arrowLeft = null; + }); + + }, + next: function() { + mfp.direction = true; + mfp.index = _getLoopedId(mfp.index + 1); + mfp.updateItemHTML(); + }, + prev: function() { + mfp.direction = false; + mfp.index = _getLoopedId(mfp.index - 1); + mfp.updateItemHTML(); + }, + goTo: function(newIndex) { + mfp.direction = (newIndex >= mfp.index); + mfp.index = newIndex; + mfp.updateItemHTML(); + }, + preloadNearbyImages: function() { + var p = mfp.st.gallery.preload, + preloadBefore = Math.min(p[0], mfp.items.length), + preloadAfter = Math.min(p[1], mfp.items.length), + i; + + for(i = 1; i <= (mfp.direction ? preloadAfter : preloadBefore); i++) { + mfp._preloadItem(mfp.index+i); + } + for(i = 1; i <= (mfp.direction ? preloadBefore : preloadAfter); i++) { + mfp._preloadItem(mfp.index-i); + } + }, + _preloadItem: function(index) { + index = _getLoopedId(index); + + if(mfp.items[index].preloaded) { + return; + } + + var item = mfp.items[index]; + if(!item.parsed) { + item = mfp.parseEl( index ); + } + + _mfpTrigger('LazyLoad', item); + + if(item.type === 'image') { + item.img = $('').on('load.mfploader', function() { + item.hasSize = true; + }).on('error.mfploader', function() { + item.hasSize = true; + item.loadError = true; + _mfpTrigger('LazyLoadError', item); + }).attr('src', item.src); + } + + + item.preloaded = true; + } + } + }); + + /*>>gallery*/ + + /*>>retina*/ + + var RETINA_NS = 'retina'; + + $.magnificPopup.registerModule(RETINA_NS, { + options: { + replaceSrc: function(item) { + return item.src.replace(/\.\w+$/, function(m) { return '@2x' + m; }); + }, + ratio: 1 // Function or number. Set to 1 to disable. + }, + proto: { + initRetina: function() { + if(window.devicePixelRatio > 1) { + + var st = mfp.st.retina, + ratio = st.ratio; + + ratio = !isNaN(ratio) ? ratio : ratio(); + + if(ratio > 1) { + _mfpOn('ImageHasSize' + '.' + RETINA_NS, function(e, item) { + item.img.css({ + 'max-width': item.img[0].naturalWidth / ratio, + 'width': '100%' + }); + }); + _mfpOn('ElementParse' + '.' + RETINA_NS, function(e, item) { + item.src = st.replaceSrc(item, ratio); + }); + } + } + + } + } + }); + + /*>>retina*/ + _checkInstance(); })); \ No newline at end of file diff --git a/assets/js/plugins/smooth-scroll.js b/assets/js/plugins/smooth-scroll.js new file mode 100644 index 0000000..c4179a7 --- /dev/null +++ b/assets/js/plugins/smooth-scroll.js @@ -0,0 +1,650 @@ +/*! + * smooth-scroll v16.1.2 + * Animate scrolling to anchor links + * (c) 2020 Chris Ferdinandi + * MIT License + * http://github.com/cferdinandi/smooth-scroll + */ + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], (function () { + return factory(root); + })); + } else if (typeof exports === 'object') { + module.exports = factory(root); + } else { + root.SmoothScroll = factory(root); + } +})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this, (function (window) { + + 'use strict'; + + // + // Default settings + // + + var defaults = { + + // Selectors + ignore: '[data-scroll-ignore]', + header: null, + topOnEmptyHash: true, + + // Speed & Duration + speed: 500, + speedAsDuration: false, + durationMax: null, + durationMin: null, + clip: true, + offset: 0, + + // Easing + easing: 'easeInOutCubic', + customEasing: null, + + // History + updateURL: true, + popstate: true, + + // Custom Events + emitEvents: true + + }; + + + // + // Utility Methods + // + + /** + * Check if browser supports required methods + * @return {Boolean} Returns true if all required methods are supported + */ + var supports = function () { + return ( + 'querySelector' in document && + 'addEventListener' in window && + 'requestAnimationFrame' in window && + 'closest' in window.Element.prototype + ); + }; + + /** + * Merge two or more objects together. + * @param {Object} objects The objects to merge together + * @returns {Object} Merged values of defaults and options + */ + var extend = function () { + var merged = {}; + Array.prototype.forEach.call(arguments, (function (obj) { + for (var key in obj) { + if (!obj.hasOwnProperty(key)) return; + merged[key] = obj[key]; + } + })); + return merged; + }; + + /** + * Check to see if user prefers reduced motion + * @param {Object} settings Script settings + */ + var reduceMotion = function () { + if ('matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches) { + return true; + } + return false; + }; + + /** + * Get the height of an element. + * @param {Node} elem The element to get the height of + * @return {Number} The element's height in pixels + */ + var getHeight = function (elem) { + return parseInt(window.getComputedStyle(elem).height, 10); + }; + + /** + * Escape special characters for use with querySelector + * @author Mathias Bynens + * @link https://github.com/mathiasbynens/CSS.escape + * @param {String} id The anchor ID to escape + */ + var escapeCharacters = function (id) { + + // Remove leading hash + if (id.charAt(0) === '#') { + id = id.substr(1); + } + + var string = String(id); + var length = string.length; + var index = -1; + var codeUnit; + var result = ''; + var firstCodeUnit = string.charCodeAt(0); + while (++index < length) { + codeUnit = string.charCodeAt(index); + // Note: there’s no need to special-case astral symbols, surrogate + // pairs, or lone surrogates. + + // If the character is NULL (U+0000), then throw an + // `InvalidCharacterError` exception and terminate these steps. + if (codeUnit === 0x0000) { + throw new InvalidCharacterError( + 'Invalid character: the input contains U+0000.' + ); + } + + if ( + // If the character is in the range [\1-\1F] (U+0001 to U+001F) or is + // U+007F, […] + (codeUnit >= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F || + // If the character is the first character and is in the range [0-9] + // (U+0030 to U+0039), […] + (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || + // If the character is the second character and is in the range [0-9] + // (U+0030 to U+0039) and the first character is a `-` (U+002D), […] + ( + index === 1 && + codeUnit >= 0x0030 && codeUnit <= 0x0039 && + firstCodeUnit === 0x002D + ) + ) { + // http://dev.w3.org/csswg/cssom/#escape-a-character-as-code-point + result += '\\' + codeUnit.toString(16) + ' '; + continue; + } + + // If the character is not handled by one of the above rules and is + // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or + // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to + // U+005A), or [a-z] (U+0061 to U+007A), […] + if ( + codeUnit >= 0x0080 || + codeUnit === 0x002D || + codeUnit === 0x005F || + codeUnit >= 0x0030 && codeUnit <= 0x0039 || + codeUnit >= 0x0041 && codeUnit <= 0x005A || + codeUnit >= 0x0061 && codeUnit <= 0x007A + ) { + // the character itself + result += string.charAt(index); + continue; + } + + // Otherwise, the escaped character. + // http://dev.w3.org/csswg/cssom/#escape-a-character + result += '\\' + string.charAt(index); + + } + + // Return sanitized hash + return '#' + result; + + }; + + /** + * Calculate the easing pattern + * @link https://gist.github.com/gre/1650294 + * @param {String} type Easing pattern + * @param {Number} time Time animation should take to complete + * @returns {Number} + */ + var easingPattern = function (settings, time) { + var pattern; + + // Default Easing Patterns + if (settings.easing === 'easeInQuad') pattern = time * time; // accelerating from zero velocity + if (settings.easing === 'easeOutQuad') pattern = time * (2 - time); // decelerating to zero velocity + if (settings.easing === 'easeInOutQuad') pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration + if (settings.easing === 'easeInCubic') pattern = time * time * time; // accelerating from zero velocity + if (settings.easing === 'easeOutCubic') pattern = (--time) * time * time + 1; // decelerating to zero velocity + if (settings.easing === 'easeInOutCubic') pattern = time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration + if (settings.easing === 'easeInQuart') pattern = time * time * time * time; // accelerating from zero velocity + if (settings.easing === 'easeOutQuart') pattern = 1 - (--time) * time * time * time; // decelerating to zero velocity + if (settings.easing === 'easeInOutQuart') pattern = time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; // acceleration until halfway, then deceleration + if (settings.easing === 'easeInQuint') pattern = time * time * time * time * time; // accelerating from zero velocity + if (settings.easing === 'easeOutQuint') pattern = 1 + (--time) * time * time * time * time; // decelerating to zero velocity + if (settings.easing === 'easeInOutQuint') pattern = time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; // acceleration until halfway, then deceleration + + // Custom Easing Patterns + if (!!settings.customEasing) pattern = settings.customEasing(time); + + return pattern || time; // no easing, no acceleration + }; + + /** + * Determine the document's height + * @returns {Number} + */ + var getDocumentHeight = function () { + return Math.max( + document.body.scrollHeight, document.documentElement.scrollHeight, + document.body.offsetHeight, document.documentElement.offsetHeight, + document.body.clientHeight, document.documentElement.clientHeight + ); + }; + + /** + * Calculate how far to scroll + * Clip support added by robjtede - https://github.com/cferdinandi/smooth-scroll/issues/405 + * @param {Element} anchor The anchor element to scroll to + * @param {Number} headerHeight Height of a fixed header, if any + * @param {Number} offset Number of pixels by which to offset scroll + * @param {Boolean} clip If true, adjust scroll distance to prevent abrupt stops near the bottom of the page + * @returns {Number} + */ + var getEndLocation = function (anchor, headerHeight, offset, clip) { + var location = 0; + if (anchor.offsetParent) { + do { + location += anchor.offsetTop; + anchor = anchor.offsetParent; + } while (anchor); + } + location = Math.max(location - headerHeight - offset, 0); + if (clip) { + location = Math.min(location, getDocumentHeight() - window.innerHeight); + } + return location; + }; + + /** + * Get the height of the fixed header + * @param {Node} header The header + * @return {Number} The height of the header + */ + var getHeaderHeight = function (header) { + return !header ? 0 : (getHeight(header) + header.offsetTop); + }; + + /** + * Calculate the speed to use for the animation + * @param {Number} distance The distance to travel + * @param {Object} settings The plugin settings + * @return {Number} How fast to animate + */ + var getSpeed = function (distance, settings) { + var speed = settings.speedAsDuration ? settings.speed : Math.abs(distance / 1000 * settings.speed); + if (settings.durationMax && speed > settings.durationMax) return settings.durationMax; + if (settings.durationMin && speed < settings.durationMin) return settings.durationMin; + return parseInt(speed, 10); + }; + + var setHistory = function (options) { + + // Make sure this should run + if (!history.replaceState || !options.updateURL || history.state) return; + + // Get the hash to use + var hash = window.location.hash; + hash = hash ? hash : ''; + + // Set a default history + history.replaceState( + { + smoothScroll: JSON.stringify(options), + anchor: hash ? hash : window.pageYOffset + }, + document.title, + hash ? hash : window.location.href + ); + + }; + + /** + * Update the URL + * @param {Node} anchor The anchor that was scrolled to + * @param {Boolean} isNum If true, anchor is a number + * @param {Object} options Settings for Smooth Scroll + */ + var updateURL = function (anchor, isNum, options) { + + // Bail if the anchor is a number + if (isNum) return; + + // Verify that pushState is supported and the updateURL option is enabled + if (!history.pushState || !options.updateURL) return; + + // Update URL + history.pushState( + { + smoothScroll: JSON.stringify(options), + anchor: anchor.id + }, + document.title, + anchor === document.documentElement ? '#top' : '#' + anchor.id + ); + + }; + + /** + * Bring the anchored element into focus + * @param {Node} anchor The anchor element + * @param {Number} endLocation The end location to scroll to + * @param {Boolean} isNum If true, scroll is to a position rather than an element + */ + var adjustFocus = function (anchor, endLocation, isNum) { + + // Is scrolling to top of page, blur + if (anchor === 0) { + document.body.focus(); + } + + // Don't run if scrolling to a number on the page + if (isNum) return; + + // Otherwise, bring anchor element into focus + anchor.focus(); + if (document.activeElement !== anchor) { + anchor.setAttribute('tabindex', '-1'); + anchor.focus(); + anchor.style.outline = 'none'; + } + window.scrollTo(0 , endLocation); + + }; + + /** + * Emit a custom event + * @param {String} type The event type + * @param {Object} options The settings object + * @param {Node} anchor The anchor element + * @param {Node} toggle The toggle element + */ + var emitEvent = function (type, options, anchor, toggle) { + if (!options.emitEvents || typeof window.CustomEvent !== 'function') return; + var event = new CustomEvent(type, { + bubbles: true, + detail: { + anchor: anchor, + toggle: toggle + } + }); + document.dispatchEvent(event); + }; + + + // + // SmoothScroll Constructor + // + + var SmoothScroll = function (selector, options) { + + // + // Variables + // + + var smoothScroll = {}; // Object for public APIs + var settings, anchor, toggle, fixedHeader, eventTimeout, animationInterval; + + + // + // Methods + // + + /** + * Cancel a scroll-in-progress + */ + smoothScroll.cancelScroll = function (noEvent) { + cancelAnimationFrame(animationInterval); + animationInterval = null; + if (noEvent) return; + emitEvent('scrollCancel', settings); + }; + + /** + * Start/stop the scrolling animation + * @param {Node|Number} anchor The element or position to scroll to + * @param {Element} toggle The element that toggled the scroll event + * @param {Object} options + */ + smoothScroll.animateScroll = function (anchor, toggle, options) { + + // Cancel any in progress scrolls + smoothScroll.cancelScroll(); + + // Local settings + var _settings = extend(settings || defaults, options || {}); // Merge user options with defaults + + // Selectors and variables + var isNum = Object.prototype.toString.call(anchor) === '[object Number]' ? true : false; + var anchorElem = isNum || !anchor.tagName ? null : anchor; + if (!isNum && !anchorElem) return; + var startLocation = window.pageYOffset; // Current location on the page + if (_settings.header && !fixedHeader) { + // Get the fixed header if not already set + fixedHeader = document.querySelector(_settings.header); + } + var headerHeight = getHeaderHeight(fixedHeader); + var endLocation = isNum ? anchor : getEndLocation(anchorElem, headerHeight, parseInt((typeof _settings.offset === 'function' ? _settings.offset(anchor, toggle) : _settings.offset), 10), _settings.clip); // Location to scroll to + var distance = endLocation - startLocation; // distance to travel + var documentHeight = getDocumentHeight(); + var timeLapsed = 0; + var speed = getSpeed(distance, _settings); + var start, percentage, position; + + /** + * Stop the scroll animation when it reaches its target (or the bottom/top of page) + * @param {Number} position Current position on the page + * @param {Number} endLocation Scroll to location + * @param {Number} animationInterval How much to scroll on this loop + */ + var stopAnimateScroll = function (position, endLocation) { + + // Get the current location + var currentLocation = window.pageYOffset; + + // Check if the end location has been reached yet (or we've hit the end of the document) + if (position == endLocation || currentLocation == endLocation || ((startLocation < endLocation && window.innerHeight + currentLocation) >= documentHeight)) { + + // Clear the animation timer + smoothScroll.cancelScroll(true); + + // Bring the anchored element into focus + adjustFocus(anchor, endLocation, isNum); + + // Emit a custom event + emitEvent('scrollStop', _settings, anchor, toggle); + + // Reset start + start = null; + animationInterval = null; + + return true; + + } + }; + + /** + * Loop scrolling animation + */ + var loopAnimateScroll = function (timestamp) { + if (!start) { start = timestamp; } + timeLapsed += timestamp - start; + percentage = speed === 0 ? 0 : (timeLapsed / speed); + percentage = (percentage > 1) ? 1 : percentage; + position = startLocation + (distance * easingPattern(_settings, percentage)); + window.scrollTo(0, Math.floor(position)); + if (!stopAnimateScroll(position, endLocation)) { + animationInterval = window.requestAnimationFrame(loopAnimateScroll); + start = timestamp; + } + }; + + /** + * Reset position to fix weird iOS bug + * @link https://github.com/cferdinandi/smooth-scroll/issues/45 + */ + if (window.pageYOffset === 0) { + window.scrollTo(0, 0); + } + + // Update the URL + updateURL(anchor, isNum, _settings); + + // If the user prefers reduced motion, jump to location + if (reduceMotion()) { + window.scrollTo(0, Math.floor(endLocation)); + return; + } + + // Emit a custom event + emitEvent('scrollStart', _settings, anchor, toggle); + + // Start scrolling animation + smoothScroll.cancelScroll(true); + window.requestAnimationFrame(loopAnimateScroll); + + }; + + /** + * If smooth scroll element clicked, animate scroll + */ + var clickHandler = function (event) { + + // Don't run if event was canceled but still bubbled up + // By @mgreter - https://github.com/cferdinandi/smooth-scroll/pull/462/ + if (event.defaultPrevented) return; + + // Don't run if right-click or command/control + click or shift + click + if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) return; + + // Check if event.target has closest() method + // By @totegi - https://github.com/cferdinandi/smooth-scroll/pull/401/ + if (!('closest' in event.target)) return; + + // Check if a smooth scroll link was clicked + toggle = event.target.closest(selector); + if (!toggle || toggle.tagName.toLowerCase() !== 'a' || event.target.closest(settings.ignore)) return; + + // Only run if link is an anchor and points to the current page + if (toggle.hostname !== window.location.hostname || toggle.pathname !== window.location.pathname || !/#/.test(toggle.href)) return; + + // Get an escaped version of the hash + var hash; + try { + hash = escapeCharacters(decodeURIComponent(toggle.hash)); + } catch(e) { + hash = escapeCharacters(toggle.hash); + } + + // Get the anchored element + var anchor; + if (hash === '#') { + if (!settings.topOnEmptyHash) return; + anchor = document.documentElement; + } else { + anchor = document.querySelector(hash); + } + anchor = !anchor && hash === '#top' ? document.documentElement : anchor; + + // If anchored element exists, scroll to it + if (!anchor) return; + event.preventDefault(); + setHistory(settings); + smoothScroll.animateScroll(anchor, toggle); + + }; + + /** + * Animate scroll on popstate events + */ + var popstateHandler = function (event) { + + // Stop if history.state doesn't exist (ex. if clicking on a broken anchor link). + // fixes `Cannot read property 'smoothScroll' of null` error getting thrown. + if (history.state === null) return; + + // Only run if state is a popstate record for this instantiation + if (!history.state.smoothScroll || history.state.smoothScroll !== JSON.stringify(settings)) return; + + // Only run if state includes an anchor + + // if (!history.state.anchor && history.state.anchor !== 0) return; + + // Get the anchor + var anchor = history.state.anchor; + if (typeof anchor === 'string' && anchor) { + anchor = document.querySelector(escapeCharacters(history.state.anchor)); + if (!anchor) return; + } + + // Animate scroll to anchor link + smoothScroll.animateScroll(anchor, null, {updateURL: false}); + + }; + + /** + * Destroy the current initialization. + */ + smoothScroll.destroy = function () { + + // If plugin isn't already initialized, stop + if (!settings) return; + + // Remove event listeners + document.removeEventListener('click', clickHandler, false); + window.removeEventListener('popstate', popstateHandler, false); + + // Cancel any scrolls-in-progress + smoothScroll.cancelScroll(); + + // Reset variables + settings = null; + anchor = null; + toggle = null; + fixedHeader = null; + eventTimeout = null; + animationInterval = null; + + }; + + /** + * Initialize Smooth Scroll + * @param {Object} options User settings + */ + var init = function () { + + // feature test + if (!supports()) throw 'Smooth Scroll: This browser does not support the required JavaScript methods and browser APIs.'; + + // Destroy any existing initializations + smoothScroll.destroy(); + + // Selectors and variables + settings = extend(defaults, options || {}); // Merge user options with defaults + fixedHeader = settings.header ? document.querySelector(settings.header) : null; // Get the fixed header + + // When a toggle is clicked, run the click handler + document.addEventListener('click', clickHandler, false); + + // If updateURL and popState are enabled, listen for pop events + if (settings.updateURL && settings.popstate) { + window.addEventListener('popstate', popstateHandler, false); + } + + }; + + + // + // Initialize plugin + // + + init(); + + + // + // Public APIs + // + + return smoothScroll; + + }; + + return SmoothScroll; + +})); diff --git a/assets/js/tabs.js b/assets/js/tabs.js new file mode 100644 index 0000000..426532c --- /dev/null +++ b/assets/js/tabs.js @@ -0,0 +1,49 @@ +const removeActiveClasses = function (ulElement) { + const lis = ulElement.querySelectorAll("li"); + Array.prototype.forEach.call(lis, function (li) { + li.classList.remove("active"); + }); +}; + +const getChildPosition = function (element) { + var parent = element.parentNode; + var i = 0; + for (var i = 0; i < parent.children.length; i++) { + if (parent.children[i] === element) { + return i; + } + } + + throw new Error("No parent found"); +}; + +window.addEventListener("load", function () { + const tabLinks = document.querySelectorAll("ul.tab li a"); + + Array.prototype.forEach.call(tabLinks, function (link) { + link.addEventListener( + "click", + function (event) { + event.preventDefault(); + + liTab = link.parentNode; + ulTab = liTab.parentNode; + position = getChildPosition(liTab); + if (liTab.className.includes("active")) { + return; + } + + removeActiveClasses(ulTab); + tabContentId = ulTab.getAttribute("data-tab"); + tabContentElement = document.getElementById(tabContentId); + removeActiveClasses(tabContentElement); + + tabContentElement + .querySelectorAll("li") + [position].classList.add("active"); + liTab.classList.add("active"); + }, + false + ); + }); +}); diff --git a/assets/js/vendor/jquery/jquery-3.5.1.js b/assets/js/vendor/jquery/jquery-3.5.1.js new file mode 100644 index 0000000..5093733 --- /dev/null +++ b/assets/js/vendor/jquery/jquery-3.5.1.js @@ -0,0 +1,10872 @@ +/*! + * jQuery JavaScript Library v3.5.1 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2020-05-04T22:49Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + return typeof obj === "function" && typeof obj.nodeType !== "number"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var + version = "3.5.1", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.3.5 + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://js.foundation/ + * + * Date: 2020-03-14 + */ +( function( window ) { +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // Instance methods + hasOwn = ( {} ).hasOwnProperty, + arr = [], + pop = arr.pop, + pushNative = arr.push, + push = arr.push, + slice = arr.slice, + + // Use a stripped-down indexOf as it's faster than native + // https://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[ i ] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + + "ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] + // or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rhtml = /HTML$/i, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + return nonHex ? + + // Strip the backslash prefix from a non-hex escape sequence + nonHex : + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // CSS string/identifier serialization + // https://drafts.csswg.org/cssom/#common-serializing-idioms + rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, + fcssescape = function( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; + }, + { dir: "parentNode", next: "legend" } + ); + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android<4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + + // Can't trust NodeList.length + while ( ( target[ j++ ] = els[ i++ ] ) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && + + // Support: IE 8 only + // Exclude object elements + ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split( "|" ), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[ i ] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + a.sourceIndex - b.sourceIndex; + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( ( cur = cur.nextSibling ) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return ( name === "input" || name === "button" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11 + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + /* jshint -W018 */ + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + var namespace = elem.namespaceURI, + docElem = ( elem.ownerDocument || elem ).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 11, Edge + if ( subWindow.addEventListener ) { + subWindow.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( subWindow.attachEvent ) { + subWindow.attachEvent( "onunload", unloadHandler ); + } + } + + // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, + // Safari 4 - 5 only, Opera <=11.6 - 12.x only + // IE/Edge & older browsers don't support the :scope pseudo-class. + // Support: Safari 6.0 only + // Safari 6.0 supports :scope but it's an alias of :root there. + support.scope = assert( function( el ) { + docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); + return typeof el.querySelectorAll !== "undefined" && + !el.querySelectorAll( ":scope fieldset div" ).length; + } ); + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert( function( el ) { + el.className = "i"; + return !el.getAttribute( "className" ); + } ); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert( function( el ) { + el.appendChild( document.createComment( "" ) ); + return !el.getElementsByTagName( "*" ).length; + } ); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + docElem.appendChild( el ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter[ "ID" ] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find[ "ID" ] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find[ "TAG" ] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See https://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // https://bugs.jquery.com/ticket/12359 + docElem.appendChild( el ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Firefox <=3.6 - 5 only + // Old Firefox doesn't throw on a badly-escaped identifier. + el.querySelectorAll( "\\\f" ); + rbuggyQSA.push( "[\\r\\n\\f]" ); + } ); + + assert( function( el ) { + el.innerHTML = "" + + ""; + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( el.querySelectorAll( "[name=d]" ).length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE9-11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + docElem.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: Opera 10 - 11 only + // Opera 10-11 does not throw on post-comma invalid pseudos + el.querySelectorAll( "*,:x" ); + rbuggyQSA.push( ",.*:" ); + } ); + } + + if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector ) ) ) ) { + + assert( function( el ) { + + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( el, "*" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( el, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + } ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); + } : + function( a, b ) { + if ( b ) { + while ( ( b = b.parentNode ) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + // Choose the first element that is related to our preferred document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a == document || a.ownerDocument == preferredDoc && + contains( preferredDoc, a ) ) { + return -1; + } + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b == document || b.ownerDocument == preferredDoc && + contains( preferredDoc, b ) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + return a == document ? -1 : + b == document ? 1 : + /* eslint-enable eqeqeq */ + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( ( cur = cur.parentNode ) ) { + ap.unshift( cur ); + } + cur = b; + while ( ( cur = cur.parentNode ) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[ i ] === bp[ i ] ) { + i++; + } + + return i ? + + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[ i ], bp[ i ] ) : + + // Otherwise nodes in our document sort first + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + ap[ i ] == preferredDoc ? -1 : + bp[ i ] == preferredDoc ? 1 : + /* eslint-enable eqeqeq */ + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( support.matchesSelector && documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; +}; + +Sizzle.escape = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + // If no nodeType, this is expected to be an array + while ( ( node = elem[ i++ ] ) ) { + + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || + match[ 5 ] || "" ).replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + + ")" + className + "(" + whitespace + "|$)" ) ) && classCache( + className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + /* eslint-disable max-len */ + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + /* eslint-enable max-len */ + + }; + }, + + "CHILD": function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + // Use previously-cached element index if available + if ( useCache ) { + + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + + // Use the same loop as above to seek `elem` from the start + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + ( outerCache[ node.uniqueID ] = {} ); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + // Potentially complex pseudos + "not": markFunction( function( selector ) { + + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + // Don't keep the element (issue #299) + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + "has": markFunction( function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + } ), + + "contains": markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; + }; + } ), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + + // lang value must be a valid identifier + if ( !ridentifier.test( lang || "" ) ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && + ( !document.hasFocus || document.hasFocus() ) && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + // Boolean properties + "enabled": createDisabledPseudo( false ), + "disabled": createDisabledPseudo( true ), + + "checked": function( elem ) { + + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return ( nodeName === "input" && !!elem.checked ) || + ( nodeName === "option" && !!elem.selected ); + }, + + "selected": function( elem ) { + + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos[ "empty" ]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo( function() { + return [ 0 ]; + } ), + + "last": createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + "even": createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "odd": createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? + argument + length : + argument > length ? + length : + argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rcombinators.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrim, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || + ( outerCache[ elem.uniqueID ] = {} ); + + if ( skip && skip === elem.nodeName.toLowerCase() ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = uniqueCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( + selector || "*", + context.nodeType ? [ context ] : context, + [] + ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + // Restore matcherIn since elem is not yet a final match + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens + .slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), + + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + + // They will have gone through all possible matchers + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( + selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) + ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find[ "ID" ]( token.matches[ 0 ] + .replace( runescape, funescape ), context ) || [] )[ 0 ]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + // Abort if we hit a combinator + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + // Search, expanding context for leading sibling combinators + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || + context + ) ) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert( function( el ) { + + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert( function( el ) { + el.innerHTML = ""; + return el.firstChild.getAttribute( "href" ) === "#"; +} ) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + } ); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert( function( el ) { + el.innerHTML = ""; + el.firstChild.setAttribute( "value", "" ); + return el.firstChild.getAttribute( "value" ) === ""; +} ) ) { + addHandle( "value", function( elem, _name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + } ); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert( function( el ) { + return el.getAttribute( "disabled" ) == null; +} ) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + ( val = elem.getAttributeNode( name ) ) && val.specified ? + val.value : + null; + } + } ); +} + +return Sizzle; + +} )( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; +jQuery.escapeSelector = Sizzle.escape; + + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +}; +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + // Support: IE 11+ + // elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.stackTrace ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the stack, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getStackHook ) { + process.stackTrace = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the master Deferred + master = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + master.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( master.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return master.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); + } + + return master.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, stack ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (#9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + return result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (#504, #13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + + which: function( event ) { + var button = event.button; + + // Add which for key events + if ( event.which == null && rkeyEvent.test( event.type ) ) { + return event.charCode != null ? event.charCode : event.keyCode; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { + if ( button & 1 ) { + return 1; + } + + if ( button & 2 ) { + return 3; + } + + if ( button & 4 ) { + return 2; + } + + return 0; + } + + return event.which; + } +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + delegateType: delegateType + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // Support: IE <=10 - 11, Edge 12 - 13 only + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (#15098, #14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 + // Some styles come back with percentage values, even though they shouldn't + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (#8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px"; + tr.style.height = "1px"; + trChild.style.height = "9px"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, #12537) + // .css('--customProperty) (#3144) + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || + + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + // Normalize "" and auto + val = parseFloat( val ) || 0; + + // Adjust for the element's box model + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "animationIterationCount": true, + "columnCount": true, + "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, + "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (#7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug #9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (#7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 15 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) > -1 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + return this.each( function() { + var className, i, self, classNames; + + if ( isValidValue ) { + + // Toggle individual class names + i = 0; + self = jQuery( this ); + classNames = classesToArray( value ); + + while ( ( className = classNames[ i++ ] ) ) { + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (#14686, #14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( + dataPriv.get( cur, "events" ) || Object.create( null ) + )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +// Support: Firefox <=44 +// Firefox doesn't have focus(in | out) events +// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +// +// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 +// focus(in | out) events fire after focus & blur events, +// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order +// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 +if ( !support.focusin ) { + jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + dataPriv.remove( doc, fix ); + + } else { + dataPriv.access( doc, fix, attaches ); + } + } + }; + } ); +} +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) { + xml = undefined; + } + + if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; +}; + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ) + .filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ) + .map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 15 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Use a noop converter for missing script + if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { + s.converters[ "text script" ] = function() {}; + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (#11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( " + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 使用 Jekyll + minimal-mistakes 在 Github pages 上架設自己的部落格 - Jekyll x Minimal-Mistakes x GitHub pages + + +

+ +

原來架設 Blog 也能如此輕鬆簡單 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 3 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

為什麼寫 Blog 文章

+ +

工作上受到了很多教學網站及 Blog 文章非常多的幫助,因此想架設一個自己的 Blog,將工作及生活學到的技術知識記錄下來,幫助自己複習,也希望能幫助到有需要的人。

+ +

為什麼選用 Jekyll + minimal-mistakes

+ +

選用 Jekyll 的原因

+ +
    +
  1. 可用 Markdown 語法寫文章,Jekyll 會將 markdown 轉成 html 檔案
  2. +
  3. 有非常豐富的主題
  4. +
  5. 可客製化
  6. +
+ +

選用 Minimal-mistakes 主題的原因

+ +
    +
  1. 較多人使用,Github 上 star 有 9k
  2. +
  3. 支援 Dark skin
  4. +
  5. 可以放大圖,跟 Medium 類似
  6. +
+ +

其實原本我是用 Octopress ,後來因為 Octopress 已不再維護,且主題沒有 Jekyll 來得豐富,最後決定使用 Jekyll 重新架一個 Blog

+ +
+

Octopress

+
+ +

廢話不多說,現在讓我們一起用 Jekyll 在 Github pages 上架設一個專屬 Blog 吧!

+ +

為什麼選 GitHub Pages

+ +

因為免費不用花錢租一台機器,也不需買 domain 跟 SSL,GitHub Pages 通通幫你搞定,當然之後要換自己買的 domain 跟 SSL 也是做得到

+ +

事前準備

+ +

申請 Github 帳號

+ +

安裝 Git

+ +
+

做版本控管

+
+ +
brew install git
+git --version       #檢查版本
+
+ +

安裝 rbenv

+ +
+

管理 ruby 版本

+
+ +
brew install rbenv
+rbenv init
+# 依照你使用的 Shell,如果是 bash 請替換為 ~/.bashrc
+echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.zshrc
+echo 'eval "$(rbenv init -)"' >> ~/.zshrc
+rbenv -v        #檢查版本
+
+ +

安裝 ruby

+ +
rbenv install 3.0.0
+rbenv global 3.0.0
+rbenv rehash        # Rehash rbenv shims (run this after installing executables)
+
+ +

rbenv -v 檢查版本

+ +

檢查 RubyGems

+ +
+

Gem 套件管理服務,讓你能立即地發佈及安裝你的 Gem 套件,裝 ruby 時會一起安裝

+
+ +
gem update --system
+gem -v
+
+ +

檢查 GCC / Make

+ +
gcc -v
+g++ -v
+make -v
+
+ +

建立 Jekyll Blog 網站

+ +
+

這邊我是參考 GitHub 官網教學,如果英文可以的朋友可以直接參考 GitHub 的教學 Creating a GitHub Pages site with Jekyll

+
+ +

打開終端機,cd 到要建立 Blog 的目錄

+ +
cd PARENT-FOLDER
+
+ +

創建一個 Git repo 目錄,REPOSITORY-NAME 替換為你 GitHub repo 想取的名字 (e.g. blog)

+ +
git init REPOSITORY-NAME
+
+ +

cd 到 repo 目錄

+ +
cd REPOSITORY-NAME
+
+ +

決定你 GitHub Pages 網站發佈抓取 source 的方式 Publishing sources for GitHub Pages sites

+ +
    +
  1. 用 gh-pages branch 當發佈 source
  2. +
  3. 用任何分支上的 /docs folder 作為發佈 source
  4. +
+ +

這邊我選擇使用1. 用 gh-pages branch 當發佈 source

+ +

創建 gh-pages branch

+ +
git checkout --orphan gh-pages
+# Creates a new branch, with no history or contents, called gh-pages and switches to the gh-pages branch
+
+ +

創建一個新的 Jekyll site

+ +
jekyll new --skip-bundle .      # --skip-bundle : jekyll 創建時先不執行 `bundle install` 安裝 gem 套件
+
+ +

打開 Gemfile,在 gem “jekyll” 前面加上 “#”,不安裝 jekyll +加入 github-pages gem

+ +
gem "github-pages", "~> GITHUB-PAGES-VERSION", group: :jekyll_plugins
+
+
+ +
+

請將 GITHUB-PAGES-VERSION 替換成 github-pages 最後支援版本,請參考 Dependency versions

+
+ +

存檔 Gemfile,執行 bundle install

+ +
+

安裝 Gemfile 中的 gem 套件

+
+ +
bundle install
+
+ +

修改 _config.yml

+ +
domain: my-site.github.io       # if you want to force HTTPS, specify the domain without the http at the start, e.g. example.com
+url: https://my-site.github.io  # the base hostname and protocol for your site, e.g. http://example.com
+baseurl: /REPOSITORY-NAME/      # place folder name if the site is served in a subfolder
+
+ +

加入 webrick gem

+ +
bundle add webrick
+
+ +

Fix favicon.ico not found issue

+ +
touch favicon.ico
+
+ +

在 local 測試 Jekyll Blog 網站

+ +

打開終端機執行

+ +
bundle install
+bundle exec jekyll serve
+
+ +

點擊 Server address: http://localhost:4000 預覽你的網站

+ +

create_a_new_repo_on_github

+ +

恭喜,你已經完成了你的第一個 Jekyll 網站 🎉🎉🎉🎉🎉 +但我們只能在 local 欣賞,不能分享給朋友同事看,接上來我們將他推上去 GitHub,讓 GitHub Pages 幫我們建置 Blog 吧!

+ +

上傳 GitHub 讓 GitHub Pages 自動建置 Blog

+ +

首先要到 GitHub 網站創建一個 repository,請使用 Public 專案,名字隨便你取 (e.g. blog)

+ +

create_a_new_repo_on_github

+ +

複製 repo 的 URL 等等會用到

+ +

先回到終端機將我們的測試過的 code commit

+ +
git add .
+git commit -m "[feature] Initial GitHub pages site with Jekyll"
+
+ +

將剛剛複製的 repo URL 加入 git remote URL

+ +
git remote add origin https://github.com/USER/REPOSITORY.git
+
+ +

將 code 推上 GitHub

+ +
git push -u origin BRANCH   # 因為上面是用 gh-pages 作為 source,這邊就直接將 BRANCH 換為 gh-pages 推上 GitHub 即可
+
+ +

回到 GitHub 網頁 -> Settings -> Pages 點擊上面的網址 https://USERNAME.github.io/

+ +

jekyll_github_pages

+ +

恭喜,你已經成功將 Jekyll 網站推上 GitHub Pages,現在全世界都看得到你的 Blog 了 🎉🎉🎉🎉🎉

+ +

但我們的 Blog 目前還非常的陽春,讓我們為 Blog 套上主題美化吧!

+ +

加入 Minimal-mistakes 主題

+ +
+

這邊我是參考 minimal-mistakes 官網教學,如果英文可以的朋友可以直接參考 minimal-mistakes 的教學 Quick-Start Guide

+
+ +

這邊 minimal-mistakes 作者提供了三種方式來安裝主題

+ +
    +
  1. Gem-based method
  2. +
  3. Remote theme method
  4. +
  5. Fork
  6. +
+ +

GitHub Pages 很適合第 2 種方式,所以這邊我們選擇第二種安裝方式 +打開 Gemfile 並加入下面的 code

+ +
source "https://rubygems.org"
+
+gem "github-pages", group: :jekyll_plugins
+gem "jekyll-include-cache", group: :jekyll_plugins
+
+ +

打開 _config.yml 找到 plugins 並加入 jekyll-include-cache plugin

+ +

抓取及更新 bundle 的 gems 套件

+ +
bundle
+
+ +

打開 _config.yml 加入 remote_theme: "mmistakes/minimal-mistakes@4.24.0",並移除其他 theme:remote_theme

+ +

編輯 _config.yml,這邊我是複製 Minimal Mistakes/index.html,並保留之前修改過的 domain, url, baseurl

+ +
    +
  • 將 index.md 內容替換成 Minimal Mistakes/index.md
  • +
  • 打開_posts/0000-00-00-welcome-to-jekyll.markdown,將layout: post改成layout: single
  • +
  • 刪除 about.md
  • +
+ +

好了,在測試一下結果如何

+ +
bundle install
+bundle exec jekyll serve
+
+ +

jekyll_with_minimal_mistakes_theme_local_test

+ +

恭喜,你已經成功套用 Minimal-mistakes 主題進你的 Jekyll Blog 了 🎉🎉🎉🎉🎉

+ +

最後我們將 code commit 推上 GitHub

+ +
git add .
+git commit -m "[feature] Add Minimal-mistakes theme to Jekyll"
+git push origin gh-pages
+
+ +

jekyll_with_minimal_mistakes_theme_github_pages

+ +

Done! 🎉🎉🎉🎉🎉

+ +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/how-to-add-your-jekyll-blog-website-to-google-search-console/index.html b/blog/how-to-add-your-jekyll-blog-website-to-google-search-console/index.html new file mode 100644 index 0000000..96d47bf --- /dev/null +++ b/blog/how-to-add-your-jekyll-blog-website-to-google-search-console/index.html @@ -0,0 +1,863 @@ + + + + + + +如何使 Jekyll website 能被 Google 搜索到 - Google x Search x Blog - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 如何使 Jekyll website 能被 Google 搜索到 - Google x Search x Blog + + +

+ +

Google 大大,你把我的網頁藏哪去了? +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

在 Google 搜尋不到我的 Blog

+ +

當我們在建立完 Jekyll Blog 後,會發現竟然 Google 不到我們的 Blog 網站,別擔心這是因為我們尚未將網站提交加入 Google Search Console 中

+ +

將網站加入 Google Search Console

+ +

進入 Google Search Console

+ +

這邊使用網址前綴方式添加,加你的 Blog domain 填入並繼續

+ +

google_search_console

+ +

下載 googlexxxxxxxxxx.html 檔案放到 Jekyll 根目錄,commit 上傳到 GitHub 上,點擊驗證

+ +

google_search_console_verify

+ +

驗證成功 👍

+ +

google_search_console_verify

+ +

提交 Sitemap

+ +

「Sitemap」是一種用來提供網站資訊的檔案,您可以在其中列出網頁、影片和其他檔案的資訊,並呈現這些內容彼此間的關係。Google 等搜尋引擎都會讀取網站的 Sitemap 檔案,藉此以更有效率的方式檢索網站。

+ +
+

瞭解 Sitemap

+
+ +

打開 Gemfilegroup :jekyll_plugins 中加入 jekyll-sitemap

+ +
group :jekyll_plugins do
+  gem "jekyll-sitemap"
+end
+
+ +

打開 _config.ymlplugins 加入 jekyll-sitemap

+ +
plugins:
+  - jekyll-sitemap
+
+ +

執行 bundle install 安裝 jekyll-sitemap 並在 local 測試一下,在 _site/ 資料夾中會出現 sitemap.xml 文件,代表 jekyll-sitemap 有順利產生 sitemap

+ +
bundle install
+bundle exec jekyll serve
+
+ +

將 code commit 推上 GitHub,回到 Google Search Console 提交 sitemap

+ +

google_search_console

+ +

完成,等待下次 Google Search Engine 下更新檢索就能搜尋到我們 Blog 的內容囉! 🙌

+ +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/how-to-add-your-octopress-blog-website-to-google-search-console/index.html b/blog/how-to-add-your-octopress-blog-website-to-google-search-console/index.html new file mode 100644 index 0000000..8093937 --- /dev/null +++ b/blog/how-to-add-your-octopress-blog-website-to-google-search-console/index.html @@ -0,0 +1,831 @@ + + + + + + +如何使 Octopress website 能被 Google 搜索到 - Google x Search x Blog - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 如何使 Octopress website 能被 Google 搜索到 - Google x Search x Blog + + +

+ +

Google 大大,你把我的網頁藏哪去了? +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

在 Google 搜尋不到我的 Blog

+ +

當我們在建立完 Jekyll Blog 後,會發現竟然 Google 不到我們的 Blog 網站,別擔心這是因為我們尚未將網站提交加入 Google Search Console 中

+ +

將網站加入 Google Search Console

+ +

進入 Google Search Console

+ +

這邊使用網址前綴方式添加,加你的 Blog domain 填入並繼續

+ +

google_search_console

+ +

下載 googlexxxxxxxxxx.html 檔案放到 octopress/source 目錄下,commit 上傳到 GitHub 上,點擊驗證

+ +
rake gen_deploy
+
+ +

google_search_console_verify

+ +

驗證成功 👍

+ +

google_search_console_verify

+ +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog/octopress-setup/index.html b/blog/octopress-setup/index.html new file mode 100644 index 0000000..b1d6aa1 --- /dev/null +++ b/blog/octopress-setup/index.html @@ -0,0 +1,956 @@ + + + + + + +使用 Octopress 架設靜態 Blog - Octopress x GitHub pages x Blog - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 使用 Octopress 架設靜態 Blog - Octopress x GitHub pages x Blog + + +

+ +

原來架設 Blog 也能如此輕鬆簡單 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

為什麼要寫 Blog?

+ +

工作中受到了很多教學網站及博客文章(簡書, CSDN, Medium…)非常多的幫助,因此想寫部落格將自己學到的技術記錄下來,一方面讓自己複習,一方面也希望能幫助到有需要的人。

+ +

為什麼用 Octopress?

+ +

其實有很多現成免費的部落格平台像是 Medium, Blogger 等…,但最後我還是決定用 Octopress 來建立自己的部落格,它吸引我的原因如下:

+ +
    +
  1. 使用 Git 做版本控管,託管於 Github
  2. +
  3. 使用 Markdown 寫文章
  4. +
  5. 可以學到東西
  6. +
  7. 免費
  8. +
+ +

Git 對軟體工程師再熟悉不過了,且託管於工程師最愛充滿開源專案的 Github 平台,用 Markdown 寫文章,也能訓練自己寫 README 語法的熟練程度,又可以學到東西,因此選擇 Octopress。

+ +

事前準備

+ + + +
brew install git
+
+ + + +
brew install ruby
+
+ +

確認版本

+ +
ruby --version
+
+ +

建置 Octopress

+ +
git clone git://github.com/imathis/octopress.git octopress
+cd octopress
+
+ +

安裝依賴

+ +
gem install bundler
+rbenv rehash    # If you use rbenv, rehash to be able to run the bundle command
+bundle install
+
+ +

安裝 Octopress 預設主題

+ +
rake install
+
+ +

Github page

+ +

Github page提供我們免費架設靜態網站,雖然有些限制(Ex: 沒有 DB, Server 配置等…),但非常簡易就能部署,非常適合用來做個人部落格。

+ +
    +
  1. Github申請一個帳號
  2. +
  3. 建立一個 repository,命名為[username].github.io,[username]為你 Github 的用戶名,請務必要用此格式命名,不然後面會無法部署。
  4. +
+ +

完成後會看到一個 ssh URL

+ +
git@github.com:username/username.github.io.git
+
+ +

,這就是你的遠端 repository 位置

+ +

部署到 Github page

+ +
    +
  • 設置 Github page repo 位置
  • +
+ +
rake setup_github_pages
+
+ +

此時會要你輸入 github page 的位置,還記得上面建立完成後所產生的 ssh URL,複製並輸入

+ +
git@github.com:username/username.github.io.git
+
+ +
    +
  • 產生及部署部落格
  • +
+ +
rake generate
+rake deploy
+
+ +

上面指令會產生出你的部落格,並把產生的檔案複製到_deploy/資料匣中,並把它們加入 git 中,commit 及 push 到 master branch。

+ +

到這裡你可以先在 Browser 打開 http://username.github.io/,就可以看到你的部落格囉!

+ +

最後別忘記將 source commit 並 push 到遠端的 repo

+ +
git add .
+git commit -m 'your message'
+git push origin source
+
+ +

發布文章

+ +

所有的文章必須放在source/_posts目錄下

+ +
    +
  • 創建新文章
  • +
+ +
rake new_post["title"]
+
+ +

此時在source/_posts目錄下會產生一個YYYY-MM-DD-post-title.markdown的檔案,我們就可以打開檔案開始寫部落格了

+ +
cd source/_posts/
+vim YYYY-MM-DD-post-title.markdown
+
+ +

也可用其他編輯器開啟寫文章,像是: MacDown等…

+ +

寫完文章後再執行前面提到的產生及部署部落格

+ +
rake generate
+rake deploy
+
+ +

or

+ +
rake gen_deploy
+
+ +

最後再將剛所寫的文章加入 commit,並 push 到遠端的 repo

+ +
git add .
+git commit -m 'your message'
+git push origin source
+
+ +

octopress_github_pages

+ +

總結

+ +

Octopress 基礎建置就到這邊,未來如果有時間會再深入研究更多 Octopress 的新功能,寫成文章分享給大家!

+ +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/aws/index.html b/categories/aws/index.html new file mode 100644 index 0000000..0662296 --- /dev/null +++ b/categories/aws/index.html @@ -0,0 +1,324 @@ + + + + + + +AWS - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

AWS

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/blog/index.html b/categories/blog/index.html new file mode 100644 index 0000000..4905856 --- /dev/null +++ b/categories/blog/index.html @@ -0,0 +1,465 @@ + + + + + + +Blog - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Blog

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/cryptography/index.html b/categories/cryptography/index.html new file mode 100644 index 0000000..1fab7af --- /dev/null +++ b/categories/cryptography/index.html @@ -0,0 +1,324 @@ + + + + + + +Cryptography - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Cryptography

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/design-pattern/index.html b/categories/design-pattern/index.html new file mode 100644 index 0000000..b353771 --- /dev/null +++ b/categories/design-pattern/index.html @@ -0,0 +1,1452 @@ + + + + + + +Design Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Design Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (25) - Strategy Pattern (策略模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

策略模式提供了一種靈活的解決方案,讓系統能根據需求動態切換不同的行為邏輯,實現高可擴展性與低耦合性。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (24) - State Pattern (狀態模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過狀態模式,設計一個飲水機的運作機制,根據不同狀態執行加熱、冷卻或待機的行為。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (23) - Observer Pattern (觀察者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過觀察者模式,實現安全系統主機的警報通知機制,當警報觸發時,主機自動通知平板、iOS 和 Android 手機。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (22) - Memento Pattern (備忘錄模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解備忘錄模式如何幫助我們實現狀態恢復,像是常見的 Ctrl+Z 功能,讓我們回到之前的操作狀態。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (20) - Iterator Pattern (迭代器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解迭代器模式如何提供一種順序來訪問集合內元素的方法,而不需要暴露集合的底層表示。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (17) - Proxy Pattern (代理模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解代理模式如何通過控制對物件的訪問來提升系統的安全性、效能及靈活性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (15) - Facade Pattern (外觀模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索外觀模式如何簡化系統複雜性,提供一個統一的介面來訪問子系統的功能,提升程式碼的可讀性與維護性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (13) - Composite Pattern (組合模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解組合模式如何以一致的方式操作單個物件與物件集合,實現對樹狀結構的靈活管理。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (12) - Bridge Pattern (橋接模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解橋接模式如何解耦抽象與實現,打造更靈活且易於擴展的系統設計,滿足複雜需求的同時降低維護成本。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (11) - Adapter Pattern (轉接器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解如何使用轉接器模式來解決介面不兼容問題,讓不同類別無縫合作,增強程式設計靈活性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (9) - Prototype Pattern (原型模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入原型模式:探索如何透過物件複製技術,有效提升軟體開發中的資源管理與設計模式的靈活性。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (8) - Builder Pattern (建造者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索建造者模式,學習如何分步構建複雜對象,使程式碼更加靈活和易於維護。通過實例展示如何使用建造者模式簡化對象創建過程,提升程式碼的可讀性和可擴展性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (4) - UML (統一建模語言) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解UML,學習如何用UML圖清晰展現設計模式,提升軟體設計能力。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (2) - Design Principles (設計原則) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

學習如何透過單一職責和開放封閉等設計原則提升程式碼質量,打造靈活、可維護的軟體系統。 +

+
+
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/devops/index.html b/categories/devops/index.html new file mode 100644 index 0000000..b60a795 --- /dev/null +++ b/categories/devops/index.html @@ -0,0 +1,512 @@ + + + + + + +DevOps - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

DevOps

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/google/index.html b/categories/google/index.html new file mode 100644 index 0000000..7d5bdd3 --- /dev/null +++ b/categories/google/index.html @@ -0,0 +1,324 @@ + + + + + + +Google - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Google

+ + + + + + +
+
+ +

+ + Google AdSense + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

如何透過 Google AdSense 爲我們的網站加入廣告賺取收益 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 0000000..aee448f --- /dev/null +++ b/categories/index.html @@ -0,0 +1,4257 @@ + + + + + + +Posts by Category - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + + + +
+ +

Posts by Category

+ + + + + + + +
    + + + + + + + + + + + + + +
  • + + Design Pattern 25 + +
  • + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  • + + DevOps 5 + +
  • + + + + + + + + + + + + + +
  • + + Blog 4 + +
  • + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  • + + P2P 3 + +
  • + + + + + +
  • + + Tools 3 + +
  • + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  • + + Mobile 1 + +
  • + + + + + +
  • + + AWS 1 + +
  • + + + + + + + +
  • + + Pay 1 + +
  • + + + +
  • + + Technology 1 + +
  • + + + + + +
  • + + Cryptography 1 + +
  • + + + +
  • + + OpenSSH 1 + +
  • + + + +
  • + + Security 1 + +
  • + + + +
  • + + Google 1 + +
  • + + + +
+ + + + + + + + + + + + + + + +
+

Design Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (25) - Strategy Pattern (策略模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

策略模式提供了一種靈活的解決方案,讓系統能根據需求動態切換不同的行為邏輯,實現高可擴展性與低耦合性。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (24) - State Pattern (狀態模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過狀態模式,設計一個飲水機的運作機制,根據不同狀態執行加熱、冷卻或待機的行為。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (23) - Observer Pattern (觀察者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過觀察者模式,實現安全系統主機的警報通知機制,當警報觸發時,主機自動通知平板、iOS 和 Android 手機。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (22) - Memento Pattern (備忘錄模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解備忘錄模式如何幫助我們實現狀態恢復,像是常見的 Ctrl+Z 功能,讓我們回到之前的操作狀態。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (20) - Iterator Pattern (迭代器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解迭代器模式如何提供一種順序來訪問集合內元素的方法,而不需要暴露集合的底層表示。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (17) - Proxy Pattern (代理模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解代理模式如何通過控制對物件的訪問來提升系統的安全性、效能及靈活性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (15) - Facade Pattern (外觀模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索外觀模式如何簡化系統複雜性,提供一個統一的介面來訪問子系統的功能,提升程式碼的可讀性與維護性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (13) - Composite Pattern (組合模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解組合模式如何以一致的方式操作單個物件與物件集合,實現對樹狀結構的靈活管理。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (12) - Bridge Pattern (橋接模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解橋接模式如何解耦抽象與實現,打造更靈活且易於擴展的系統設計,滿足複雜需求的同時降低維護成本。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (11) - Adapter Pattern (轉接器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解如何使用轉接器模式來解決介面不兼容問題,讓不同類別無縫合作,增強程式設計靈活性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (9) - Prototype Pattern (原型模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入原型模式:探索如何透過物件複製技術,有效提升軟體開發中的資源管理與設計模式的靈活性。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (8) - Builder Pattern (建造者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索建造者模式,學習如何分步構建複雜對象,使程式碼更加靈活和易於維護。通過實例展示如何使用建造者模式簡化對象創建過程,提升程式碼的可讀性和可擴展性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (4) - UML (統一建模語言) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解UML,學習如何用UML圖清晰展現設計模式,提升軟體設計能力。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (2) - Design Principles (設計原則) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

學習如何透過單一職責和開放封閉等設計原則提升程式碼質量,打造靈活、可維護的軟體系統。 +

+
+
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

DevOps

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + +
+

Blog

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

P2P

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + +
+

Tools

+
+ + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Mobile

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + +
+

AWS

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + +
+

Pay

+
+ + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Technology

+
+ + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+ Back to top ↑ +
+ + + + + +
+

Cryptography

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

OpenSSH

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Security

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Google

+
+ + + + + +
+
+ +

+ + Google AdSense + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

如何透過 Google AdSense 爲我們的網站加入廣告賺取收益 +

+
+
+ + +
+ Back to top ↑ +
+ + + + +
+
+ +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/mobile/index.html b/categories/mobile/index.html new file mode 100644 index 0000000..c3353bc --- /dev/null +++ b/categories/mobile/index.html @@ -0,0 +1,324 @@ + + + + + + +Mobile - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Mobile

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/openssh/index.html b/categories/openssh/index.html new file mode 100644 index 0000000..5cf42ba --- /dev/null +++ b/categories/openssh/index.html @@ -0,0 +1,324 @@ + + + + + + +OpenSSH - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

OpenSSH

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/p2p/index.html b/categories/p2p/index.html new file mode 100644 index 0000000..b0b7f62 --- /dev/null +++ b/categories/p2p/index.html @@ -0,0 +1,418 @@ + + + + + + +P2P - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

P2P

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/pay/index.html b/categories/pay/index.html new file mode 100644 index 0000000..6aacce3 --- /dev/null +++ b/categories/pay/index.html @@ -0,0 +1,324 @@ + + + + + + +Pay - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Pay

+ + + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/security/index.html b/categories/security/index.html new file mode 100644 index 0000000..6073bb4 --- /dev/null +++ b/categories/security/index.html @@ -0,0 +1,324 @@ + + + + + + +Security - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Security

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/technology/index.html b/categories/technology/index.html new file mode 100644 index 0000000..ae32c63 --- /dev/null +++ b/categories/technology/index.html @@ -0,0 +1,324 @@ + + + + + + +Technology - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Technology

+ + + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/categories/tools/index.html b/categories/tools/index.html new file mode 100644 index 0000000..6f8c0ef --- /dev/null +++ b/categories/tools/index.html @@ -0,0 +1,418 @@ + + + + + + +Tools - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Tools

+ + + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cryptography/openssh/security/how-to-enable-rsa-encryption-algorithm-key-in-openssh-8.8/index.html b/cryptography/openssh/security/how-to-enable-rsa-encryption-algorithm-key-in-openssh-8.8/index.html new file mode 100644 index 0000000..f15bf49 --- /dev/null +++ b/cryptography/openssh/security/how-to-enable-rsa-encryption-algorithm-key-in-openssh-8.8/index.html @@ -0,0 +1,1009 @@ + + + + + + +How to Enable RSA Encryption Algorithm Key in OpenSSH 8.8 - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + How to Enable RSA Encryption Algorithm Key in OpenSSH 8.8 + + +

+ +

如何在 OpenSSH 8.8 中重新啟用 RSA 加密支援,確保可以繼續使用 RSA 金鑰。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 3 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

最近在修改公司的 jenkins CI/CD 架構,Dockerize 我們的 Android Building Environment。 +在我啟動 Debian 12 container 來建構我的環境時,配置 RSA Key 到公司的 GitLab server,卻無法抓取 source code,故有了這篇文章希望未來有遇到可以拿出來快速解決問題,也希望幫助到遇到一樣問題的人。

+ +

經過分析後才發現,原來 Debian 12 預設使用 OpenSSH 8.8,而 OpenSSH 8.8 預設將 RSA 加密算法關閉,因為安全是以及過時的問題 +但公司 GitLab server 較舊,目前只支援較舊的 ssh Key 演算法 RSA,因此需要找方法使其支援。

+ +

準備作業

+ +
+

如果你已經有環境,可以跳過此段落

+
+ +

首先我先用 Docker 啟一台 Debian 12 的 container 如下

+ +
> docker pull debian:bookworm
+bookworm: Pulling from library/debian
+9c5ed83eaf5c: Pull complete
+Digest: sha256:45f2e735295654f13e3be10da2a6892c708f71a71be845818f6058982761a6d3
+Status: Downloaded newer image for debian:bookworm
+docker.io/library/debian:bookworm
+
+ +
> docker run -it --name debian-bookworm-for-test-openssh debian:bookworm
+root@b5336f2395fd:/#
+
+ +

更新 apt

+ +
> apt update
+Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
+Get:2 http://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]
+Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
+Get:4 http://deb.debian.org/debian bookworm/main arm64 Packages [8688 kB]
+Get:5 http://deb.debian.org/debian bookworm-updates/main arm64 Packages [13.7 kB]
+Get:6 http://deb.debian.org/debian-security bookworm-security/main arm64 Packages [166 kB]
+Fetched 9122 kB in 2s (4556 kB/s)
+Reading package lists... Done
+Building dependency tree... Done
+Reading state information... Done
+All packages are up to date.
+
+ +

安裝 Git

+ +
> apt install git
+
+ +

確定 openSSH 版本

+ +

可以查看 openSSH 版本是否 > 8.8

+ +
> ssh -V
+OpenSSH_9.2p1 Debian-2+deb12u3, OpenSSL 3.0.13 30 Jan 2024
+
+ +

建立 SSH RSA Key

+ +

建立 Key

+ +
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
+
+ +

將 ssh key 加入 ssh-agent

+ +
> eval "$(ssh-agent -s)"
+Agent pid 3160
+> ssh-add ~/.ssh/id_rsa
+Identity added: /root/.ssh/id_rsa (your_email@example.com)
+
+ +

將 Public Key 上傳到 Git server

+ +

cat 出 public key

+ +
> cat ~/.ssh/id_rsa.pub
+
+ +

上傳到你的 Git Server

+ +

測試 SSH 連線

+ +

x.x.x.x 請換成你的 git server IP or Domain

+ +
ssh -T -v git@x.x.x.x
+OpenSSH_9.2p1 Debian-2+deb12u3, OpenSSL 3.0.13 30 Jan 2024
+debug1: Reading configuration data /etc/ssh/ssh_config
+debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files
+debug1: /etc/ssh/ssh_config line 21: Applying options for *
+debug1: Connecting to x.x.x.x [x.x.x.x] port 22.
+debug1: Connection established.
+debug1: identity file /root/.ssh/id_rsa type 0
+debug1: identity file /root/.ssh/id_rsa-cert type -1
+debug1: identity file /root/.ssh/id_ecdsa type -1
+debug1: identity file /root/.ssh/id_ecdsa-cert type -1
+debug1: identity file /root/.ssh/id_ecdsa_sk type -1
+debug1: identity file /root/.ssh/id_ecdsa_sk-cert type -1
+debug1: identity file /root/.ssh/id_ed25519 type -1
+debug1: identity file /root/.ssh/id_ed25519-cert type -1
+debug1: identity file /root/.ssh/id_ed25519_sk type -1
+debug1: identity file /root/.ssh/id_ed25519_sk-cert type -1
+debug1: identity file /root/.ssh/id_xmss type -1
+debug1: identity file /root/.ssh/id_xmss-cert type -1
+debug1: identity file /root/.ssh/id_dsa type -1
+debug1: identity file /root/.ssh/id_dsa-cert type -1
+debug1: Local version string SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u3
+debug1: Remote protocol version 2.0, remote software version OpenSSH_6.0p1 Debian-4+deb7u4
+debug1: compat_banner: match: OpenSSH_6.0p1 Debian-4+deb7u4 pat OpenSSH* compat 0x04000000
+debug1: Authenticating to x.x.x.x:22 as 'git'
+debug1: load_hostkeys: fopen /root/.ssh/known_hosts2: No such file or directory
+debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory
+debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory
+debug1: SSH2_MSG_KEXINIT sent
+debug1: SSH2_MSG_KEXINIT received
+debug1: kex: algorithm: ecdh-sha2-nistp256
+debug1: kex: host key algorithm: ecdsa-sha2-nistp256
+debug1: kex: server->client cipher: aes128-ctr MAC: umac-64@openssh.com compression: none
+debug1: kex: client->server cipher: aes128-ctr MAC: umac-64@openssh.com compression: none
+debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
+debug1: SSH2_MSG_KEX_ECDH_REPLY received
+debug1: Server host key: ecdsa-sha2-nistp256 SHA256:PTKJPZC0PPuujGp7jIZvu/PyNzUg4aj4pNF0Nlgi290
+debug1: load_hostkeys: fopen /root/.ssh/known_hosts2: No such file or directory
+debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts: No such file or directory
+debug1: load_hostkeys: fopen /etc/ssh/ssh_known_hosts2: No such file or directory
+debug1: Host 'x.x.x.x' is known and matches the ECDSA host key.
+debug1: Found key in /root/.ssh/known_hosts:1
+debug1: rekey out after 4294967296 blocks
+debug1: SSH2_MSG_NEWKEYS sent
+debug1: expecting SSH2_MSG_NEWKEYS
+debug1: SSH2_MSG_NEWKEYS received
+debug1: rekey in after 4294967296 blocks
+debug1: get_agent_identities: bound agent to hostkey
+debug1: get_agent_identities: ssh_fetch_identitylist: agent contains no identities
+debug1: Will attempt key: /root/.ssh/id_rsa RSA SHA256:IPjmgGepFVyKiqZF74LyLzcywh2Qx4AcWerAw2tXqfU
+debug1: Will attempt key: /root/.ssh/id_ecdsa
+debug1: Will attempt key: /root/.ssh/id_ecdsa_sk
+debug1: Will attempt key: /root/.ssh/id_ed25519
+debug1: Will attempt key: /root/.ssh/id_ed25519_sk
+debug1: Will attempt key: /root/.ssh/id_xmss
+debug1: Will attempt key: /root/.ssh/id_dsa
+debug1: SSH2_MSG_SERVICE_ACCEPT received
+debug1: Authentications that can continue: publickey,password
+debug1: Next authentication method: publickey
+debug1: Offering public key: /root/.ssh/id_rsa RSA SHA256:IPjmgGepFVyKiqZF74LyLzcywh2Qx4AcWerAw2tXqfU
+debug1: send_pubkey_test: no mutual signature algorithm
+debug1: Trying private key: /root/.ssh/id_ecdsa
+debug1: Trying private key: /root/.ssh/id_ecdsa_sk
+debug1: Trying private key: /root/.ssh/id_ed25519
+debug1: Trying private key: /root/.ssh/id_ed25519_sk
+debug1: Trying private key: /root/.ssh/id_xmss
+debug1: Trying private key: /root/.ssh/id_dsa
+debug1: Next authentication method: password
+git@x.x.x.x's password:
+
+ +
+

debug1: send_pubkey_test: no mutual signature algorithm +發現原因是 ssh client 和 server 之間沒有共同的簽名算法引起的,導致公鑰驗證失敗

+
+ +

解決方法

+ +

打開 openSSH 8.8 release document 會發現提到這一段

+ +
+

Incompatibility is more likely when connecting to older SSH +implementations that have not been upgraded or have not closely tracked +improvements in the SSH protocol. For these cases, it may be necessary +to selectively re-enable RSA/SHA1 to allow connection and/or user +authentication via the HostkeyAlgorithms and PubkeyAcceptedAlgorithms +options. For example, the following stanza in ~/.ssh/config will enable +RSA/SHA1 for host and user authentication for a single destination host:

+
+ +
Host old-host
+  HostkeyAlgorithms +ssh-rsa
+  PubkeyAcceptedAlgorithms +ssh-rsa
+
+ +

開啟 ~/.ssh/config 檔案並加上如下 config

+ +
Host x.x.x.x
+  HostkeyAlgorithms +ssh-rsa
+  PubkeyAcceptedAlgorithms +ssh-rsa
+
+ +

再跑一次 ssh -T -v git@x.x.x.x 就會發現成功了 🎉

+ +

參考

+ + + + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-1-object-oriented-concepts/index.html b/design pattern/design-pattern-1-object-oriented-concepts/index.html new file mode 100644 index 0000000..1ac2b15 --- /dev/null +++ b/design pattern/design-pattern-1-object-oriented-concepts/index.html @@ -0,0 +1,869 @@ + + + + + + +Design Pattern (1) - Object-Oriented Concepts (物件導向概念) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (1) - Object-Oriented Concepts (物件導向概念) + + +

+ +

探索封裝、繼承、多態和抽象的力量,為理解複雜設計模式奠定基礎。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

Object-Oriented Concepts 物件導向概念

+ +

物件導向設計的四大核心概念,為設計模式的理解奠定基礎。讓我們一同簡單探索這些概念。

+ +

Encapsulation 封裝

+ +

封裝是將屬性及方法的實作細節隱藏在類別中,只暴露必要的方法給使用者,保護內部屬性和方法不被隨意修改。

+ +
+

就像開車時,我們只需知道踩油門會加速,踩煞車會停止,而不必了解馬達、電瓶、發動機等原理。這些細節都被封裝在引擎蓋下。

+
+ +

Inheritance 繼承

+ +

繼承使子類別可以繼承父類別的屬性和方法,達到程式碼重複使用的目的。

+ +
+

在自然界中,狗與貓都是動物,都能呼吸和行動;花草和樹木都是植物,都能進行光合作用。這就是繼承的概念。

+
+ +

Polymorphism 多型

+ +

多型為不同的類別提供統一的介面或抽象類別,以操作不同的實體物件。

+ +
+

以 iPhone 6S 為例,不論是由台積電還是三星代工的晶片,使用者拿到的 iPhone 6S 功能都一樣。這就是多型。

+
+ +

Abstraction 抽象

+ +

抽象通過類型或接口隱藏實作細節,只提供必要的功能給使用者。

+ +
+

我們在手機上安裝的 App 是對各種應用程式的抽象名稱;去市場買水果,不論是蘋果還是香蕉,水果也是一種抽象名稱。這就是抽象。

+
+ +

總結

+ +

理解了物件導向的核心概念後,我們將進一步探討這些概念如何引導我們進入設計原則的世界。物件導向概念為我們提供了創建模塊化、可重用和易於維護程式碼的基礎,而設計原則則教我們如何有效地應用這些概念來解決更複雜的設計問題。接下來,我們將探討這些原則,並了解它們如何幫助我們實現高質量的軟體設計。

+ +

design_pattern_design_principle_architecture

+ +
+

Object-Oriented Concepts -> Design Principle -> Design Pattern

+
+ +

參考

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長 🙂

+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-10-singleton-pattern/index.html b/design pattern/design-pattern-10-singleton-pattern/index.html new file mode 100644 index 0000000..7f6afaa --- /dev/null +++ b/design pattern/design-pattern-10-singleton-pattern/index.html @@ -0,0 +1,926 @@ + + + + + + +Design Pattern (10) - Singleton Pattern (單例模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (10) - Singleton Pattern (單例模式) + + +

+ +

深入單例模式:如何確保一個類別只有一個實體,提供一個全域 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們收到了一個需求:開發一個應用程式,該應用程式需要與資料庫進行頻繁的交互。為了確保資料庫連接的效率和資源的合理使用,我們需要設計一個系統來管理資料庫連接。

+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

design_pattern_singleton_pattern_uml_1

+ +

我們有 CRUD 四個 function 以及 constructor 用來建立 DatabaseClient

+ +

察覺 Forces

+ +

來看看上面這樣的設計會有哪些問題

+ +
    +
  1. 資源管理:多個資料庫連接會消耗大量資源,導致性能下降。
  2. +
  3. 一致性:需要確保所有資料庫操作使用相同的連接,以避免數據不一致。
  4. +
  5. 效率:頻繁創建和銷毀資料庫連接會降低系統效率。
  6. +
+ +

套用 Singleton Pattern ( Solution ) 得到新的 Context ( Resulting Context )

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Singleton Pattern 解決這個問題

+ +

先來看一下 Singleton Pattern 的 UML

+ +

design_pattern_singleton_pattern_uml_2

+ +

我們可以發現,單例模式其實就是透過 getInstance() 方法去取得實體,而每次取的時都會去判斷內部 property instance 是否為 null,如果是 null 就創建一個新的,如果不是就回傳 instance property 的值,如此就能保證此 class 的實體只會有一個。

+ +

我們來將 DatabaseClient 套用 Singleton Pattern

+ +

design_pattern_singleton_pattern_uml_3

+ +

如此我們就得到了一個全新的 Resulting Context

+ +

物件導向程式設計 (OOP)

+ +

再來我們就可以開始進行物件導向程式開發

+ +

[DatabaseClient]

+ +
class DatabaseClient {
+
+    fun create(tableName:String, data: Map<String, Any>): Int {
+        return 0
+    }
+
+    fun read(tableName:String, conditions: Map<String, Any>): Int {
+        return 0
+    }
+
+    fun update(tableName:String, data: Map<String, Any>, conditions: Map<String, Any>): Int {
+        return 0
+    }
+
+    fun delete(tableName:String, conditions: Map<String, Any>): Int {
+        return 0
+    }
+
+    companion object {
+        var mInstance: DatabaseClient? = null
+        fun getInstance(): DatabaseClient {
+            if (mInstance == null) {
+                mInstance = DatabaseClient()
+            }
+            return mInstance!!
+        }
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    val db = DatabaseClient.getInstance()
+    db.create("test", mapOf(Pair("test", "123")))
+}
+
+ +

這樣就完成了,這邊稍微提一下,其實 kotlin 語言有提供 object 來讓我們輕鬆實作 Singleton Pattern,如下

+ +

[DatabaseClient]

+ +
object DatabaseClient {
+
+    fun create(tableName:String, data: Map<String, Any>): Int {
+        return 0
+    }
+
+    fun read(tableName:String, conditions: Map<String, Any>): Int {
+        return 0
+    }
+
+    fun update(tableName:String, data: Map<String, Any>, conditions: Map<String, Any>): Int {
+        return 0
+    }
+
+    fun delete(tableName:String, conditions: Map<String, Any>): Int {
+        return 0
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    val db = DatabaseClient
+    db.create("test", mapOf(Pair("test", "123")))
+}
+
+ +

如此就能更簡單的操作 Singleton 的單例類別了!

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-11-adapter-pattern/index.html b/design pattern/design-pattern-11-adapter-pattern/index.html new file mode 100644 index 0000000..4bb4ee3 --- /dev/null +++ b/design pattern/design-pattern-11-adapter-pattern/index.html @@ -0,0 +1,942 @@ + + + + + + +Design Pattern (11) - Adapter Pattern (轉接器模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (11) - Adapter Pattern (轉接器模式) + + +

+ +

了解如何使用轉接器模式來解決介面不兼容問題,讓不同類別無縫合作,增強程式設計靈活性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們收到了一個需求:公司現有的 股票數據系統 使用 XML 格式 存儲與傳遞數據,而新引入的 第三方股票分析系統 僅支援 JSON 格式。為了整合兩個系統,我們需要設計一個解決方案,使得現有數據可以被第三方分析系統正確接收和處理。

+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

adapter_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,上述程式碼可以運行,但存在以下問題:

+ +
    +
  1. +

    責任分散

    + +
      +
    • Client 負責數據轉換,這違反了單一職責原則(SRP)。
    • +
    • 一旦轉換邏輯變複雜,Client 的程式碼將變得難以維護。
    • +
    +
  2. +
  3. +

    重複性高

    + +
      +
    • 如果其他系統需要同樣的轉換邏輯,程式碼將無法重複使用,導致重複性問題。
    • +
    +
  4. +
  5. +

    耦合性高

    + +
      +
    • Client 必須了解 XmlStockDataJsonAnalyzer 的具體實現細節,導致高耦合性。
    • +
    • 未來若資料來源或目標格式改變,Client 必須大幅修改。
    • +
    +
  6. +
  7. +

    無法適應變化

    +
      +
    • 若引入更多資料格式(如 CSV 或 YAML),每種格式都需要在 Client 中實現轉換邏輯,難以擴展。
    • +
    +
  8. +
+ +

套用 Adapter Pattern ( Solution ) 得到新的 Context ( Resulting Context )

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Adapter Pattern 解決這個問題

+ +

先來看一下 Adapter Pattern 的 UML

+ +

adapter_pattern_uml_2

+ +
    +
  • +

    Target (目標介面): +定義客戶端需要的介面。 +本例中是 JsonAnalyzer 的方法 analyzeJsonData,目標是處理 JSON 格式的股票數據。

    +
  • +
  • +

    Adaptee (被轉接者): +定義現有的不相容介面。本例中是 XmlStockData,它提供 XML 格式的股票數據。

    +
  • +
  • +

    Adapter (轉接器): +實現 Target 的介面,並內部持有 Adaptee。負責將 Adaptee 的數據轉換為 Target 所需的格式。本例中是 StockDataAdapter,它將 XML 格式的數據轉換為 JSON。

    +
  • +
+ +

將 Adapter Pattern 套用到我們的應用吧

+ +

adapter_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[JsonAnalyzer]

+ +
interface JsonAnalyzer {
+    fun analyzeJsonData(json: String)
+}
+
+ +

[XmlStockData]

+ +
class XmlStockData {
+    fun getXmlData(): String {
+        return """
+            <stocks>
+                <stock>
+                    <symbol>TSLA</symbol>
+                    <price>675.50</price>
+                </stock>
+                <stock>
+                    <symbol>AMZN</symbol>
+                    <price>3201.65</price>
+                </stock>
+            </stocks>
+        """
+    }
+}
+
+ +

[StockDataAdapter]

+ +
class StockDataAdapter(private val xmlStockData: XmlStockData) : JsonAnalyzer {
+    override fun analyzeJsonData(json: String) {
+        println("Analyzing JSON data: $json")
+    }
+
+    fun convertAndAnalyze() {
+        val xml = xmlStockData.getXmlData()
+        val json = XML.toJSONObject(xml).toString(4)
+        analyzeJsonData(json)
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    val xmlStockData = XmlStockData()
+    val adapter = StockDataAdapter(xmlStockData)
+
+    adapter.convertAndAnalyze()
+}
+
+ +

結論

+ +

套用 Adapter Pattern 後,我們成功將數據轉換的邏輯封裝到轉接器中,並減少了 Client 的責任。這樣的設計提高了系統的靈活性和可維護性,同時為未來擴展新格式提供了便利。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-12-bridge-pattern/index.html b/design pattern/design-pattern-12-bridge-pattern/index.html new file mode 100644 index 0000000..8355b71 --- /dev/null +++ b/design pattern/design-pattern-12-bridge-pattern/index.html @@ -0,0 +1,980 @@ + + + + + + +Design Pattern (12) - Bridge Pattern (橋接模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (12) - Bridge Pattern (橋接模式) + + +

+ +

深入了解橋接模式如何解耦抽象與實現,打造更靈活且易於擴展的系統設計,滿足複雜需求的同時降低維護成本。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們收到了一個需求:公司現有的 保全系統,在偵測到不同類型的事件(如火警、竊盜警鈴)時,需要以多種通知方式向用戶發送警報訊息。支援的通知方式包括:

+ +
    +
  • APNS (Apple iOS Push)
  • +
  • FCM (Google Android Push)
  • +
  • Email
  • +
  • SMS
  • +
+ +

警報事件則可能包括:

+ +
    +
  • Fire (火警)
  • +
  • Burglar (竊盜警鈴)
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

bridge_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,上述程式碼可以運行,但存在以下問題:

+ +
    +
  1. 高耦合性 (Tight Coupling):
  2. +
+ +
    +
  • 警報類型 和 通知方式 被緊密地耦合在一起,這使得每次新增警報類型或通知方式時,都必須在多個類別中進行修改。
  • +
  • 系統的維護成本較高,每個新需求都可能導致代碼的重構。
  • +
+ +
    +
  1. 難以擴展 (Difficulty in Extending):
  2. +
+ +
    +
  • 每增加一種新的警報類型或通知方式,都需要在每個組合中創建新的類別,導致代碼增長迅速。
  • +
  • 如果需求變更(例如新增一種新的通知方式或警報類型),則需要修改大量的程式碼。
  • +
+ +
    +
  1. 重複代碼 (Code Duplication):
  2. +
+ +
    +
  • 由於每一種通知方式與警報事件的組合都需要實現一個具體的類別,導致了大量重複代碼,增加了程式碼維護的困難。
  • +
+ +
    +
  1. 靈活性差 (Lack of Flexibility):
  2. +
+ +
    +
  • 當某一層次(例如警報事件類型或通知方式)需要進行修改時,必須修改多個相關類別,這樣的設計使得系統的變動成本高。
  • +
+ +

套用 Bridge Pattern ( Solution ) 得到新的 Context ( Resulting Context )

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Bridge Pattern 解決這個問題

+ +

先來看一下 Bridge Pattern 的 UML

+ +

bridge_pattern_uml_2

+ +
    +
  • Abstraction (抽象層):定義通知功能,負責使用具體的消息發送方式來發送通知。
  • +
  • RefinedAbstraction (具體化的抽象層):擴展抽象層,實現不同類型的警報通知,例如火警通知或竊盜警鈴通知。
  • +
  • Implementor (實作層):定義消息發送的接口,負責處理具體的消息發送邏輯。
  • +
  • ConcreteImplementor (具體的實作層):提供具體的消息發送實作,例如 APNS、FCM、Email、SMS。
  • +
+ +

將 Bridge Pattern 套用到我們的應用吧

+ +

bridge_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[Abstraction: AlarmNotification]

+ +
abstract class AlarmNotification(sender: MessageSender) {
+    protected var sender: MessageSender
+
+    init {
+        this.sender = sender
+    }
+
+    abstract fun notifyUser(details: String?)
+}
+
+ +

[RefinedAbstraction: FireAlarmNotification and BurglarAlarmNotification]

+ +
class FireAlarmNotification(sender: MessageSender) : AlarmNotification(sender) {
+    override fun notifyUser(details: String?) {
+        sender.sendMessage("Fire Alarm: $details")
+    }
+}
+
+class BurglarAlarmNotification(sender: MessageSender) : AlarmNotification(sender) {
+    override fun notifyUser(details: String?) {
+        sender.sendMessage("Theft Alarm: $details")
+    }
+}
+
+ +

[Implementor: MessageSender]

+ +
interface MessageSender {
+    fun sendMessage(message: String?)
+}
+
+ +

[ConcreteImplementor: APNSSender, FCMSender, EmailSender, and SMSSender]

+ +
class APNSSender : MessageSender {
+    override fun sendMessage(message: String?) {
+        println("Sending APNS Notification: $message")
+    }
+}
+
+class FCMSender : MessageSender {
+    override fun sendMessage(message: String?) {
+        println("Sending FCM Notification: $message")
+    }
+}
+
+class EmailSender : MessageSender {
+    override fun sendMessage(message: String?) {
+        println("Sending Email: $message")
+    }
+}
+
+class SMSSender : MessageSender {
+    override fun sendMessage(message: String?) {
+        println("Sending SMS: $message")
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    // Sending Fire Alarm via APNS
+    val fireAPNS: AlarmNotification = FireAlarmNotification(APNSSender())
+    fireAPNS.notifyUser("Smoke detected in Zone 1.")
+
+    // Sending Burglar Alarm via FCM
+    val burglarFCM: AlarmNotification = BurglarAlarmNotification(FCMSender())
+    burglarFCM.notifyUser("Unauthorized access detected at Main Door.")
+
+    // Sending Fire Alarm via Email
+    val fireEmail: AlarmNotification = FireAlarmNotification(EmailSender())
+    fireEmail.notifyUser("Temperature exceeds threshold in Zone 3.")
+
+    // Sending Burglar Alarm via SMS
+    val burglarSMS: AlarmNotification = BurglarAlarmNotification(SMSSender())
+    burglarSMS.notifyUser("Motion detected in Warehouse.")
+}
+
+ +

結論

+ +

通過套用 Bridge Pattern,我們成功將通知的抽象層與實際的消息發送方式進行了分離,這樣一來,每種警報通知類型和發送方式可以獨立演進,並且能夠輕鬆地新增新型的通知方式或警報類型。這不僅提高了程式的靈活性,還減少了維護的難度,當需求變更時,也能夠更輕鬆地應對擴展需求。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-13-composite-pattern/index.html b/design pattern/design-pattern-13-composite-pattern/index.html new file mode 100644 index 0000000..e70fae0 --- /dev/null +++ b/design pattern/design-pattern-13-composite-pattern/index.html @@ -0,0 +1,963 @@ + + + + + + +Design Pattern (13) - Composite Pattern (組合模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (13) - Composite Pattern (組合模式) + + +

+ +

深入了解組合模式如何以一致的方式操作單個物件與物件集合,實現對樹狀結構的靈活管理。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們收到了一個需求:實作一個檔案系統,其目錄可以包含檔案或子目錄,並且需要提供統一的操作介面來列出目錄內容。此系統應支援以下功能:

+ +
    +
  • 支援樹狀結構的表示。
  • +
  • 可操作單一檔案和目錄。
  • +
  • 新增檔案或目錄時無需大幅修改現有程式碼。
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

composite_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,上述需求可能會遇到以下問題:

+ +
    +
  1. 高耦合性 (Tight Coupling): +
      +
    • 單一檔案和目錄集合的操作邏輯分散在多個類別中,導致系統維護困難。
    • +
    +
  2. +
  3. +

    重複代碼 (Code Duplication)

    + +
      +
    • 每次操作目錄內容時,需分別處理檔案與子目錄,導致相似邏輯多處重複。
    • +
    +
  4. +
  5. +

    難以擴展 (Difficulty in Extending)

    + +
      +
    • 新增檔案或目錄類型時,需大幅修改程式碼,影響系統穩定性。
    • +
    +
  6. +
  7. 靈活性差 (Lack of Flexibility): +
      +
    • 操作層需清楚區分單一檔案與目錄集合,增加程式碼複雜度。
    • +
    +
  8. +
+ +

套用 Composite Pattern ( Solution ) 得到新的 Context ( Resulting Context )

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Composite Pattern 解決這個問題。

+ +

先來看一下 Composite Pattern 的 UML:

+ +

composite_pattern_uml_2

+ +
    +
  • Component (組件介面):定義統一的操作介面,單一檔案與目錄都需實作此介面。
  • +
  • Leaf (葉子節點):表示檔案,不能再包含子節點。
  • +
  • Composite (組合節點):表示目錄,可包含子節點(檔案或子目錄),並實作遞迴操作的邏輯。
  • +
+ +

將 Composite Pattern 套用到我們的應用吧

+ +

composite_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[Component: FileSystemComponent]

+ +
abstract class FileSystemComponent(val name: String) {
+    open fun display(indent: String = "") {
+        println("$indent$name")
+    }
+
+    open fun add(component: FileSystemComponent) {
+        throw UnsupportedOperationException("Cannot add component to a leaf.")
+    }
+
+    open fun remove(component: FileSystemComponent) {
+        throw UnsupportedOperationException("Cannot remove component from a leaf.")
+    }
+}
+
+ +

[Leaf: File]

+ +
class File(name: String) : FileSystemComponent(name) {
+    override fun display(indent: String) {
+        println("$indent- File: $name")
+    }
+}
+
+ +

[Composite: Directory]

+ +
class Directory(name: String) : FileSystemComponent(name) {
+    private val children = mutableListOf<FileSystemComponent>()
+
+    override fun add(component: FileSystemComponent) {
+        children.add(component)
+    }
+
+    override fun remove(component: FileSystemComponent) {
+        children.remove(component)
+    }
+
+    override fun display(indent: String) {
+        println("$indent+ Directory: $name")
+        children.forEach { it.display("$indent  ") }
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    // Build Directories and files
+    val root = Directory("Root")
+    val folder1 = Directory("Folder1")
+    val folder2 = Directory("Folder2")
+
+    val file1 = File("File1.txt")
+    val file2 = File("File2.txt")
+    val file3 = File("File3.txt")
+
+    // Add files & directories into directories
+    root.add(folder1)
+    root.add(file1)
+
+    folder1.add(folder2)
+    folder1.add(file2)
+
+    folder2.add(file3)
+
+    // display file structure
+    root.display()
+}
+
+ +

[Output]

+ +
+ Directory: Root
+  + Directory: Folder1
+    + Directory: Folder2
+      - File: File3.txt
+    - File: File2.txt
+  - File: File1.txt
+
+ +

結論

+ +

通過套用 Composite Pattern,我們成功實現了單一檔案與目錄集合的統一操作。有效降低了系統的耦合性,並且提供了高效的擴展性,當需要新增新的檔案類型或目錄結構時,無需大幅修改現有程式碼。透過此模式,開發者能夠以簡潔且一致的方式處理樹狀結構的邏輯,提升了程式的靈活性與可維護性。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-14-decorator-pattern/index.html b/design pattern/design-pattern-14-decorator-pattern/index.html new file mode 100644 index 0000000..fc1af19 --- /dev/null +++ b/design pattern/design-pattern-14-decorator-pattern/index.html @@ -0,0 +1,963 @@ + + + + + + +Design Pattern (14) - Decorator Pattern (裝飾者模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (14) - Decorator Pattern (裝飾者模式) + + +

+ +

深入了解裝飾者模式如何動態為物件增加功能,同時保持系統的靈活性與開放性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們收到了一個需求:咖啡店的 POS 系統需要計算不同咖啡及其附加選項(如牛奶、糖漿、奶泡等)的價格。具體需求如下:

+ +
    +
  • 咖啡種類包括基本款的 Espresso 和 House Blend。
  • +
  • 每種咖啡都可以加不同的附加項,例如牛奶、巧克力糖漿、奶泡。
  • +
  • 系統應該支持動態組合不同的附加項,而不需要針對每種組合定義類別。
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

decorator_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,上述需求可能會遇到以下問題:

+ +
    +
  1. +

    類別爆炸 (Class Explosion)

    + +
      +
    • 為每一種咖啡及其附加選項組合創建類別,導致類別數量迅速增長。
    • +
    +
  2. +
  3. +

    高耦合性 (Tight Coupling)

    + +
      +
    • 咖啡與附加選項緊密耦合,修改某一部分時可能影響整體。
    • +
    +
  4. +
  5. +

    靈活性差 (Lack of Flexibility)

    + +
      +
    • 系統無法動態地添加或移除附加選項,只能依賴預先定義的組合。
    • +
    +
  6. +
  7. +

    重複代碼 (Code Duplication)

    +
      +
    • 每種組合的實作邏輯重複,導致維護困難。
    • +
    +
  8. +
+ +

套用 Decorator Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Decorator Pattern 解決這個問題。

+ +

先來看一下 Decorator Pattern 的 UML:

+ +

decorator_pattern_uml_2

+ +
    +
  • Component (組件介面):定義基本的行為或功能。
  • +
  • ConcreteComponent (具體組件):實作基本功能的具體類別,例如基本咖啡。
  • +
  • Decorator (裝飾者介面):維護對 Component 的引用,並在此基礎上添加新功能。
  • +
  • ConcreteDecorator (具體裝飾者):實作新增的功能,例如牛奶、糖漿等附加選項。
  • +
+ +

將 Decorator Pattern 套用到我們的應用吧

+ +

decorator_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[Component: Beverage]

+ +
interface Beverage {
+    val description: String
+    fun cost(): Double
+}
+
+ +

[ConcreteComponent: Espresso and HouseBlend]

+ +
class Espresso : Beverage {
+    override val description = "Espresso"
+    override fun cost() = 1.99
+}
+
+class HouseBlend : Beverage {
+    override val description = "House Blend Coffee"
+    override fun cost() = 0.89
+}
+
+ +

[Decorator: CondimentDecorator]

+ +
abstract class CondimentDecorator(protected val beverage: Beverage) : Beverage() {
+    override abstract val description: String
+}
+
+ +

[ConcreteDecorator: Milk, Mocha, and Whip]

+ +
class Milk(beverage: Beverage) : CondimentDecorator(beverage) {
+    override val description = "${beverage.description}, Milk"
+    override fun cost() = beverage.cost() + 0.3
+}
+
+class ChocolateSyrup(beverage: Beverage) : CondimentDecorator(beverage) {
+    override val description = "${beverage.description}, Chocolate Syrup"
+    override fun cost() = beverage.cost() + 0.5
+}
+
+class WhippedCream(beverage: Beverage) : CondimentDecorator(beverage) {
+    override val description = "${beverage.description}, Whipped Cream"
+    override fun cost() = beverage.cost() + 0.4
+}
+
+ +

[Client]

+ +
fun main() {
+    // Make an Espresso
+    val espresso = Espresso()
+    println("${espresso.description}: $${espresso.cost()}")
+
+    // Make an Espresso with Milk、Chocolate Syrup and Whipped Cream
+    val customBeverage = WhippedCream(
+        ChocolateSyrup(
+            Milk(Espresso())
+        )
+    )
+    println("${customBeverage.description}: $${customBeverage.cost()}")
+
+    // Make an HouseBlend with Milk and double Whipped Cream
+    val layeredBeverage = WhippedCream(
+        WhippedCream(
+            Milk(HouseBlend())
+        )
+    )
+    println("${layeredBeverage.description}: $${layeredBeverage.cost()}")
+}
+
+ +

[Output]

+ +
Espresso: $1.99
+Espresso, Milk, Chocolate Syrup, Whipped Cream: $3.19
+House Blend, Milk, Whipped Cream, Whipped Cream: $2.49
+
+ +

結論

+ +

通過套用 Decorator Pattern,我們成功動態地為物件添加新功能,並保持類別數量的可控性。此模式不僅提供了靈活的擴展方式,還減少了重複代碼和耦合性,使得系統更易於維護和擴展。透過裝飾者模式,開發者可以更高效地應對需求變更,滿足複雜功能的實現。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-15-facade-pattern/index.html b/design pattern/design-pattern-15-facade-pattern/index.html new file mode 100644 index 0000000..42005d0 --- /dev/null +++ b/design pattern/design-pattern-15-facade-pattern/index.html @@ -0,0 +1,963 @@ + + + + + + +Design Pattern (15) - Facade Pattern (外觀模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (15) - Facade Pattern (外觀模式) + + +

+ +

探索外觀模式如何簡化系統複雜性,提供一個統一的介面來訪問子系統的功能,提升程式碼的可讀性與維護性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

假設我們正在開發一個家庭影院系統,該系統包含以下子系統:

+ +
    +
  • DVD 播放器
  • +
  • 環繞音響
  • +
  • 燈光
  • +
  • 投影機
  • +
+ +

用戶希望能輕鬆開啟或關閉家庭影院的所有功能,而不需要逐一操作各個設備。

+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

facade_pattern_uml_1

+ +

察覺 Forces

+ +

在設計階段,我們注意到以下設計難題:

+ +
    +
  1. +

    子系統過於複雜:需要多個步驟才能完成操作。

    +
  2. +
  3. +

    操作繁瑣:用戶需要熟悉每個子系統的細節。

    +
  4. +
  5. +

    缺乏一致性:不同子系統之間的操作方式可能不同,導致混亂。

    +
  6. +
+ +

這些 Forces 驅使我們採用外觀模式來簡化介面,減少系統的操作複雜度。

+ +

套用 Facade Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Facade Pattern 解決這個問題。

+ +

先來看一下 Facade Pattern 的 UML:

+ +

facade_pattern_uml_2

+ +
    +
  • Subsystems (子系統):表示系統中的一組類別或模組,它們各自負責不同的功能。例如,在家庭影院系統中,包括 DVDPlayer、SurroundSound、Lights 和 Projector 等子系統。
  • +
  • Facade (外觀類別):提供一個簡化的介面來封裝子系統的複雜性。它負責協調多個子系統,以完成用戶的一個請求。例如,HomeTheaterFacade 提供 watchMovie() 和 endMovie() 方法來簡化對子系統的操作。
  • +
+ +

將 Facade Pattern 套用到我們的應用吧

+ +

facade_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[Subsystems]

+ +
class DVDPlayer {
+    fun on() = println("DVD Player is ON")
+    fun play() = println("DVD Player is playing")
+    fun off() = println("DVD Player is OFF")
+}
+
+class SurroundSound {
+    fun on() = println("Surround Sound is ON")
+    fun setVolume(level: Int) = println("Surround Sound volume set to $level")
+    fun off() = println("Surround Sound is OFF")
+}
+
+class Lights {
+    fun dim(level: Int) = println("Lights dimmed to $level%")
+    fun on() = println("Lights are ON")
+}
+
+class Projector {
+    fun on() = println("Projector is ON")
+    fun setMode(mode: String) = println("Projector set to $mode mode")
+    fun off() = println("Projector is OFF")
+}
+
+ +

[Facade: HomeTheaterFacade]

+ +
class HomeTheaterFacade(
+    private val dvdPlayer: DVDPlayer,
+    private val surroundSound: SurroundSound,
+    private val lights: Lights,
+    private val projector: Projector
+) {
+    fun watchMovie() {
+        println("Get ready to watch a movie...")
+        lights.dim(10)
+        projector.on()
+        projector.setMode("Cinema")
+        surroundSound.on()
+        surroundSound.setVolume(5)
+        dvdPlayer.on()
+        dvdPlayer.play()
+    }
+
+    fun endMovie() {
+        println("Shutting down the home theater...")
+        dvdPlayer.off()
+        surroundSound.off()
+        projector.off()
+        lights.on()
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    val dvdPlayer = DVDPlayer()
+    val surroundSound = SurroundSound()
+    val lights = Lights()
+    val projector = Projector()
+
+    val homeTheater = HomeTheaterFacade(dvdPlayer, surroundSound, lights, projector)
+
+    // The Start
+    homeTheater.watchMovie()
+
+    println()
+
+    // The End
+    homeTheater.endMovie()
+}
+
+ +

[Output]

+ +
Get ready to watch a movie...
+Lights dimmed to 10%
+Projector is ON
+Projector set to Cinema mode
+Surround Sound is ON
+Surround Sound volume set to 5
+DVD Player is ON
+DVD Player is playing
+
+Shutting down the home theater...
+DVD Player is OFF
+Surround Sound is OFF
+Projector is OFF
+Lights are ON
+
+ +

結論

+ +

外觀模式通過為複雜系統提供一個簡單的介面,降低了系統的操作成本,提升了用戶體驗。它特別適用於子系統較多且操作繁瑣的情境。藉由使用外觀模式,開發者能夠更專注於系統核心邏輯,同時提升程式碼的可維護性與擴展性。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-16-flyweight-pattern/index.html b/design pattern/design-pattern-16-flyweight-pattern/index.html new file mode 100644 index 0000000..42653c3 --- /dev/null +++ b/design pattern/design-pattern-16-flyweight-pattern/index.html @@ -0,0 +1,946 @@ + + + + + + +Design Pattern (16) - Flyweight Pattern (享元模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (16) - Flyweight Pattern (享元模式) + + +

+ +

探索享元模式如何透過共享技術有效減少記憶體使用,提升應用效能。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

假設我們正在開發一個森林場景的渲染系統,該系統需要顯示數百棵甚至數千棵樹木。

+ +

每棵樹包含兩類資料:

+ +
    +
  1. 內部狀態 (Intrinsic State):不隨環境改變的資料,例如樹的種類、顏色、紋理等。
  2. +
  3. 外部狀態 (Extrinsic State):因環境而異的資料,例如樹的座標 (x, y)。
  4. +
+ +

如果為每棵樹都建立完整的物件,將導致記憶體消耗過大。因此,我們需要一種共享內部狀態的方式來優化記

+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

flyweight_pattern_uml_1

+ +

察覺 Forces

+ +

在設計階段,我們注意到以下設計難題:

+ +
    +
  1. 大量重複資料:每棵樹都包含相同的種類、顏色和紋理資料。
  2. +
  3. 性能問題:對於數千棵樹的場景渲染,過多的物件會導致記憶體不足或性能瓶頸。
  4. +
  5. 共享與獨立的平衡:如何在共享資料的同時,保留每棵樹的獨立外部狀態。
  6. +
+ +

為解決這些問題,我們採用了享元模式。

+ +

套用 Flyweight Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Flyweight Pattern 解決這個問題。

+ +

先來看一下 flyweight Pattern 的 UML:

+ +

flyweight_pattern_uml_2

+ +
    +
  • Flyweight (享元介面):定義共享物件的操作。
  • +
  • ConcreteFlyweight (具體享元類別):實作共享物件的功能,儲存可以共享的狀態。
  • +
  • FlyweightFactory (享元工廠):用於創建和管理共享物件,確保相同的物件只創建一次。
  • +
  • Client (客戶端):使用享元物件,並管理不能共享的狀態。
  • +
+ +

將 flyweight Pattern 套用到我們的應用吧

+ +

flyweight_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[FFlyweight: Tree & TreeType (樹類別)]

+ +
class Tree(
+    private val x: Int,
+    private val y: Int,
+    private val type: TreeType
+) {
+    fun draw() {
+        type.draw(x, y)
+    }
+}
+
+class TreeType(
+    val name: String,
+    val color: String,
+    val texture: String
+) {
+    fun draw(x: Int, y: Int) {
+        println("Drawing tree: $name, color: $color, texture: $texture at ($x, $y)")
+    }
+}
+
+ +

[FlyweightFactory: TreeFactory (樹工廠類別)]

+ +
object TreeFactory {
+    private val treeTypes = mutableMapOf<String, TreeType>()
+
+    fun getTreeType(name: String, color: String, texture: String): TreeType {
+        return treeTypes.computeIfAbsent(name) {
+            println("Creating new TreeType: $name")
+            TreeType(name, color, texture)
+        }
+    }
+}
+
+ +

[Client: Forest (森林類別)]

+ +
class Forest {
+    private val trees = mutableListOf<Tree>()
+
+    fun plantTree(x: Int, y: Int, name: String, color: String, texture: String) {
+        val treeType = TreeFactory.getTreeType(name, color, texture)
+        val tree = Tree(x, y, treeType)
+        trees.add(tree)
+    }
+
+    fun draw() {
+        for (tree in trees) {
+            tree.draw()
+        }
+    }
+}
+
+ +

[Main Function]

+ +
fun main() {
+    val forest = Forest()
+
+    // Planting trees in the forest
+    forest.plantTree(10, 20, "Oak", "Green", "Rough")
+    forest.plantTree(15, 25, "Pine", "Dark Green", "Smooth")
+    forest.plantTree(10, 20, "Oak", "Green", "Rough") // Reuses the same TreeType as the first Oak
+
+    // Draw all trees
+    forest.draw()
+}
+
+ +

[Output]

+ +
Creating new TreeType: Oak
+Creating new TreeType: Pine
+Drawing tree: Oak, color: Green, texture: Rough at (10, 20)
+Drawing tree: Pine, color: Dark Green, texture: Smooth at (15, 25)
+Drawing tree: Oak, color: Green, texture: Rough at (10, 20)
+
+ +

結論

+ +

享元模式通過共享技術,有效降低了系統的記憶體使用量,提升了效能。它特別適用於需要大量重複物件的情境,例如文字編輯器、遊戲開發等場景。然而,在使用時需要小心區分內部與外部狀態,以確保系統設計的正確性與靈活性。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-17-proxy-pattern/index.html b/design pattern/design-pattern-17-proxy-pattern/index.html new file mode 100644 index 0000000..2bec788 --- /dev/null +++ b/design pattern/design-pattern-17-proxy-pattern/index.html @@ -0,0 +1,975 @@ + + + + + + +Design Pattern (17) - Proxy Pattern (代理模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (17) - Proxy Pattern (代理模式) + + +

+ +

了解代理模式如何通過控制對物件的訪問來提升系統的安全性、效能及靈活性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們的任務是建立一個影片播放系統,需求如下:

+ +
    +
  • 應用能播放多個影片,但避免每次都重複下載相同的影片。
  • +
  • 影片需要在用戶第一次訪問時下載,之後從快取中取得以節省資源。
  • +
  • 提供一個透明的介面,無需讓客戶端知道影片是透過代理取得的。
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

proxy_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,我們可能面臨以下挑戰:

+ +
    +
  1. +

    高頻寬消耗 (High Bandwidth Usage)

    + +
      +
    • 如果每次播放影片都重新下載,將導致不必要的頻寬浪費。
    • +
    +
  2. +
  3. +

    延遲時間 (High Latency)

    + +
      +
    • 每次下載影片會增加播放前的等待時間,影響用戶體驗。
    • +
    +
  4. +
  5. +

    客戶端耦合 (Client Coupling)

    +
      +
    • 如果客戶端需要處理影片的下載邏輯,會增加不必要的複雜性。
    • +
    +
  6. +
+ +

套用 Proxy Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Proxy Pattern 解決這個問題。

+ +

Proxy Pattern 提供了解決方案,通過引入 Proxy 物件來控制對核心物件的訪問,實現快取功能並提升效能。

+ +

先來看一下 Proxy Pattern 的 UML:

+ +

proxy_pattern_uml_2

+ +

以下是 Proxy Pattern 的主要角色:

+ +
    +
  • Subject (主題介面):定義核心物件與代理物件的共同介面。
  • +
  • RealSubject (具體主題):核心物件,負責實際下載與播放影片。
  • +
  • Proxy (代理):代理物件,控制對核心物件的訪問,實現快取功能。
  • +
+ +

將 Proxy Pattern 套用到我們的應用吧

+ +

proxy_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[Subject: VideoPlayer]

+ +
interface VideoPlayer {
+    fun download(name: String): String
+    fun play(data: String)
+}
+
+ +

[RealSubject: YoutubeVideoPlayer]

+ +
class YoutubeVideoPlayer : VideoPlayer {
+    override fun download(name: String): String {
+        println("Downloading video from YouTube: $name")
+        // 模擬下載結果返回的影片資料
+        return "VideoData($name)"
+    }
+
+    override fun play(data: String) {
+        println("Playing video: $data")
+    }
+}
+
+ +

[Proxy: ProxyVideoPlayer]

+ +
class ProxyVideoPlayer(
+    private val player: YoutubeVideoPlayer
+) : VideoPlayer {
+
+    private val cacheVideoList = mutableMapOf<String, String>()
+
+    override fun download(name: String): String {
+        return if (cacheVideoList.containsKey(name)) {
+            println("Fetching video from cache: $name")
+            cacheVideoList[name]!!
+        } else {
+            println("First time download for: $name")
+            val videoData = player.download(name)
+            cacheVideoList[name] = videoData
+            videoData
+        }
+    }
+
+    override fun play(data: String) {
+        player.play(data)
+    }
+}
+
+ +

[Client: VideoPlayerManager]

+ +
class VideoPlayerManager(private val player: VideoPlayer) {
+    fun playVideo(name: String) {
+        println("Request to play video: $name")
+        val videoData = player.download(name)
+        player.play(videoData)
+    }
+}
+
+fun main() {
+    // Using ProxyVideoPlayer
+    val youtubePlayer = YoutubeVideoPlayer()
+    val proxyPlayer = ProxyVideoPlayer(youtubePlayer)
+    val manager = VideoPlayerManager(proxyPlayer)
+
+    // Play video
+    manager.playVideo("funny_cats.mp4")
+    manager.playVideo("funny_cats.mp4") // using cache
+    manager.playVideo("epic_fail.mp4")
+    manager.playVideo("funny_cats.mp4") // using cache
+}
+
+ +

[Output]

+ +
Request to play video: funny_cats.mp4
+First time download for: funny_cats.mp4
+Downloading video from YouTube: funny_cats.mp4
+Playing video: VideoData(funny_cats.mp4)
+
+Request to play video: funny_cats.mp4
+Fetching video from cache: funny_cats.mp4
+Playing video: VideoData(funny_cats.mp4)
+
+Request to play video: epic_fail.mp4
+First time download for: epic_fail.mp4
+Downloading video from YouTube: epic_fail.mp4
+Playing video: VideoData(epic_fail.mp4)
+
+Request to play video: funny_cats.mp4
+Fetching video from cache: funny_cats.mp4
+Playing video: VideoData(funny_cats.mp4)
+
+ +

結論

+ +

透過 Proxy Pattern,我們成功實現了影片快取的功能,解決了頻寬消耗與延遲時間過長的問題。此外,代理物件與核心物件共享相同的介面,對客戶端保持透明性,進一步降低耦合性。此模式特別適用於需要控制對資源訪問的場景,例如遠端代理、安全代理與智慧代理,為系統提供了靈活性與可擴展性。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-18-chain-of-responsibility-pattern/index.html b/design pattern/design-pattern-18-chain-of-responsibility-pattern/index.html new file mode 100644 index 0000000..99fab55 --- /dev/null +++ b/design pattern/design-pattern-18-chain-of-responsibility-pattern/index.html @@ -0,0 +1,965 @@ + + + + + + +Design Pattern (18) - Chain of Responsibility Pattern (責任鏈模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (18) - Chain of Responsibility Pattern (責任鏈模式) + + +

+ +

了解責任鏈模式如何讓請求能被多個對象動態處理,提升系統靈活性與可擴展性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們的任務是建立一個日誌處理系統,需求如下:

+ +
    +
  • 系統支持多層次日誌處理(如 Console、File、Database 等)。
  • +
  • 請求可以被多個處理器處理,且處理器的組合應具備動態調整能力。
  • +
  • 確保每層處理器的責任彼此獨立,並能擴展新處理器而不影響既有邏輯。
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

chain_of_responsibility_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,我們可能面臨以下挑戰:

+ +
    +
  1. +

    高耦合性 (High Coupling)

    + +
      +
    • 如果客戶端需要直接控制每個日誌處理器,將導致代碼過於複雜且難以維護。
    • +
    +
  2. +
  3. +

    缺乏靈活性 (Lack of Flexibility)

    + +
      +
    • 無法輕鬆地調整處理器的執行順序或新增處理器。
    • +
    +
  4. +
  5. +

    違反開放關閉原則 (Violates OCP)

    +
      +
    • 若需支持新的日誌處理方式,必須修改客戶端邏輯,導致系統穩定性下降。
    • +
    +
  6. +
+ +

套用 Chain of Responsibility Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

先來看一下 Chain of Responsibility Pattern 的 UML:

+ +

chain_of_responsibility_pattern_uml_2

+ +

責任鏈模式提供了解決方案,通過將處理器鏈接成一條動態的責任鏈,使請求能被多個處理器依次處理,降低耦合性並提升系統的靈活性與可擴展性。

+ +

以下是 Chain of Responsibility Pattern 的主要角色:

+ +
    +
  • Handler (處理者介面):定義處理請求的介面,並包含指向下一個處理者的引用。
  • +
  • ConcreteHandler (具體處理者):實現處理邏輯,並根據條件決定是否將請求傳遞給下一個處理者。
  • +
  • Client (客戶端):發送請求,並設定處理者的責任鏈結構。
  • +
+ +

將 Chain of Responsibility Pattern 套用到我們的應用吧

+ +

chain_of_responsibility_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[Handler: Logger]

+ +
abstract class Logger(private val nextLogger: Logger? = null) {
+
+    abstract fun log(level: LogLevel, message: String)
+
+    protected fun passToNext(level: LogLevel, message: String) {
+        nextLogger?.log(level, message)
+    }
+}
+
+ +

[LogLevel Enum]

+ +
enum class LogLevel {
+    INFO, WARNING, ERROR
+}
+
+ +

[ConcreteHandler: ConsoleLogger]

+ +
class ConsoleLogger(nextLogger: Logger? = null) : Logger(nextLogger) {
+
+    override fun log(level: LogLevel, message: String) {
+        if (level == LogLevel.INFO) {
+            println("ConsoleLogger: $message")
+        }
+        passToNext(level, message)
+    }
+}
+
+ +

[ConcreteHandler: FileLogger]

+ +
class FileLogger(nextLogger: Logger? = null) : Logger(nextLogger) {
+
+    override fun log(level: LogLevel, message: String) {
+        if (level == LogLevel.WARNING) {
+            println("FileLogger: $message")
+        }
+        passToNext(level, message)
+    }
+}
+
+ +

[ConcreteHandler: DatabaseLogger]

+ +
class DatabaseLogger(nextLogger: Logger? = null) : Logger(nextLogger) {
+
+    override fun log(level: LogLevel, message: String) {
+        if (level == LogLevel.ERROR) {
+            println("DatabaseLogger: $message")
+        }
+        passToNext(level, message)
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    val loggerChain = ConsoleLogger(FileLogger(DatabaseLogger()))
+
+    println("Sending INFO log...")
+    loggerChain.log(LogLevel.INFO, "This is an informational message.")
+
+    println("\nSending WARNING log...")
+    loggerChain.log(LogLevel.WARNING, "This is a warning message.")
+
+    println("\nSending ERROR log...")
+    loggerChain.log(LogLevel.ERROR, "This is an error message.")
+}
+
+ +

[Output]

+ +
Sending INFO log...
+ConsoleLogger: This is an informational message.
+
+Sending WARNING log...
+FileLogger: This is a warning message.
+
+Sending ERROR log...
+DatabaseLogger: This is an error message.
+
+ +

結論

+ +

透過 Chain of Responsibility Pattern,我們成功實現了動態的責任鏈結構,讓請求能被多個處理器依次處理。這不僅降低了客戶端與處理器之間的耦合,還提供了高度靈活性與擴展性,使系統更具彈性。此模式特別適合需要多層次處理的場景,例如日誌處理、請求驗證、事件處理等,為系統設計提供了強大的工具。

+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-19-command-pattern/index.html b/design pattern/design-pattern-19-command-pattern/index.html new file mode 100644 index 0000000..d3bedd0 --- /dev/null +++ b/design pattern/design-pattern-19-command-pattern/index.html @@ -0,0 +1,1018 @@ + + + + + + +Design Pattern (19) - Command Pattern (命令模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (19) - Command Pattern (命令模式) + + +

+ +

了解命令模式如何將操作與執行解耦,讓程式具備更高的靈活性與可擴展性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們需要一個音樂播放器控制系統,需求如下:

+ +
    +
  • 使用者可以透過遙控器控制音樂播放器執行「播放」、「暫停」和「停止」操作。
  • +
  • 支援撤銷 (Undo) 功能,例如撤銷暫停會恢復播放。
  • +
  • 按鈕行為應保持靈活,方便未來擴充新功能,例如「下一首」或「重播」。
  • +
+ +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

command_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,我們可能面臨以下挑戰:

+ +
    +
  1. +

    高耦合性 (High Coupling)

    + +
      +
    • 客戶端需要直接操作每個具體設備的功能,導致耦合度過高,不利於系統擴展。
    • +
    +
  2. +
  3. +

    缺乏靈活性 (Lack of Flexibility)

    + +
      +
    • 如果需要新增設備或操作,客戶端需要修改大量程式碼,增加了維護成本。
    • +
    +
  4. +
  5. +

    撤銷/重做困難 (Undo/Redo Complexity)

    +
      +
    • 系統沒有統一的方式處理操作歷史,導致撤銷和重做功能難以實現。
    • +
    +
  6. +
+ +

套用 Command Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Command Pattern 解決這個問題。

+ +

先來看一下 Command Pattern 的 UML:

+ +

command_pattern_uml_2

+ +

以下是 Command Pattern 的主要角色:

+ +

角色與職責

+ +
    +
  1. +

    Receiver (接收者)
    +實際執行音樂播放邏輯的物件,例如播放、暫停和停止操作。

    +
  2. +
  3. +

    Command (命令介面)
    +定義命令的共同介面,保證命令的可執行性 (Execute) 與可撤銷性 (Undo)。

    +
  4. +
  5. +

    ConcreteCommand (具體命令)
    +將具體的播放控制操作封裝到命令物件中,例如「播放命令」、「暫停命令」和「停止命令」。

    +
  6. +
  7. +

    Invoker (呼叫者)
    +遙控器,負責執行命令並追蹤命令歷史,以支援撤銷操作。

    +
  8. +
  9. +

    Client (客戶端)
    +負責初始化命令、接收者與遙控器之間的對應關係。

    +
  10. +
+ +

將 Command Pattern 套用到我們的應用吧

+ +

command_pattern_uml_3

+ +
+ +

物件導向程式設計 (OOP)

+ +

[Receiver: 音樂播放器]

+ +
class MusicPlayer {
+    fun play() {
+        println("Music is playing")
+    }
+
+    fun pause() {
+        println("Music is paused")
+    }
+
+    fun stop() {
+        println("Music is stopped")
+    }
+}
+
+ +

[Command: 命令介面]

+ +
interface Command {
+    fun execute()
+    fun undo()
+}
+
+ +

[ConcreteCommand: 具體命令]

+ +
class PlayCommand(private val player: MusicPlayer) : Command {
+    override fun execute() {
+        player.play()
+    }
+
+    override fun undo() {
+        player.pause() // 撤銷播放則暫停
+    }
+}
+
+class PauseCommand(private val player: MusicPlayer) : Command {
+    override fun execute() {
+        player.pause()
+    }
+
+    override fun undo() {
+        player.play() // 撤銷暫停則播放
+    }
+}
+
+class StopCommand(private val player: MusicPlayer) : Command {
+    override fun execute() {
+        player.stop()
+    }
+
+    override fun undo() {
+        println("Cannot undo stop") // 撤銷停止通常無法恢復
+    }
+}
+
+ +

[Invoker: 遙控器]

+ +
class RemoteControl {
+    private val commandHistory = mutableListOf<Command>()
+
+    fun pressButton(command: Command) {
+        command.execute()
+        commandHistory.add(command)
+    }
+
+    fun pressUndo() {
+        if (commandHistory.isNotEmpty()) {
+            val lastCommand = commandHistory.removeLast()
+            lastCommand.undo()
+        } else {
+            println("No command to undo")
+        }
+    }
+}
+
+ +

[Client: 客戶端]

+ +
fun main() {
+    val player = MusicPlayer()
+
+    val playCommand = PlayCommand(player)
+    val pauseCommand = PauseCommand(player)
+    val stopCommand = StopCommand(player)
+
+    val remoteControl = RemoteControl()
+
+    // Play music
+    remoteControl.pressButton(playCommand)
+
+    // Pause music
+    remoteControl.pressButton(pauseCommand)
+
+    // Undo
+    remoteControl.pressUndo()
+
+    // Stop music
+    remoteControl.pressButton(stopCommand)
+
+    // Undo
+    remoteControl.pressUndo()
+}
+
+ +

Output

+ +
Music is playing
+Music is paused
+Music is playing
+Music is stopped
+Cannot undo stop
+
+ +

結論

+ +

透過 Command Pattern,我們成功解除了客戶端與具體設備的耦合,讓系統更具靈活性。此外,命令模式還方便了操作的撤銷與重做功能的實現,大大提升了系統的擴展性與維護性。此模式特別適用於需要排程請求、記錄操作歷史或提供撤銷/重做功能的場景。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-2-design-principle/index.html b/design pattern/design-pattern-2-design-principle/index.html new file mode 100644 index 0000000..d5b367e --- /dev/null +++ b/design pattern/design-pattern-2-design-principle/index.html @@ -0,0 +1,1257 @@ + + + + + + +Design Pattern (2) - Design Principles (設計原則) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (2) - Design Principles (設計原則) + + +

+ +

學習如何透過單一職責和開放封閉等設計原則提升程式碼質量,打造靈活、可維護的軟體系統。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

Design Principle

+ +

Design Principle 是用來幫助我們改善物件導向設計的建議,幫助我們設計出更好的軟體。

+ +

SOLID 物件導向程式設計基本五大原則

+ +

Single Responsibility Principle (SRP) 單一職責原則

+ +

物件應該僅具有一種單一功能,應只會有一個理由去改變此物件

+ +

e.g. +我們要做登入頁面功能,我們會這樣寫

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

依照單一職責原則,我們應該要將 API 及 DB 的功能分開,修改如下

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

有些文章會說 save, delete function 也須拆開在不同 class(DeleteDBService, SaveDBService)處理, 因為 save. delete 是不同職責修改項目, 不應動到另一個 class, 但我認為這樣 Over Design 反而不好維護, 拆分職責應適當不過度

+ +

Open Closed Principle (OCP) 開放封閉原則

+ +

對於擴充開放,對於修改封閉

+ +

e.g. +我們常常會需要檢查使用者登入的帳密等等,我們來做一個檢查器吧

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

但假如今天客戶想要增加 Email、Phone Number、Device Mac 等等的格式檢查,那我們必須修改到 Validator class 的程式碼,這樣會影響到其他程式碼,打破了 Open-Closed Principle,對於擴充開放,對於修改封閉,那我們可以怎麼改進,如下

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

如此要新增 Email、Phone Number、Device Mac 格式檢查,我們只需要新增相對應的檢查器即可 EmailValidator、PhoneNumberValidator 及 DeviceMacValidator,既不會影響其他程式碼(對修改封閉),也容易擴充新的檢查器(對擴充開放)

+ +

Liskov Substitution Principle (LSP) 里氏替換原則

+ +

程式中的物件應該是可以在不改變程式正確性的前提下被它的子類所替換的

+ +

e.g. 我們需要計算正方形及長方形的面積

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

上面的例子我們將正方形繼承長方形,但正方形的 getArea() 卻不符合長方形的結果,這就打破了 LSP。

+ +
    +
  • 增加程式碼的健全度,在使用不同的子類別的時候,可以大幅度的保證彼此之間的相容性。只要父類別可以使用,基本上子類別也可以使用
  • +
  • 子類別如果要新增功能,獨立在父類別的功能之外,才不會在搬移到其他子類別的時候發生奇怪的問題,也可以將功能切分乾淨,區分職責
  • +
+ +

Interface Segregation Principle (ISP) 介面隔離原則

+ +

多個特定客戶端介面要好於一個寬泛用途的介面

+ +

e.g. +今天需要設計如何讓使用者操作車子

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

工程師可以開啟 DebugMode, 但駕駛使用者不應該可以開啟 DebugMode,因此我們來改變程式碼將 enableDebugMode() 隔離成獨立介面吧!

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

如此就只有工程師能進入 DebugMode

+ +

Dependency Inversion Principle (DIP) 依賴反向原則

+ +

高階模組不應該依賴於低階模組,兩者都應該依賴抽象, +抽象不應該依賴細節,細節應該依賴抽象。

+ +

e.g. 設計一個能不同房間加入不同 IoT 設備的系統,可以新增刪除房間,例如客廳有智慧音箱、溫度控制器,廚房有煙霧偵測器等…

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

如果今天 SQLite 因某些問題(速度過慢等等…)因素,導致我們必須換成 CoreData 或其他 Database 呢? +你會發現我們無法抽換,但如果依賴於抽象編寫,程式碼就會非常好抽換及測試,下面讓我們修改一下程式碼

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

抽象 - interface, protocol, abstract class

+ +
    +
  • 依賴於抽象可以使我們的程式碼更加有彈性, 也更好抽換依賴物件
  • +
  • 養成多寫一層抽象成能使程式碼更好維護、測試
  • +
  • 抽象層能使我們非常容易的製作假物件快速測試程式邏輯
  • +
+ +

Encapsulate What Varies 封裝變化

+ +

找出程式中可能需要更動之處,把它們獨立出來,不要和那些不需要更動的程式碼混再一起。

+ +

假設今天要設計一間鬆餅店,可以訂購鬆餅

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

但老闆今天想增加新口味 ChocolateChip ,但 cook(), plate(), addButter() 這些程式並不需要修改,所以我們應該將會變化的程式碼抽出來封裝,減少對不需變動的程式碼產生影響。

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

如此我們就可以隨時添加新口味且不會影響其他不會變動的程式碼。

+ +

Favor composition over inheritance 多用合成,少用繼承

+ +

HAS-A (composition) can be better than IS-A (inheritance)

+ +

盡量使用合成來取代繼承,並不是完全不使用繼承,而是多數情況下你應該考慮使用合成而不是繼承

+ +

假設今天要設計一間咖啡店,裡面有賣很多咖啡,我們可以這樣設計

+ +

design_pattern_design_principle_favor_composition_over_inheritance_1

+ +

但如果今天客人要加 Butter 及 Milk,我們又要定義一個新的 class CoffeeWithButterAndMilk 繼承 Coffee +我們會發現,隨著調味料種類越多,咖啡的組合也會越來越多,以及牛奶價格上漲,那所有包含牛奶的 Coffee 都必須修改

+ +

如果我們用合成取代繼承呢,從牛奶咖啡是(is-A)咖啡,變成咖啡有(has-A)各種調味料

+ +

design_pattern_design_principle_favor_composition_over_inheritance_2

+ +

你會發現使用合成取代繼承有下列好處

+ +
    +
  1. 可以在 Run time 替換不同的調料物件
  2. +
  3. 新增一種新調料只需新增一個對應的 class
  4. +
  5. 沒有重複的程式碼
  6. +
  7. 避免 class 數量爆炸性增加
  8. +
+ +
+

再次強調不是完全不用繼承,而是”盡量”用合成取代繼承,像圖中例子 Mocha、Butter 及 Milk 也是有使用到繼承去繼承 Condiment

+
+ +

Loose Coupling 鬆耦合

+ +

將每個組件獨立開來,使部件之間的相互影響降低

+ +

再來看一個範例,今天要做一個 Weather App,他可以取得溫度並顯示在螢幕上。

+ +

design_pattern_design_principle_loose_coupling_1

+ +

你會發現 WeatherApp 與 LCDScreen 緊密耦合,今天如果老闆想改成在 Widget 或 LED 上來顯示,WeatherApp 的 screen 屬性及 display 方法都要修改,且不能在 Run time 任意替換。

+ +

我們改一下 UML 來將兩者之間做解耦

+ +

design_pattern_design_principle_loose_coupling_2

+ +

這樣不管老闆想改成什麼螢幕都能夠很輕易替換,因為 WeatherApp 依賴的是介面,不再是實體,善用抽象介面解耦兩個實體物件吧!

+ +

Program to Interfaces 基於介面編程

+ +

寫程式是針對介面而寫,而不是針對實踐方式而寫。

+ +

當你針對介面編寫,你會發現任何物件都變得非常好抽換,當你需要注入假資料測試,你可以作假物件實作此介面即可,當你在做 MVC MVP 等架構,只需要實作此介面就能輕易替換組件,當你開始已介面去思考,你會發現程式碼變得非常有彈性、且好擴充測試,Apple 甚至在 WWDC15 中提到 Swift 的 Protocol-Oriented Programming,代表介面(協議)思考的重要性,所以從現在起開始從介面思考吧!

+ +

來個例子,今天我們要設計一個基本網站,有一個 WebSystem 及 DB 來存讀資料

+ +

design_pattern_design_principle_program_to_interface_1

+ +

今天我們想在上 Production 前,將 CommercialDB 換成 TestDB +做測試,但目前 KillerWebSystem 的 db 型別為 CommercialDB 無法抽換

+ +

讓我們修改一下

+ +

design_pattern_design_principle_program_to_interface_2

+ +

寫一個 AbstractDB 的介面或抽象類別,讓 CommercialDB 及 TestDB 都實作此介面,這樣就能很輕鬆的在測試環境替換 DB

+ +

總結

+ +

在本篇文章中,我們深入探討了設計原則的重要性,並透過實際的例子,如何在不同環境下靈活切換數據庫實例,展示了這些原則在實際開發中的應用。通過引入 AbstractDB 介面,我們看到了如何將具體的數據庫實現(如 CommercialDB 和 TestDB)與系統的其他部分解耦,從而提高了程式碼的靈活性和可維護性。

+ +

這一過程不僅鞏固了我們對物件導向概念的理解,也為我們進一步探索設計模式鋪平了道路。設計原則是構建健壯、可擴展和靈活系統的基石,而設計模式則提供了一套解決特定設計問題的模板和最佳實踐。

+ +

隨著我們即將進入設計模式的探索,期待您能夠將這些原則與即將學習的模式結合起來,進一步提升您的軟件設計和開發能力。下一篇文章將帶您深入設計模式的世界,探索如何通過這些模式解決更加複雜的設計挑戰,敬請期待。

+ +

design_pattern_design_principle_architecture

+ +
+

Object-Oriented Concepts -> Design Principle -> Design Pattern

+
+ +

參考

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長 🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-20-iterator-pattern/index.html b/design pattern/design-pattern-20-iterator-pattern/index.html new file mode 100644 index 0000000..8e19cc6 --- /dev/null +++ b/design pattern/design-pattern-20-iterator-pattern/index.html @@ -0,0 +1,1006 @@ + + + + + + +Design Pattern (20) - Iterator Pattern (迭代器模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (20) - Iterator Pattern (迭代器模式) + + +

+ +

了解迭代器模式如何提供一種順序來訪問集合內元素的方法,而不需要暴露集合的底層表示。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們的任務是設計一個檔案系統搜尋工具,需求如下:

+ +
    +
  • 使用者可以選擇不同的檔案搜尋方式,例如 廣度優先搜尋 (BFS)深度優先搜尋 (DFS)
  • +
  • 客戶端不需要關心搜尋邏輯的實現細節,只需使用統一的迭代器介面來遍歷搜尋結果。
  • +
  • 系統需要具備擴展性,方便新增其他搜尋法,例如基於檔案大小排序的搜尋。
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

iterator_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,我們可能面臨以下挑戰:

+ +
    +
  1. +

    高耦合性 (High Coupling)

    + +
      +
    • 客戶端需要直接操作每種搜尋方式的實現細節,導致代碼臃腫且難以維護。
    • +
    +
  2. +
  3. +

    缺乏一致性 (Lack of Consistency)

    + +
      +
    • 不同搜尋方式的結果訪問方式可能不一致。
    • +
    +
  4. +
  5. +

    違反開放關閉原則 (Violates OCP)

    +
      +
    • 若新增搜尋法或更改現有搜尋邏輯,需要修改客戶端程式碼。
    • +
    +
  6. +
+ +

套用 Iterator Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Iterator Pattern 解決這個問題。

+ +

迭代器模式允許我們對搜尋結果進行順序訪問,而不需要暴露搜尋邏輯的細節。

+ +

先來看一下 Iterator Pattern 的 UML:

+ +

iterator_pattern_uml_2

+ +

以下是 Iterator Pattern 的主要角色:

+ +
    +
  • Iterator (迭代器介面):定義訪問搜尋結果的方法,例如 hasNext()next()
  • +
  • ConcreteIterator (具體迭代器):實現不同的搜尋邏輯,如 BFS 或 DFS。
  • +
  • Aggregate (聚合介面):定義方法來創建迭代器。
  • +
  • ConcreteAggregate (具體聚合類別):實現聚合介面,提供檔案系統資料的具體實現。
  • +
+ +

將 Iterator Pattern 套用到我們的應用吧

+ +

iterator_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[Iterator]

+ +
interface Iterator<T> {
+    fun hasNext(): Boolean
+    fun next(): T
+}
+
+ +

[Aggregate: FileSystem]

+ +
interface FileSystem {
+    fun createIterator(): Iterator<File>
+}
+
+ +

[ConcreteIterator: BFSIterator, DFSIterator]

+ +
class BFSIterator(private val root: File) : Iterator<File> {
+    private val queue = ArrayDeque<File>()
+
+    init {
+        queue.add(root)
+    }
+
+    override fun hasNext(): Boolean {
+        return queue.isNotEmpty()
+    }
+
+    override fun next(): File {
+        if (!hasNext()) throw NoSuchElementException()
+        val current = queue.removeFirst()
+        if (current.isDirectory) {
+            queue.addAll(current.listFiles().orEmpty())
+        }
+        return current
+    }
+}
+
+class DFSIterator(private val root: File) : Iterator<File> {
+    private val stack = ArrayDeque<File>()
+
+    init {
+        stack.add(root)
+    }
+
+    override fun hasNext(): Boolean {
+        return stack.isNotEmpty()
+    }
+
+    override fun next(): File {
+        if (!hasNext()) throw NoSuchElementException()
+        val current = stack.removeLast()
+        if (current.isDirectory) {
+            stack.addAll(current.listFiles().orEmpty())
+        }
+        return current
+    }
+}
+
+ +

[ConcreteAggregate: DefaultFileSystem]

+ +
class DefaultFileSystem(private val root: File, private val searchMethod: SearchMethod) : FileSystem {
+    override fun createIterator(): Iterator<File> {
+        return when (searchMethod) {
+            SearchMethod.BFS -> BFSIterator(root)
+            SearchMethod.DFS -> DFSIterator(root)
+        }
+    }
+}
+
+enum class SearchMethod {
+    BFS, DFS
+}
+
+ +

[File]

+ +
data class File(val name: String, val isDirectory: Boolean, val children: List<File> = emptyList()) {
+    fun listFiles(): List<File> = if (isDirectory) children else emptyList()
+}
+
+ +

[Client]

+ +
fun main() {
+    val fileSystem = DefaultFileSystem(
+        root = File(
+            name = "root",
+            isDirectory = true,
+            children = listOf(
+                File("file1.txt", false),
+                File("folder1", true, listOf(
+                    File("file2.txt", false),
+                    File("file3.txt", false)
+                )),
+                File("folder2", true, listOf(
+                    File("file4.txt", false)
+                ))
+            )
+        ),
+        searchMethod = SearchMethod.BFS
+    )
+
+    val iterator = fileSystem.createIterator()
+    println("Files:")
+    while (iterator.hasNext()) {
+        println("- ${iterator.next().name}")
+    }
+}
+
+ +

[Output]

+ +
Files:
+- root
+- file1.txt
+- folder1
+- folder2
+- file2.txt
+- file3.txt
+- file4.txt
+
+ +

結論

+ +

透過 Iterator Pattern,我們成功實現了不同搜尋法的整合,讓客戶端能以一致的方式訪問搜尋結果。此模式提升了系統的靈活性與擴展性,特別適合處理多種遍歷邏輯的場景,例如檔案搜尋、樹狀結構遍歷等。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-21-mediator-pattern/index.html b/design pattern/design-pattern-21-mediator-pattern/index.html new file mode 100644 index 0000000..1fd2385 --- /dev/null +++ b/design pattern/design-pattern-21-mediator-pattern/index.html @@ -0,0 +1,961 @@ + + + + + + +Design Pattern (21) - Mediator Pattern (中介者模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (21) - Mediator Pattern (中介者模式) + + +

+ +

了解中介者模式如何協調物件之間的交互,減少物件之間的耦合性並促進系統的可擴展性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們的任務是設計一個 聊天室應用程式,需求如下:

+ +
    +
  • 使用者可以透過聊天室傳遞訊息。
  • +
  • 每個使用者都不需要直接管理其他使用者的資訊。
  • +
  • 新增或移除使用者不應影響其他使用者的運作。
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,我們來快速分析:

+ +
    +
  • 若使用者彼此直接通信,會導致複雜的相依關係,增加維護成本。
  • +
  • 我們需要一個集中管理的角色,來協調使用者之間的訊息傳遞。
  • +
+ +

mediator_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,我們可能面臨以下挑戰:

+ +
    +
  1. +

    高耦合性 (High Coupling)

    + +
      +
    • 使用者彼此之間直接通信,導致新增或移除使用者時需修改多處程式碼。
    • +
    +
  2. +
  3. +

    難以擴展 (Hard to Extend)

    + +
      +
    • 若要增加新功能(如訊息過濾或廣播機制),需要修改多個使用者的邏輯。
    • +
    +
  4. +
  5. +

    複雜度上升 (Increased Complexity)

    + +
      +
    • 使用者之間的關聯數量隨著使用者數量增長呈指數級增加。
    • +
    +
  6. +
+ +

套用 Mediator Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Mediator Pattern 解決這個問題。

+ +

中介者模式引入了一個中介者來負責協調使用者之間的交互。使用者只需與中介者通信,從而降低相互之間的耦合性。

+ +

以下是 Mediator Pattern 的 UML 圖:

+ +

mediator_pattern_uml_2

+ +

Mediator Pattern 的主要角色:

+ +
    +
  • Mediator (中介者介面):定義協調參與者的方法,例如傳遞訊息。
  • +
  • ConcreteMediator (具體中介者):實現中介者的行為,處理使用者之間的訊息傳遞。
  • +
  • Colleague (同事類別):表示參與者,所有訊息均通過中介者進行傳遞。
  • +
  • ConcreteColleague (具體同事類別):實現具體參與者的行為,並依賴中介者進行通信。
  • +
+ +

讓我們將 Mediator Pattern 套用到聊天室應用程式中。

+ +

mediator_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

[Mediator]

+ +
interface ChatMediator {
+    fun sendMessage(message: String, user: User)
+    fun addUser(user: User)
+}
+
+ +

[ConcreteMediator]

+ +
class ChatRoomMediator : ChatMediator {
+    private val users = mutableListOf<User>()
+
+    override fun sendMessage(message: String, user: User) {
+        users.filter { it != user }.forEach { it.receive(message) }
+    }
+
+    override fun addUser(user: User) {
+        users.add(user)
+    }
+}
+
+ +

[Colleague]

+ +
abstract class User(protected val mediator: ChatMediator, val name: String) {
+    abstract fun send(message: String)
+    abstract fun receive(message: String)
+}
+
+ +

[ConcreteColleague]

+ +
class ChatUser(mediator: ChatMediator, name: String) : User(mediator, name) {
+    override fun send(message: String) {
+        println("$name 發送訊息:$message")
+        mediator.sendMessage(message, this)
+    }
+
+    override fun receive(message: String) {
+        println("$name 收到訊息:$message")
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    val chatMediator = ChatRoomMediator()
+
+    val user1 = ChatUser(chatMediator, "Alice")
+    val user2 = ChatUser(chatMediator, "Bob")
+    val user3 = ChatUser(chatMediator, "Charlie")
+
+    chatMediator.addUser(user1)
+    chatMediator.addUser(user2)
+    chatMediator.addUser(user3)
+
+    user1.send("Hello, everyone!")
+    user2.send("Hi, Alice!")
+}
+
+ +

[Output]

+ +
Alice 發送訊息:Hello, everyone!
+Bob 收到訊息:Hello, everyone!
+Charlie 收到訊息:Hello, everyone!
+Bob 發送訊息:Hi, Alice!
+Alice 收到訊息:Hi, Alice!
+Charlie 收到訊息:Hi, Alice!
+
+ +

結論

+ +

透過中介者模式,我們成功降低了使用者之間的耦合性,並實現了靈活的訊息傳遞機制。此模式非常適合處理多物件之間的交互,例如聊天室、事件系統或 GUI 組件通信等場景。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-22-memento-pattern/index.html b/design pattern/design-pattern-22-memento-pattern/index.html new file mode 100644 index 0000000..99a2b12 --- /dev/null +++ b/design pattern/design-pattern-22-memento-pattern/index.html @@ -0,0 +1,954 @@ + + + + + + +Design Pattern (22) - Memento Pattern (備忘錄模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (22) - Memento Pattern (備忘錄模式) + + +

+ +

了解備忘錄模式如何幫助我們實現狀態恢復,像是常見的 Ctrl+Z 功能,讓我們回到之前的操作狀態。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們的任務是設計一個文字編輯器,需求如下:

+ +
    +
  • 使用者可以輸入文字,並隨時按下 Ctrl+Z 回復上一步。
  • +
  • 系統需要保存歷史狀態以供回復。
  • +
  • 客戶端不需要了解狀態保存的實現細節,只需使用一個簡單的回復操作即可。
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

memento_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,我們可能面臨以下挑戰:

+ +
    +
  1. +

    資料喪失風險 (Data Loss Risk)

    + +
      +
    • 如果我們僅保留當前狀態,將無法回復到之前的狀態。
    • +
    +
  2. +
  3. +

    高耦合性 (High Coupling)

    + +
      +
    • 客戶端需要直接操作狀態管理邏輯,導致複雜性增加。
    • +
    +
  4. +
  5. +

    難以擴展 (Hard to Extend)

    + +
      +
    • 新增功能或改變狀態保存方式時,可能需要修改大量程式碼。
    • +
    +
  6. +
+ +

套用 Memento Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Memento Pattern 解決這個問題

+ +

先來看一下 Memento Pattern 的 UML

+ +

memento_pattern_uml_2

+ +

備忘錄模式引入了三個角色:

+ +
    +
  1. Originator (發起者):保存當前狀態到備忘錄,或從備忘錄中恢復狀態。
  2. +
  3. Memento (備忘錄):存儲 Originator 的內部狀態。
  4. +
  5. Caretaker (管理者):負責保存和恢復備忘錄,但不直接操作其內容。
  6. +
+ +

將 Memento Pattern 套用到我們的應用吧

+ +

memento_pattern_uml_3

+ +

物件導向程式設計 (OOP)

+ +

Originator

+ +
class TextEditor {
+    private var text: String = ""
+
+    fun type(newText: String) {
+        text += newText
+    }
+
+    fun getText(): String = text
+
+    fun save(): Memento = Memento(text)
+
+    fun restore(memento: Memento) {
+        text = memento.getText()
+    }
+
+    data class Memento(private val state: String) {
+        fun getText(): String = state
+    }
+}
+
+ +

Caretaker

+ +
class History {
+    private val mementos = mutableListOf<TextEditor.Memento>()
+
+    fun save(memento: TextEditor.Memento) {
+        mementos.add(memento)
+    }
+
+    fun undo(): TextEditor.Memento? {
+        if (mementos.isNotEmpty()) {
+            return mementos.removeAt(mementos.size - 1)
+        }
+        return null
+    }
+}
+
+ +

Client

+ +
fun main() {
+    val textEditor = TextEditor()
+    val history = History()
+
+    // Typing string
+    textEditor.type("Hello")
+    history.save(textEditor.save())
+
+    textEditor.type(", World")
+    history.save(textEditor.save())
+
+    textEditor.type("! This is Memento Pattern.")
+    println("Current Text:${textEditor.getText()}") // Output: Hello, World! This is Memento Pattern.
+
+    // Pressed Ctrl+Z
+    textEditor.restore(history.undo()!!)
+    println("Excute undo Text:${textEditor.getText()}") // Output: Hello, World!
+
+    // Pressed Ctrl+Z again
+    textEditor.restore(history.undo()!!)
+    println("Excute undo Text:${textEditor.getText()}") // Output: Hello
+}
+
+ +

Output

+ +
Current Text: Hello, World! This is Memento Pattern.
+Excute undo Text: Hello, World!
+Excute undo Text: Hello
+
+ +

結論

+ +

透過備忘錄模式,我們成功實現了文字編輯器的狀態恢復功能,讓使用者能夠輕鬆地回復到之前的操作狀態。這種模式廣泛應用於文字處理器、遊戲保存系統以及其他需要狀態恢復的場景。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-23-observer-pattern/index.html b/design pattern/design-pattern-23-observer-pattern/index.html new file mode 100644 index 0000000..c588044 --- /dev/null +++ b/design pattern/design-pattern-23-observer-pattern/index.html @@ -0,0 +1,993 @@ + + + + + + +Design Pattern (23) - Observer Pattern (觀察者模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (23) - Observer Pattern (觀察者模式) + + +

+ +

透過觀察者模式,實現安全系統主機的警報通知機制,當警報觸發時,主機自動通知平板、iOS 和 Android 手機。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們的任務是設計一個 安全系統主機 (Panel),需求如下:

+ +
    +
  • 主機負責監控不同的感測器,例如煙霧探測器或門窗感測器。
  • +
  • 當警報觸發時,主機需要通知所有已註冊的設備,例如平板、iOS 和 Android 手機。
  • +
  • 設備可以動態地加入或移除通知清單。
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

observer_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,我們可能面臨以下挑戰:

+ +
    +
  1. +

    高耦合性 (High Coupling)

    + +
      +
    • 如果主機直接與每一個設備互動,程式碼會變得難以維護,每次新增或移除設備都需要修改主機邏輯。
    • +
    +
  2. +
  3. +

    缺乏彈性 (Lack of Flexibility)

    + +
      +
    • 新增設備需要修改現有程式碼,違反開放關閉原則 (OCP)。
    • +
    +
  4. +
  5. +

    通知不一致 (Inconsistent Notifications)

    +
      +
    • 當警報觸發時,難以確保每個設備都能正確接收到通知。
    • +
    +
  6. +
+ +
+ +

套用 Observer Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Observer Pattern 解決這個問題

+ +

先來看一下 Memento Pattern 的 UML

+ +

observer_pattern_uml_2

+ +

觀察者模式提供了一個一對多的通知機制,當主機的狀態改變時,會自動通知所有已訂閱的設備。

+ +
    +
  • Subject (主體):安全系統主機,負責管理所有設備並在警報觸發時發送通知。
  • +
  • Observer (觀察者):設備,例如平板、iOS 和 Android 手機,接收通知並根據警報執行操作。
  • +
  • ConcreteSubject (具體主體):實際的安全系統主機,包含警報邏輯。
  • +
  • ConcreteObserver (具體觀察者):具體的設備實現,例如 Android 設備或 iOS 設備。
  • +
+ +

將 Observer Pattern 套用到我們的應用吧

+ +

observer_pattern_uml_3

+ +

實作

+ +

[Subject: AlarmSystem]

+ +
interface AlarmSystem {
+    fun addObserver(observer: Device)
+    fun removeObserver(observer: Device)
+    fun notifyObservers(alarmMessage: String)
+}
+
+ +

[Observer: Device]

+ +
interface Device {
+    fun onAlarmTriggered(alarmMessage: String)
+}
+
+ +

[ConcreteSubject: SecurityPanel]

+ +
class SecurityPanel : AlarmSystem {
+    private val devices = mutableListOf<Device>()
+
+    override fun addObserver(observer: Device) {
+        devices.add(observer)
+    }
+
+    override fun removeObserver(observer: Device) {
+        devices.remove(observer)
+    }
+
+    override fun notifyObservers(alarmMessage: String) {
+        for (device in devices) {
+            device.onAlarmTriggered(alarmMessage)
+        }
+    }
+
+    fun triggerAlarm(zone: String) {
+        val message = "警報觸發於 $zone!"
+        println("主機通知: $message")
+        notifyObservers(message)
+    }
+}
+
+ +

[ConcreteObserver: Devices]

+ +
class Tablet : Device {
+    override fun onAlarmTriggered(alarmMessage: String) {
+        println("平板收到通知: $alarmMessage")
+    }
+}
+
+class IOSDevice : Device {
+    override fun onAlarmTriggered(alarmMessage: String) {
+        println("iOS 設備收到通知: $alarmMessage")
+    }
+}
+
+class AndroidDevice : Device {
+    override fun onAlarmTriggered(alarmMessage: String) {
+        println("Android 設備收到通知: $alarmMessage")
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    val securityPanel = SecurityPanel()
+
+    val tablet = Tablet()
+    val iosDevice = IOSDevice()
+    val androidDevice = AndroidDevice()
+
+    // add observers
+    securityPanel.addObserver(tablet)
+    securityPanel.addObserver(iosDevice)
+    securityPanel.addObserver(androidDevice)
+
+    // trigger alarm
+    securityPanel.triggerAlarm("客廳")
+    securityPanel.triggerAlarm("廚房")
+
+    // remove observer
+    securityPanel.removeObserver(androidDevice)
+    securityPanel.triggerAlarm("臥室")
+}
+
+ +

[Output]

+ +
主機通知: 警報觸發於 客廳!
+平板收到通知: 警報觸發於 客廳!
+iOS 設備收到通知: 警報觸發於 客廳!
+Android 設備收到通知: 警報觸發於 客廳!
+
+主機通知: 警報觸發於 廚房!
+平板收到通知: 警報觸發於 廚房!
+iOS 設備收到通知: 警報觸發於 廚房!
+Android 設備收到通知: 警報觸發於 廚房!
+
+主機通知: 警報觸發於 臥室!
+平板收到通知: 警報觸發於 臥室!
+iOS 設備收到通知: 警報觸發於 臥室!
+
+ +

結論

+ +

透過 Observer Pattern,我們構建了一個靈活的安全系統通知機制,設備可以動態地加入或移除,且主機與設備之間的耦合度降低,遵循開放關閉原則 (OCP)。此模式適用於任何需要實現通知機制的場景,例如:

+ +
    +
  • 即時警報系統
  • +
  • 訊息推送系統
  • +
  • 事件分發系統
  • +
+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-24-state-pattern/index.html b/design pattern/design-pattern-24-state-pattern/index.html new file mode 100644 index 0000000..5be17cf --- /dev/null +++ b/design pattern/design-pattern-24-state-pattern/index.html @@ -0,0 +1,1019 @@ + + + + + + +Design Pattern (24) - State Pattern (狀態模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (24) - State Pattern (狀態模式) + + +

+ +

透過狀態模式,設計一個飲水機的運作機制,根據不同狀態執行加熱、冷卻或待機的行為。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +
+ +

需求

+ +

我們的任務是設計一個 飲水機,需求如下:

+ +
    +
  • 飲水機有三種狀態: +
      +
    • 加熱中:提升水溫至熱水。
    • +
    • 冷卻中:降低水溫至冷水。
    • +
    • 待機中:維持現有水溫。
    • +
    +
  • +
  • 使用者可透過按鈕切換飲水機的狀態。
  • +
  • 飲水機需要根據當前狀態執行正確的行為,例如加熱狀態時加熱水,但不可冷卻。
  • +
+ +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

state_pattern_uml_1

+ +

察覺 Forces

+ +

在未使用設計模式的情況下,我們可能面臨以下挑戰:

+ +
    +
  1. +

    高耦合性 (High Coupling)

    + +
      +
    • 狀態邏輯與飲水機核心功能混合在一起,導致程式碼難以維護。
    • +
    +
  2. +
  3. +

    違反單一職責原則 (SRP)

    + +
      +
    • 飲水機類別需要同時處理狀態邏輯與主要功能,責任過於繁重。
    • +
    +
  4. +
  5. +

    難以擴展 (Hard to Extend)

    +
      +
    • 新增或修改狀態行為需更改飲水機核心邏輯,違反開放關閉原則 (OCP)。
    • +
    +
  6. +
+ +
+ +

套用 State Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 State Pattern 解決這個問題

+ +

察覺 Forces 後,我們可以套用 State Pattern,將狀態邏輯封裝成獨立的類別,達到以下效果:

+ +

state_pattern_uml_2

+ +

狀態模式有三個角色:

+ +
    +
  1. +

    State (狀態介面)
    +定義所有具體狀態需要實現的行為。

    +
  2. +
  3. +

    ConcreteState (具體狀態)
    +每個具體狀態類別實現 State 介面,並負責該狀態下的具體行為邏輯。

    +
  4. +
  5. +

    Context (上下文)
    +負責維護當前狀態,並提供介面讓外部操作。在執行操作時,將請求委派給當前狀態物件。

    +
  6. +
+ +
    +
  • 飲水機類別負責狀態管理,而非具體行為實現,降低耦合度。
  • +
  • 每個狀態專注於自身行為,符合單一職責原則。
  • +
  • 新增或修改狀態無需影響飲水機核心邏輯,符合開放關閉原則。
  • +
+ +

將 State Pattern 套用到我們的應用吧

+ +

state_pattern_uml_3

+ +
+ +

物件導向設計 (OOP)

+ +

[State: WaterDispenserState]

+ +
interface WaterDispenserState {
+    fun handleRequest()
+}
+
+ +

[ConcreteStates: HeatingState, CoolingState, StandbyState]

+ +
class HeatingState : WaterDispenserState {
+    override fun handleRequest() {
+        println("加熱中:水溫正在提升,請稍候...")
+    }
+}
+
+class CoolingState : WaterDispenserState {
+    override fun handleRequest() {
+        println("冷卻中:水溫正在降低,請稍候...")
+    }
+}
+
+class StandbyState : WaterDispenserState {
+    override fun handleRequest() {
+        println("待機中:飲水機維持現有水溫,隨時可用。")
+    }
+}
+
+ +

[Context: WaterDispenser]

+ +
class WaterDispenser {
+    private var currentState: WaterDispenserState = StandbyState()
+
+    fun setState(state: WaterDispenserState) {
+        currentState = state
+        println("狀態切換:${state::class.simpleName}")
+    }
+
+    fun pressButton() {
+        currentState.handleRequest()
+    }
+}
+
+ +

[Client]

+ +
fun main() {
+    val dispenser = WaterDispenser()
+
+    // 初始狀態為待機中
+    dispenser.pressButton()
+
+    // 切換到加熱狀態
+    dispenser.setState(HeatingState())
+    dispenser.pressButton()
+
+    // 切換到冷卻狀態
+    dispenser.setState(CoolingState())
+    dispenser.pressButton()
+
+    // 回到待機狀態
+    dispenser.setState(StandbyState())
+    dispenser.pressButton()
+}
+
+ +

[Output]

+ +
待機中:飲水機維持現有水溫,隨時可用。
+狀態切換:HeatingState
+加熱中:水溫正在提升,請稍候...
+狀態切換:CoolingState
+冷卻中:水溫正在降低,請稍候...
+狀態切換:StandbyState
+待機中:飲水機維持現有水溫,隨時可用。
+
+ +

結論

+ +

透過 State Pattern,我們成功將飲水機的狀態邏輯與核心功能分離,實現以下優勢:

+ +
    +
  1. 降低耦合度
  2. +
+ +
    +
  • 飲水機類別專注於狀態切換,具體行為由狀態類別負責。
  • +
+ +
    +
  1. 符合設計原則
  2. +
+ +
    +
  • 單一職責原則 (SRP):每個狀態類別專注於自身行為。
  • +
  • 開放關閉原則 (OCP):新增狀態無需修改現有程式碼。
  • +
+ +
    +
  1. 易於擴展
  2. +
+ +
    +
  • 新增或修改狀態行為時,不影響其他部分。
  • +
+ +

此模式特別適合處理複雜的狀態轉換場景,例如:

+ +
    +
  • ATM 機的插卡、操作、取卡狀態。
  • +
  • 文檔編輯器的編輯、檢視、列印模式。
  • +
+ +

狀態模式讓程式結構更具彈性,是開發狀態機制應用的最佳選擇!

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-25-strategy-pattern/index.html b/design pattern/design-pattern-25-strategy-pattern/index.html new file mode 100644 index 0000000..a0b8634 --- /dev/null +++ b/design pattern/design-pattern-25-strategy-pattern/index.html @@ -0,0 +1,1014 @@ + + + + + + +Design Pattern (25) - Strategy Pattern (策略模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (25) - Strategy Pattern (策略模式) + + +

+ +

策略模式提供了一種靈活的解決方案,讓系統能根據需求動態切換不同的行為邏輯,實現高可擴展性與低耦合性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +
+ +

需求

+ +

在設計一個 電商運費計算系統 時,我們需要滿足以下需求:

+ +
    +
  1. 支援多種運費計算方式,例如: +
      +
    • 一般配送:固定運費。
    • +
    • 快速配送:依重量計費。
    • +
    • 國際配送:根據地區與重量計費。
    • +
    +
  2. +
  3. 系統需具備良好的擴展性: +
      +
    • 能夠方便地新增新的運費計算方式。
    • +
    +
  4. +
  5. 避免使用大量的 if-else 或 switch-case
  6. +
  7. 使用者應能輕鬆切換運費計算方式。
  8. +
+ +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

strategy_pattern_uml_1

+ +

察覺 Forces

+ +

如果未套用設計模式,我們可能會遇到以下問題:

+ +
    +
  1. +

    難以維護

    + +
      +
    • 運費計算邏輯混雜在主程式內,新增或修改一種計算方式可能會影響其他部分。
    • +
    +
  2. +
  3. +

    違反開放關閉原則 (OCP)

    + +
      +
    • 每次新增運費計算方式都需修改核心業務邏輯。
    • +
    +
  4. +
  5. +

    違反單一職責原則 (SRP)

    +
      +
    • 主程式同時負責運費計算與核心業務邏輯,責任過於繁重。
    • +
    +
  6. +
+ +
+ +

套用 Strategy Pattern (Solution) 得到新的 Context (Resulting Context)

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Strategy Pattern 解決這個問題

+ +

先來看一下 Strategy Pattern 的 UML

+ +

strategy_pattern_uml_2

+ +

Strategy Pattern 的組件

+ +

策略模式的核心組件包括:

+ +
    +
  1. +

    Strategy (策略介面)
    +定義所有策略需要實現的行為。

    +
  2. +
  3. +

    ConcreteStrategy (具體策略)
    +每個具體策略類別實現特定的行為邏輯。

    +
  4. +
  5. +

    Context (上下文)
    +維護一個策略物件,並根據當前策略執行對應行為。

    +
  6. +
+ +

將 Strategy Pattern 套用到我們的應用吧

+ +

strategy_pattern_uml_3

+ +
+ +

物件導向設計 (OOP)

+ +

[Strategy: ShippingStrategy]

+ +
interface ShippingStrategy {
+    fun calculateShippingCost(weight: Double, region: String): Double
+}
+
+ +

[ConcreteStrategies: RegularShipping, ExpressShipping, InternationalShipping]

+ +
class RegularShipping : ShippingStrategy {
+    override fun calculateShippingCost(weight: Double, region: String): Double {
+        return 50.0 // 固定運費
+    }
+}
+
+class ExpressShipping : ShippingStrategy {
+    override fun calculateShippingCost(weight: Double, region: String): Double {
+        return weight * 10 // 每公斤 10 元
+    }
+}
+
+class InternationalShipping : ShippingStrategy {
+    override fun calculateShippingCost(weight: Double, region: String): Double {
+        val regionMultiplier = when (region) {
+            "Asia" -> 15
+            "Europe" -> 20
+            "America" -> 25
+            else -> 30
+        }
+        return weight * regionMultiplier
+    }
+}
+
+ +

[Context: ShippingCalculator]

+ +
class ShippingCalculator(private var strategy: ShippingStrategy) {
+
+    fun setStrategy(strategy: ShippingStrategy) {
+        this.strategy = strategy
+    }
+
+    fun calculateCost(weight: Double, region: String): Double {
+        return strategy.calculateShippingCost(weight, region)
+    }
+}
+
+
+ +

[Client]

+ +
fun main() {
+    val calculator = ShippingCalculator(RegularShipping())
+
+    println("一般配送運費: ${calculator.calculateCost(5.0, "Asia")} 元") // 固定 50 元
+
+    calculator.setStrategy(ExpressShipping())
+    println("快速配送運費: ${calculator.calculateCost(5.0, "Asia")} 元") // 5.0 * 10 = 50 元
+
+    calculator.setStrategy(InternationalShipping())
+    println("國際配送運費 (Asia): ${calculator.calculateCost(5.0, "Asia")} 元") // 5.0 * 15 = 75 元
+}
+
+ +

[Output]

+ +
一般配送運費: 50.0 
+快速配送運費: 50.0 
+國際配送運費 (Asia): 75.0 
+
+ +

結論

+ +

透過 Strategy Pattern,我們成功將運費計算邏輯與核心功能分離,並實現以下優勢:

+ +
    +
  1. 易於擴展
  2. +
+ +
    +
  • 新增運費計算方式只需實作新的策略類別,無需修改現有程式碼。
  • +
+ +
    +
  1. 低耦合性
  2. +
+ +
    +
  • 運費計算邏輯與核心業務邏輯分離,各自負責自己的功能。
  • +
+ +
    +
  1. 符合設計原則
  2. +
+ +
    +
  • 單一職責原則 (SRP):每個策略類別專注於特定運費計算邏輯。
  • +
  • 開放關閉原則 (OCP):策略模式允許在不修改現有程式碼的情況下,新增新功能。
  • +
+ +

策略模式非常適合處理需要根據條件執行不同行為的場景,例如:

+ +
    +
  • 不同的折扣策略 (滿額折扣、季節性優惠)。
  • +
  • 不同的排序算法 (快速排序、合併排序)。
  • +
  • 各類繳稅計算方式。
  • +
+ +

策略模式讓系統更具彈性,為複雜問題提供了一個優雅的解決方案。

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-3-design-pattern/index.html b/design pattern/design-pattern-3-design-pattern/index.html new file mode 100644 index 0000000..acc9ef8 --- /dev/null +++ b/design pattern/design-pattern-3-design-pattern/index.html @@ -0,0 +1,921 @@ + + + + + + +Design Pattern (3) - Design Patterns (設計模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (3) - Design Patterns (設計模式) + + +

+ +

深入探討設計模式的概念及其應用步驟,助你高效解決軟體問題。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

Design Pattern 是什麼?

+ +

Design Pattern 是在軟體工程中,用於常見問題解決的一種標準化方法。它們是經過驗證的解決方案,可以用來解決設計中的特定問題。

+ +

設計模式的組成要素

+ +

Context

+ +

Context是指設計模式應用的具體場景或背景。它描述了模式應用的環境和條件。

+ +

Forces

+ +

Forces是指在設計過程中需要考慮的各種因素,包括但不限於性能需求、可擴展性、維護性等。

+ +

Problem

+ +

Problem是指在特定Context和Forces下,開發者面臨的具體設計問題。

+ +

Solution

+ +

Solution是指設計模式提供的解決方案,它幫助開發者解決Problem,並考慮到了Forces的影響。

+ +

設計模式的應用步驟

+ +
    +
  1. 物件導向程式分析(OOA):從高層次理解應用的需求和結構。(此步驟需要劃出UML圖)
  2. +
  3. 看清楚Context:透過UML圖理解模式應用的具體場景。
  4. +
  5. 察覺Forces:識別影響設計的關鍵因素。
  6. +
  7. 找到Problem:明確需要解決的設計問題。
  8. +
  9. 套用模式:根據Problem及Forces選擇合適的設計模式。
  10. +
  11. 得到新的Resulting Context:應用模式後,獲得改進後的設計方案。(此步驟需要劃出UML圖)
  12. +
  13. 誤記導向程式設計 (OOP):透過新的Resulting Context UML圖,開始撰寫程式碼實作。
  14. +
+ +

Design Patterns Categories

+ +

Design Pattern 可以分為三種基本的類型

+ +

Creational 創建型

+ +

創建實例化物件有關的 Patterns

+ +
    +
  • Factory Method Pattern
  • +
  • Abstract Factory Pattern
  • +
  • Builder Pattern
  • +
  • Prototype Pattern
  • +
  • Singleton Pattern
  • +
+ +

Structural 結構型

+ +

物件之間如何組成更大結構的 Patterns

+ +
    +
  • Adapter Pattern
  • +
  • Bridge Pattern
  • +
  • Decorator Pattern
  • +
  • Facade Pattern
  • +
  • Proxy Pattern
  • +
  • Flyweight Pattern
  • +
  • Composite Pattern
  • +
+ +

Behavioural 行為型

+ +

物件之間行為交互的 Patterns

+ +
    +
  • Chain of Responsibility Pattern
  • +
  • Mediator Pattern
  • +
  • Iterator Pattern
  • +
  • State Pattern
  • +
  • Observer Pattern
  • +
  • Command Pattern
  • +
  • Strategy Pattern
  • +
  • Template Pattern
  • +
  • Interpreter Pattern
  • +
  • Memento Pattern
  • +
  • Visitor Pattern
  • +
+ +

總結

+ +

在本篇文章中,我們深入探討了設計模式的核心概念,包括Context、Forces、Problem和Solution。我們也學習了如何透過一系列的步驟來應用設計模式,從物件導向程式分析(OOA)開始,到最終透過UML圖來視覺化設計方案的改進。這些步驟不僅幫助我們更清晰地理解設計模式的應用過程,也為我們提供了一個結構化的方法來解決軟體開發中遇到的設計問題。

+ +

在接下來的文章中,我們將專注於UML圖的介紹和應用。UML(統一建模語言)是一種標準的圖形語言,用於規劃和視覺化軟體系統的設計。我們將學習如何使用UML圖來表示系統的結構和行為,這將進一步加深我們對設計模式應用的理解。敬請期待!

+ +
+

下一篇:深入UML圖 - 設計模式的視覺化工具

+
+ +

design_pattern_design_principle_architecture

+ +
+

Object-Oriented Concepts -> Design Principle -> Design Pattern

+
+ +

參考

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長 🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-4-uml/index.html b/design pattern/design-pattern-4-uml/index.html new file mode 100644 index 0000000..1c17bd9 --- /dev/null +++ b/design pattern/design-pattern-4-uml/index.html @@ -0,0 +1,1038 @@ + + + + + + +Design Pattern (4) - UML (統一建模語言) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (4) - UML (統一建模語言) + + +

+ +

深入了解UML,學習如何用UML圖清晰展現設計模式,提升軟體設計能力。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

UML (Unified Modeling Language)

+ +

UML 是一種用視覺圖形化來規劃建構軟體的方法。

+ +

design_pattern_4_uml

+ +
+

不要急著寫程式,尤其是遇到較複雜的功能,先思考如何設計架構畫出 UML 圖,程式才會具有可讀性、維護性及擴展性。

+
+ +

Class 類別

+ +

如圖分為三列依序是

+ +
    +
  1. Class 名稱
  2. +
  3. Attribute 屬性
  4. +
  5. Operations 方法
  6. +
+ +

design_pattern_4_uml_class

+ +

Interface 介面

+ +

Interface 有兩種表示法

+ +

一般表示法

+ +

一般型式與 Class 並無太大區別,只要在 Class Name 上方標註 <<interface>> 即可

+ +

design_pattern_4_uml_interface_1

+ +

棒棒糖表示法

+ +

用球狀來表示介面

+ +

design_pattern_4_uml_interface_2

+ +

Attribute 屬性

+ +

Visibility 可視範圍

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
SignModifiers
+Public
#Protected
~Package
-Private
+ +

design_pattern_4_uml_interface_2

+ +

Multiplicity 關聯多重性

+ +

Object 之間的數量關係,預設為 1

+ + + + + + + + + + + + + + + + + + + + + + +
Signamount
11 個
*無限多個
n...m至少 n 個,至多 m 個
+ +

Dependency 依賴

+ +
    +
  • 表示不同對象之間相互依賴關係
  • +
  • 通常用於方法的參數或回傳值
  • +
  • A uses a B
  • +
  • 箭頭指向要依賴的對象
  • +
  • 虛線 + 箭頭 表示
  • +
+ +

design_pattern_4_uml_dependency_sign

+ +

動物使用(依賴)氧氣呼吸生存

+ +

design_pattern_4_uml_dependency

+ +

Association 關聯

+ +
    +
  • 表示一個對象擁有另一個對象
  • +
  • 通常用於屬性、全域變數
  • +
  • A has a C
  • +
  • Aggregation、Composition 為子集
  • +
  • 箭頭指向要關聯的對象
  • +
  • 實線 + 箭頭 表示
  • +
+ +

design_pattern_4_uml_association_sign

+ +

每個人有(關聯)一個地址

+ +

design_pattern_4_uml_association

+ +

Aggregation 聚合

+ +
    +
  • 表示一個對象擁有另一個對象
  • +
  • A owns a B
  • +
  • Association 為超集、Composition 為子集
  • +
  • 菱形指向要聚合的對象
  • +
  • 弱關聯,關聯及被關聯對象可互相獨立存在
  • +
  • 實線 + 空心菱形 表示
  • +
+ +

design_pattern_4_uml_aggregation_sign

+ +

人擁有(聚合)衣服,人和衣服可以單獨存在

+ +

design_pattern_4_uml_aggregation

+ +

Composition 組合

+ +
    +
  • 表示一個對象擁有另一個對象
  • +
  • C is a part of A
  • +
  • Association、Aggregation 為超集
  • +
  • 菱形指向要組合的對象
  • +
  • 強關聯,被關聯對象不可獨立存在
  • +
  • 實線 + 實心菱形 表示
  • +
+ +

design_pattern_4_uml_composition_sign

+ +

人類有器官,人死了器官就無作用不存在了

+ +
+

(這邊先不討論器官可移植到別人身上的情況 😂 )

+
+ +

design_pattern_4_uml_composition

+ +

Association、Aggregation 及 Composition 三者關係

+ +
+

Aggregation and Composition are subsets of association meaning they are specific cases of association. In both aggregation and composition object of one class “owns” object of another class. But there is a subtle difference:

+ +
    +
  • Aggregation implies a relationship where the child can exist independently of the parent. Example: Class (parent) and Student (child). Delete the Class and the Students still exist.
  • +
  • Composition implies a relationship where the child cannot exist independent of the parent. Example: House (parent) and Room (child). Rooms don’t exist separate to a House.
  • +
+
+ +

design_pattern_4_uml_compare_association_aggregation_composition

+ +

Realization / Implementation 實現 / 實作

+ +
    +
  • 表示一個對象實作另一個對象
  • +
  • B implements A
  • +
  • 箭頭指向 interface
  • +
  • 虛線 + 空心箭頭 表示
  • +
+ +

design_pattern_4_uml_realization_implementation_sign

+ +

心、肝、胃、腸要實作器官

+ +

design_pattern_4_uml_realization_implementation

+ +

Generalization / Inheritance 泛化 / 繼承

+ +
    +
  • 表示一個對象繼承另一個對象
  • +
  • C is-a A
  • +
  • 箭頭指向 父類別
  • +
  • 實線 + 空心箭頭 表示
  • +
+ +

design_pattern_4_uml_generalization_inheritance_sign

+ +

人是一種動物

+ +

design_pattern_4_uml_generalization_inheritance

+ +

總結

+ +

之後的 Design Pattern 系列文章會大量使用到 UML 圖,搞懂這些圖及箭頭的含義在軟體設計上是非常有幫助的,下一篇終於要進入第一個 Design Pattern。

+ +

參考

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長 🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-5-simple-factory-pattern/index.html b/design pattern/design-pattern-5-simple-factory-pattern/index.html new file mode 100644 index 0000000..956a593 --- /dev/null +++ b/design pattern/design-pattern-5-simple-factory-pattern/index.html @@ -0,0 +1,982 @@ + + + + + + +Design Pattern (5) - Simple Factory Pattern (簡單工廠模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (5) - Simple Factory Pattern (簡單工廠模式) + + +

+ +

通過飲料點餐系統案例,學習如何使用簡單工廠模式提升程式碼的可讀性和維護性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

我們的目標是創建一套能夠根據用戶選擇動態生成飲料對象的點餐系統。首先,讓我們通過UML來分析系統的基本結構。

+ +

物件導向分析 (OOA)

+ +

design_pattern_simple_factory_pattern_uml_1

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

察覺 Forces

+ +

隨著飲料店越來越多新飲品,我們也需要修改 order 方法,但這樣容易影響不會變動的程式碼,於是我們需要找出 需要變動 以及 不需變動 的程式碼,把它們分隔開來

+ +

需要變動的程式碼

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

不需變動的程式碼

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

找出後該如何做呢,這時候需要用到 簡單工廠模式 來將其分離

+ +

套用 Solution

+ +

套用 Simple Factory Pattern 得到新的 Context (Resulting Context)

+ +

先來看一下 Simple Factory Pattern 的 UML

+ +

design_pattern_simple_factory_pattern_uml_3

+ +

其實就是定義一個工廠類別來專門處理創建物件的邏輯 +我們來將飲料點餐系統套用 Simple Factory Pattern

+ +

design_pattern_simple_factory_pattern_uml_2

+ +

物件導向程式設計 (OOP)

+ +

再來我們就可以開始進行物件導向程式開發

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

透過簡單工廠模式,我們就將 需要變動 以及 不需變動 的程式碼成功分隔開來,當要修改菜單時,只需修改 BeverageFactory 即可,不會影響到其他程式碼。

+ +
+

簡單工廠其實不是設計模式,反而比較像是一種編成習慣

+ +

有些開發者的確是把這個編成習慣誤認為 工廠模式 (Factory Pattern)

+ +

不要因為簡單工廠不是一個 真正的 模式,就忽略了它的用法。

+ +

– Head First Design Pattern Ch.4 P.117

+
+ +

總結

+ +

簡單工廠雖然不是 23 個設計模式之中的一種,但它非常簡單,且能訓練我們將變動及不會變動的程式碼分離的習慣 +來看一下我們在簡單工廠用到了哪些 Design Principle

+ +
    +
  • Encapsulate What Varies
  • +
  • Single Responsibility Principle
  • +
+ +

下一篇正式進入 23 個 Design Pattern 的第一個 Factory Method Pattern 工廠方法模式

+ +

參考

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-6-factory-method-pattern/index.html b/design pattern/design-pattern-6-factory-method-pattern/index.html new file mode 100644 index 0000000..19321c7 --- /dev/null +++ b/design pattern/design-pattern-6-factory-method-pattern/index.html @@ -0,0 +1,944 @@ + + + + + + +Design Pattern (6) - Factory Method Pattern (工廠方法模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (6) - Factory Method Pattern (工廠方法模式) + + +

+ +

深入探討工廠方法模式,通過實例展示其應用,提升程式碼的靈活性和可擴展性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

引言:一個全球化的挑戰

+ +

想像一下,你的飲料點餐系統在全球範圍內大受歡迎。隨著業務的擴展,你面臨著一個挑戰:如何滿足不同地區顧客的特定偏好?

+ +

上一篇我們利用簡單工廠模式模式成功地將需要變動 以及 不需變動 的程式碼分離。今天,我們將探討如何進一步提升我們系統的靈活性和擴展性。

+ +

需求:滿足全球化的味蕾

+ +

飲料點餐系統受到客戶的喜愛,業績非常好,於是客戶在世界各地迅速擴店。但很快的問題出現了——不同地區的顧客有著不同的偏好。

+ +
    +
  • 案例分析: +
      +
    • 美國喜歡錫蘭紅茶
    • +
    • 歐洲喜歡伯爵紅茶
    • +
    +
  • +
+ +

我們的目標是,不增加過多成本的同時,滿足這些多樣化的需求。 +(成本考量我們不將所有紅茶種類都加入菜單,只用最符合當地口味的茶葉製作紅茶)

+ +

物件導向分析(OOA)

+ +

design_pattern_factory_method_pattern_uml_1

+ +

於是我們修改簡單工廠的程式碼,新增 USBeverageFactory 及 EUBeverageFactory 來製作符合美國及歐洲當地口味的飲品

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

察覺 Forces

+ +

這樣做雖然可以滿足分店從不同工廠取得該地區的飲品,但每當有新的分店加入,就必須動到 BeverageShop 的程式碼來添加新的分店工廠,違反了 Open Closed Principle

+ +

套用 Solution

+ +

看清楚整個 Context,察覺 Forces 後,就可以套用 Factory Method Pattern 來解決這個問題

+ +

先來看一下 Factory Method Pattern 的 UML

+ +

design_pattern_factory_method_pattern_uml_2

+ +

提供一個介面用來創建物件,真正實體化的類別由子類別實作決定。 +讓我們修改一下上面的 UML

+ +

design_pattern_factory_method_pattern_uml_3

+ +

如此我們就得到了一個全新的 Resulting Context

+ +

物件導向程式設計 (OOP)

+ +

再來我們就可以開始進行物件導向程式開發

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

透過工廠方法模式,我們透過將工廠抽象化,達到可擴充性,之後如果要拓展其他分店像是日本分店,只需新增一個 JPBeverageFactory ,就能創建能做出符合日本人口味的飲料工廠,而不需修改到其他不需變動的程式碼。

+ +

總結

+ +

通過工廠方法模式,我們可以在不犧牲系統整體架構的前提下,靈活地擴展我們的產品線,滿足全球化市場的需求。這不僅提升了我們系統的可維護性和擴展性,也為我們的業務帶來了更大的機會。

+ +

我們來看看工廠方法用到哪些 Design Principle

+ +
    +
  • Encapsulate What Varies
  • +
  • Loose Coupling
  • +
  • Program to Interfaces
  • +
  • Single Responsibility Principle
  • +
  • Open Closed Principle
  • +
  • Dependency Inversion Principle
  • +
+ +

下一篇要介紹最後一個工廠模式 Abstract Factory Pattern 抽象工廠模式

+ +

參考

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-7-abstract-factory-pattern/index.html b/design pattern/design-pattern-7-abstract-factory-pattern/index.html new file mode 100644 index 0000000..988c599 --- /dev/null +++ b/design pattern/design-pattern-7-abstract-factory-pattern/index.html @@ -0,0 +1,1059 @@ + + + + + + +Design Pattern (7) - Abstract Factory Pattern (抽象工廠模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (7) - Abstract Factory Pattern (抽象工廠模式) + + +

+ +

探索如何使用抽象工廠模式創建一系列相關或依賴的物件,提升設計靈活性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

引言:全球化的挑戰擴展

+ +

想像一下,隨著你的飲料點餐系統在全球範圍內的擴展,你面臨著如何滿足不同地區顧客特定偏好的挑戰。

+ +

需求:滿足全球化的味蕾

+ +

隨著業務的全球化擴展,不同地區的顧客有著不同的偏好。且我們也不能只賣紅茶及綠茶,需要為我們的菜單增加新的飲品,一邊新增菜單一邊擴展店舖。

+ +

物件導向分析(OOA)

+ +

design_pattern_factory_method_pattern_uml_3

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

如何處理多個產品在不同分店的組合,這時就需要用到 Abstract Factory Pattern

+ +

察覺 Forces

+ +

當我們每增加一種飲品到菜單中,我們必須要修改所有的 Factory 中的方法,違反了 Open Closed Principle

+ +

套用 Solution

+ +

看清楚整個 Context,察覺 Forces 後,就可以套用 Abstract Factory Pattern 來解決這個問題

+ +

先來看一下 Abstract Factory Pattern 的 UML

+ +

design_pattern_abstract_factory_pattern_uml_1

+ +

透過將工廠抽象,使子類別能創建一系列的實體物件。

+ +

抽象工廠有個重要的判斷方式,當你所要創建的產品是一整個系列產品,且不同需求要創建不同系列,這個關係能夠畫成二維關係,這時就非常適合使用抽象工廠來建立產品

+ +

如下圖

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Country / TeaBlackTeaGreenTeaMilkTea
US FlavorCeylon(錫蘭)Gyokuro(玉露)Thai (泰奶)
EU FlavorEarlGrey(伯爵)Sencha(煎茶)Masala Chai (印度馬薩拉))
JP FlavorAssam(阿薩姆)Matcha(抹茶)Hokkaido(北海道奶茶)
+ +

讓我們根據上面的茶家族修改一下 UML 及程式碼吧(這邊只是要表達二維關係的概念,僅先實作紅茶及綠茶的部分)

+ +

design_pattern_abstract_factory_pattern_uml_2

+ +

如此我們就得到了一個全新的 Resulting Context

+ +

物件導向程式設計 (OOP)

+ +

再來我們就可以開始進行物件導向程式開發

+ + +
    + +
  • + + +
  • + +
  • + + +
  • + +
+ +

使用抽象工廠後,分店不需要知道實際是什麼茶,只要知道跟自己地區的飲料工廠取得 紅/綠/奶茶,這邊也運用到了 Dependency Inversion Principle,工廠及產品兩者皆依賴於抽象。

+ +

補充說明

+ +

下面舉幾種二維關係可以使用 Abstract Factory Pattern 的例子

+ +
+ +

做跨平台應用時,會遇到不同平台與各種 UI 元件的組合

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
OS / UI ComponentsButtonCheckbox
LinuxLinuxButtonLinuxCheckbox
MacOSMacButtonMacCheckbox
WindowsWinButtonWinCheckbox
+ +

做 App 時,會遇到需要支持 Light/Dark Mode 與各種 UI 元件的組合

+ + + + + + + + + + + + + + + + + + + + + +
Theme / UI ComponentsButtonCheckbox
Light ModeLightModeButtonLightModeCheckbox
Dark ModeDarkModeButtonDarkModeCheckbox
+ +

做 IoT 系統時,會遇到 ZWave/Zigbeee 傳輸協議與各種 Iot 裝置的組合

+ + + + + + + + + + + + + + + + + + + + + + + + +
Protocol / DeviceDimmerHueThermostat
ZWaveZWDimmerZWHueZWThermostat
ZigbeeZBDimmerZBHueZBThermostat
+ +

Factory Method Pattern vs Abstract Factory Pattern

+ +

Factory Method Pattern 工廠方法模式

+ +

對每一種產品提供相應的工廠去建立產品,產品擴充性高。

+ +

Abstract Factory Pattern 抽象工廠模式

+ +

對一整個系列的產品進行抽象建立,工廠擴充性高,如加入新的系列產品,但產品擴充性低,所有的工廠都必須加入新產品。

+ +

總結

+ +

在本文中,我們探討了工廠方法模式和抽象工廠模式的區別。工廠方法模式專注於單一產品的建立,提供高產品擴充性;而抽象工廠模式則針對一系列產品提供建立機制,提供工廠的高擴充性但產品擴充性較低。

+ +

我們來看看工廠方法用到哪些 Design Principle

+ +
    +
  • Encapsulate What Varies
  • +
  • Loose Coupling
  • +
  • Program to Interfaces
  • +
  • Single Responsibility Principle
  • +
  • Open Closed Principle
  • +
  • Dependency Inversion Principle
  • +
+ +

參考

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-8-builder-pattern/index.html b/design pattern/design-pattern-8-builder-pattern/index.html new file mode 100644 index 0000000..68f2554 --- /dev/null +++ b/design pattern/design-pattern-8-builder-pattern/index.html @@ -0,0 +1,1000 @@ + + + + + + +Design Pattern (8) - Builder Pattern (建造者模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (8) - Builder Pattern (建造者模式) + + +

+ +

探索建造者模式,學習如何分步構建複雜對象,使程式碼更加靈活和易於維護。通過實例展示如何使用建造者模式簡化對象創建過程,提升程式碼的可讀性和可擴展性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

需求

+ +

今天我們要設計一個能自動做出手搖飲的機器,但如果手搖飲店只賣紅茶、綠茶,肯定滿足不了廣大的客群需求,因此我們要能夠讓手搖飲加入各種配料,來吸引顧客。

+ +
    +
  • 珍珠 (Pearls)
  • +
  • 椰果 (Coconut Jelly)
  • +
  • 紅豆 (Red Beans)
  • +
  • 仙草凍 (Grass Jelly)
  • +
  • 布丁 (Pudding)
  • +
+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

design_pattern_builder_pattern_uml_1

+ +

但這麼做會有一個問題,假如我們今天只要加入紅豆以及布丁,就必須在其他用不到的參數傳入 false or null,參數越多越難以維護且可讀性也不高。

+ +

因此聰明的你可能想到了,可以利用寫多個不同的 constructor 來解決,如此就不需傳入不需要的參數。

+ +

design_pattern_builder_pattern_uml_2

+ +

察覺 Forces

+ +

這邊我們會發現當參數越多, 所需寫的 constructor 就越多,這樣既不好維護,也使得類別的實例化過程錯綜複雜,這個現象可以稱為 telescoping constructor

+ +
+

Telescoping constructor 是當一個類別有多個構造器,每個構造器參數數量不同,導致類別難以維護和使用的問題。

+
+ +

套用 Builder Pattern ( Solution ) 得到新的 Context ( Resulting Context )

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Builder Pattern 解決這個問題

+ +

先來看一下 Builder Pattern 的 UML

+ +

design_pattern_builder_pattern_uml_3

+ +

建構者模式主要包含以下四個角色:

+ +
    +
  1. Product(產品):Product 是 Builder Pattern 負責構建的複雜對象。它可能包含多個組件或部分,其結構根據實現的不同而變化。Product 通常是一個類,其屬性代表 Builder 構建的不同部分。
  2. +
  3. Builder(建造者):Builder 是一個介面或抽象類,宣告了構建複雜對象的步驟。它通常包括用於構建產品各個部分的方法。通過定義一個介面,Builder 允許創建不同的具體建造者,這些建造者可以生產產品的不同變體 1。
  4. +
  5. ConcreteBuilder(具體建造者):ConcreteBuilder 類實現了 Builder 介面,提供了構建產品每個部分的具體實現。每個 ConcreteBuilder 都是為創建產品的特定變體而量身定制的。它跟踪正在構建的產品,並提供設置或構建每個部分的方法 1。
  6. +
  7. Director(指導者):Director 負責管理複雜對象的構建過程。它與 Builder 合作,但不知道對象的每個部分是如何構建的。它提供了一個高級介面,用於構建產品和管理創建複雜對象所需的步驟 1。
  8. +
  9. Client(客戶端):Client 是啟動複雜對象構建過程的程式碼。它創建一個 Builder 對象並將其傳遞給 Director 以啟動構建過程。在構建完成後,Client 可能會從 Builder 那裡檢索最終產品 1。
  10. +
+ +

我們來將製作手搖飲套用 Builder Pattern

+ +

design_pattern_builder_pattern_uml_4

+ +

如此我們就得到了一個全新的 Resulting Context

+ +

物件導向程式設計 (OOP)

+ +

再來我們就可以開始進行物件導向程式開發

+ +

[Beverage]

+ +
interface Beverage {
+    var hasPearls: Boolean
+    var hasCoconutJelly: Boolean
+    var hasRedBeans: Boolean
+    var hasGrassJelly: Boolean
+    var hasPudding: Boolean
+}
+
+ +

[BubbleTea]

+ +
data class BubbleTea(override var hasPearls: Boolean,
+                     override var hasCoconutJelly: Boolean = false,
+                     override var hasRedBeans: Boolean = false,
+                     override var hasGrassJelly: Boolean = false,
+                     override var hasPudding: Boolean = false
+): Beverage {
+}
+
+ +

[GrassJellyPuddingTea]

+ +
data class GrassJellyPuddingTea(override var hasPearls: Boolean = false,
+                     override var hasCoconutJelly: Boolean = false,
+                     override var hasRedBeans: Boolean = false,
+                     override var hasGrassJelly: Boolean,
+                     override var hasPudding: Boolean
+): Beverage {
+}
+
+ +

[Builder]

+ +
interface Builder {
+    fun addPearls(): Builder
+    fun addPudding(): Builder
+    fun addGrassJelly(): Builder
+
+    fun build(): Beverage
+}
+
+ +

[BubbleTeaBuilder]

+ +
class BubbleTeaBuilder: Builder {
+    private var bubbleTea = BubbleTea(false)
+
+    override fun addPearls(): BubbleTeaBuilder {
+        bubbleTea.hasPearls = true
+        return this
+    }
+
+    override fun addPudding(): Builder {
+        return this
+    }
+
+    override fun addGrassJelly(): Builder {
+        return this
+    }
+
+    override fun build(): BubbleTea {
+        return bubbleTea
+    }
+}
+
+ +

[GrassJellyPuddingTeaBuilder]

+ +
class GrassJellyPuddingTeaBuilder: Builder {
+
+    private var grassJellyPuddingTea = GrassJellyPuddingTea(
+        false,
+        hasCoconutJelly = false,
+        hasRedBeans = false,
+        hasGrassJelly = false,
+        hasPudding = false
+    )
+
+    override fun addPearls(): Builder {
+        return this
+    }
+
+    override fun addGrassJelly(): GrassJellyPuddingTeaBuilder {
+        grassJellyPuddingTea.hasGrassJelly = true
+        return this
+    }
+
+    override fun addPudding(): GrassJellyPuddingTeaBuilder {
+        grassJellyPuddingTea.hasPudding = true
+        return this
+    }
+
+    override fun build(): GrassJellyPuddingTea {
+        return grassJellyPuddingTea
+    }
+}
+
+ +

[BeverageMaker]

+ +
class BeverageMaker(val builder: Builder) {
+    fun makeBubbleTea(): Beverage {
+        return builder.addPearls().build()
+    }
+
+    fun makeGrassJellyPuddingTea(): Beverage {
+        return builder.addGrassJelly().addPudding().build()
+    }
+}
+
+ +

[BuilderPattern.kt]

+ +
fun main() {
+    val bubbleTeaBuilder = BubbleTeaBuilder()
+    val bubbleTeaBeverageMaker = BeverageMaker(bubbleTeaBuilder)
+    val bubbleTea = bubbleTeaBeverageMaker.makeBubbleTea()
+    println(bubbleTea)
+
+    val grassJellyPuddingTeaBuilder = GrassJellyPuddingTeaBuilder()
+    val grassJellyPuddingTeaBeverageMaker = BeverageMaker(grassJellyPuddingTeaBuilder)
+    val grassJellyPuddingTea = grassJellyPuddingTeaBeverageMaker.makeGrassJellyPuddingTea()
+    println(grassJellyPuddingTea)
+}
+
+ +

如此就能很清楚的分步驟製作手搖飲了 🙌

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design pattern/design-pattern-9-prototype-pattern/index.html b/design pattern/design-pattern-9-prototype-pattern/index.html new file mode 100644 index 0000000..8522c04 --- /dev/null +++ b/design pattern/design-pattern-9-prototype-pattern/index.html @@ -0,0 +1,935 @@ + + + + + + +Design Pattern (9) - Prototype Pattern (原型模式) - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Design Pattern (9) - Prototype Pattern (原型模式) + + +

+ +

深入原型模式:探索如何透過物件複製技術,有效提升軟體開發中的資源管理與設計模式的靈活性。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +
+

您可於此 design_pattern repo 下載 Design Pattern 系列程式碼。

+
+ +

前言

+ +

這次的 Pattern 讓我想到以前做過的一個 App,但當時還沒有學習到 Pattern,所以沒有用 Pattern 來處理,現在發現這個功能很適合套用 prototype pattern

+ +

這是一個用來編輯音樂燈光秀的 App,有興趣的讀者可以下載玩玩看 🙂

+ + + +

taptap_app_edit

+ +

需求

+ +

今天收到了客人的需求,客人反應編輯完一條燈光還要編輯另外六條好浪費時間,能不能新增 Copy & Paste 的功能,加快編輯以節省時間,如下圖

+ +

taptap_app_copy

+ +

taptap_app_paste

+ +

物件導向分析 (OOA)

+ +

理解需求後,讓我們來快速實作物件導向分析吧!

+ +

design_pattern_prototype_pattern_uml_1

+ +

當我們需要複製 LightShowData 時,只需要同樣的 jsonObject 資料重新 new 一個 LightShowData 即可複製一份

+ +

察覺 Forces

+ +

來看看上面這樣的設計會有哪些問題

+ +
    +
  1. 如果我們的 constructor 很複雜,參數非常多,那麼重新 new 一個實體會需要知道很多細節。
  2. +
  3. 如果 constructor 創建實體的過程,是很耗時複雜的計算,重新 new 一個實體會讓創建過程效率變差。
  4. +
+ +

套用 Prototype Pattern ( Solution ) 得到新的 Context ( Resulting Context )

+ +

做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Prototype Pattern 解決這個問題

+ +

先來看一下 Prototype Pattern 的 UML

+ +

design_pattern_prototype_pattern_uml_2

+ +

原型模式主要包含以下兩個角色:

+ +
    +
  1. +

    Prototype(原型):這是一個抽象介面,定義了複製自身的方法。在實體實現中,這個介面可以是一個抽象類或者實體類,主要目的是提供一個複製自己的方法。這使得在不需要知道物件實體類別的情況下也能創建物件的副本。

    +
  2. +
  3. +

    Concrete Prototype(實體原型):實現或繼承自原型介面的類。這個類實現了在原型介面中定義的複製(clone)方法,用於創建自身的一個精確副本。在實體實現時,這個類需要提供一個方法來複製自身的屬性,確保新創建的物件與原有物件在狀態上是相同的,但在記憶體中是獨立的。

    +
  4. +
+ +

我們來將 LightShow App 套用 Prototype Pattern

+ +

design_pattern_prototype_pattern_uml_3

+ +

如此我們就得到了一個全新的 Resulting Context

+ +

物件導向程式設計 (OOP)

+ +

再來我們就可以開始進行物件導向程式開發

+ +

[LightShowDataPrototype]

+ +
interface LightShowDataPrototype {
+    val startIndex: Int
+    val lightDataList: List<Int>
+    fun clone(): LightShowDataPrototype
+}
+
+ +

[LightShowData]

+ +
package prototypepattern.source
+
+class LightShowData: LightShowDataPrototype {
+
+    override val startIndex: Int
+    override val lightDataList: List<Int>
+
+    constructor(originalDataList: List<Int>) {
+        startIndex = originalDataList[0]
+        lightDataList = originalDataList.subList(1, originalDataList.size).map { it * 2 }
+    }
+
+    constructor(startIndex: Int, lightDataList: List<Int>) {
+        this.startIndex = startIndex
+        this.lightDataList = lightDataList
+    }
+
+    override fun clone(): LightShowDataPrototype {
+        return LightShowData(startIndex, lightDataList.toList())
+    }
+}
+
+ +

[main]

+ +
fun main() {
+    val originalData = listOf(1, 2, 3, 4, 5)
+
+    // Before using prototype pattern
+    val originalLightShowData: LightShowDataPrototype = LightShowData(originalData)
+    val newLightShowData: LightShowDataPrototype = LightShowData(originalData)
+
+    println(originalLightShowData)
+    println(newLightShowData)
+
+    // After using prototype pattern
+    val clonedLightShowData: LightShowDataPrototype = LightShowData(originalData)
+
+    println(originalLightShowData)
+    println(clonedLightShowData)
+}
+
+ +

我們可以發現,透過 clone() 方法複製,就可以不重複執行下面的程式碼,提升程式碼效能了

+ +
originalDataList.subList(1, originalDataList.size).map { it * 2 }
+
+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/devops/getting-started-with-github-container-registry/index.html b/devops/getting-started-with-github-container-registry/index.html new file mode 100644 index 0000000..79c2b01 --- /dev/null +++ b/devops/getting-started-with-github-container-registry/index.html @@ -0,0 +1,1056 @@ + + + + + + +Getting Started with GitHub Container Registry - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Getting Started with GitHub Container Registry + + +

+ +

A Guide to Using and Managing Container Images +

+ + + +

+ + + + + + + + + + + + + + + + + + + 3 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

為什麼會寫這篇文章

+ +

隨著公司專案數量增加,每個專案的環境需求也變得更加多樣化。我們決定將原本使用Docker建置的Android Jenkins Server轉型為更靈活的架構:一個主要的Jenkins Server(Master)搭配多個Android Build Environment(Slave),後者透過Docker創建乾淨的環境。這篇文章旨在記錄此過程,不僅作為個人學習的回顧,也希望能對其他開發者提供幫助。

+ +

文章簡介

+ +

本文將引導初學者及希望深入了解如何將GitHub的新工具融入CI/CD流程的開發者,透過簡明的指南和實用的技巧,學習如何將容器映像推送至GitHub Container Registry。我將一步步展示如何設定GitHub Actions,自動化構建與部署過程,讓你的開發工作變得更加高效。

+ +

開始之前

+ +

在深入主題之前,讓我們先透過 express 框架,快速搭建一個運行於Node.js上的簡易應用。

+ +

Create a node_sample folder

+ +
mkdir node_sample
+cd node_sample
+
+ +

Install node package express

+ +
npm init -y
+npm install express
+
+ +

Create an app.js file

+ +
vim app.js
+
+ +
const express = require('express');
+const app = express();
+const port = 3000;
+
+app.get('/', (req, res) => {
+    res.send('Hello, World!');
+});
+
+app.listen(port, () => {
+    console.log(`Example app listening at http://localhost:${port}`);
+});
+
+ +

Create a .gitignore file

+ +
+

請至 gitignore.io 產生

+
+ +

Run app.js

+ +
node app.js
+
+ +

在 chrome 上打開網址 localhost:3000 就會看到如下

+ +

github_container_registry_sample_website

+ +

Create a Dockerfile

+ +
vim Dockerfile
+
+ +
FROM node:latest
+WORKDIR /usr/src/app
+COPY package*.json app.js ./
+RUN npm install
+EXPOSE 3000
+CMD ["node", "app.js"]
+
+ +

上傳 Docker Image 到 Github Container Registry

+ +

寫完 Dockerfile 後有兩種方式可以將 image 上傳到 Github Container Registry 上

+
    +
  1. 用 Command line 方式手動上傳
  2. +
  3. 用 Github Action 方式,當 repository 有 Commit 時,自動執行 Github Action 上傳
  4. +
+ +

用 Command line 方式手動上傳

+ +

利用 Dockerfile 產生 image

+ +
docker build -t node_sample .
+
+ +

github_container_registry_docker_build_image

+ +

查看 Docker images

+ +
docker images
+
+ +

github_container_registry_docker_images

+ +

創建 image tag

+ +
docker tag node_sample:latest ghcr.io/{NAMESPACE}/node_sample:latest
+
+ +
+

記得將 NAMESPACE 取代成你的 Github account name

+
+ +

Generate Personal access tokens (classic)

+ +

準備好 image 後,在我們推上 Github Container Registry 前,我們需要取得 GITHUB_TOKEN 用來登入 Github Container Registry

+ +
+

打開 Github 網頁 -> 右上角 Profile -> Settings -> Developer settings -> Personal access tokens -> Tokens (classic) -> Generate new token

+
+ +

勾選 write:packages , read:packagesdelete:packages scopes,如下圖

+ +

github_container_registry_generate_github_token

+ +

點擊 Generate token,將 token 複製起來

+ +

登入 Github Container Registry

+ +

將剛剛複製的 token 存到環境變數

+ +
export CR_PAT=YOUR_TOKEN
+
+ +

登入 Container registry service at ghcr.io

+ +
> echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
+Login Succeeded
+
+ +

上傳 Images

+ +
docker push ghcr.io/{NAMESPACE}/node_sample:latest
+
+ +

回到 Github 網頁上點擊 Packages,就能看到上傳成功了

+ +

github_container_registry_github_package

+ +

用 Github Action 方式

+ +

使用 Github Action 的方式更簡單,在 node_sample 目錄下

+ +

Create a deploy-image.yml

+ +
mkdir .github
+cd .github
+mkdir workflows
+cd workflows
+vim deploy-image.yml
+
+ +
#
+name: Create and publish a Docker image
+
+# Configures this workflow to run every time a change is pushed to the branch called `release`.
+on:
+  push:
+    branches: ["release"]
+
+# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
+env:
+  REGISTRY: ghcr.io
+  IMAGE_NAME: $
+
+# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
+jobs:
+  build-and-push-image:
+    runs-on: ubuntu-latest
+    # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
+    permissions:
+      contents: read
+      packages: write
+      attestations: write
+      id-token: write
+      #
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+      # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
+      - name: Log in to the Container registry
+        uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
+        with:
+          registry: $
+          username: $
+          password: $
+      # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
+        with:
+          images: $/$
+      # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
+      # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
+      # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
+      - name: Build and push Docker image
+        id: push
+        uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
+        with:
+          context: .
+          push: true
+          tags: $
+          labels: $
+
+      # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
+      - name: Generate artifact attestation
+        uses: actions/attest-build-provenance@v1
+        with:
+          subject-name: $/$
+          subject-digest: $
+          push-to-registry: true
+
+
+ +

Pushing node_sample repository

+ +

將 node_sample 的 source code 推到你的 Github 上,如此每次只要 release branch 有 commit 就會觸發 Github Action 去自動 build image 並上傳到 Github Container Registry

+ +

github_container_registry_github_package

+ +

下載 image

+ +
    +
  • Install from the command line
  • +
+ +
docker pull ghcr.io/nickhuangcyh/node_sample:TAG
+
+ +
    +
  • Use as base image in Dockerfile:
  • +
+ +
FROM ghcr.io/nickhuangcyh/node_sample:TAG
+
+ +

github_container_registry_download_image

+ +
+

您可於此 node_sample 下載 node_sample 的程式碼。

+
+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/devops/jenkins-1-what-is-jenkins/index.html b/devops/jenkins-1-what-is-jenkins/index.html new file mode 100644 index 0000000..fde5fb1 --- /dev/null +++ b/devops/jenkins-1-what-is-jenkins/index.html @@ -0,0 +1,840 @@ + + + + + + +Jenkins (1) - 什麼是 Jenkins - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

什麼是 Jenkins

+ +

Jenkins 是一個開源的自動化伺服器,主要用於實現持續整合(CI)和持續交付(CD)。它能夠自動化各種任務,包括建置、測試和部署軟體,從而幫助開發團隊提升效率和品質。

+ +

為什麼選擇 Jenkins

+ +
    +
  1. 開源且免費:Jenkins 是一個開源專案,任何人都可以免費使用和修改。
  2. +
  3. 豐富的插件生態系統:Jenkins 擁有超過 1,500 個插件,能夠擴展其功能以滿足不同的需求。
  4. +
  5. 社群支持:Jenkins 擁有一個活躍的社群,提供豐富的資源和支援。
  6. +
  7. 易於整合:Jenkins 可以與多種工具和平台整合,如 Git、Docker、Kubernetes 等。
  8. +
+ +

Jenkins 的核心概念

+ +
    +
  1. Pipeline:Jenkins Pipeline 是一套插件,支持實現和集成持續交付流水線。Pipeline 用程式碼來定義整個建置過程,包括建置、測試和部署。
  2. +
  3. Node:Node 是 Jenkins 執行工作的機器,可以是 Jenkins 主伺服器或其他代理伺服器。
  4. +
  5. Job:Job 是 Jenkins 中的基本建置單位,定義了具體的建置、測試和部署任務。
  6. +
  7. Executor:Executor 是 Jenkins 用來執行 Job 的工作單元,每個 Node 可以有多個 Executor。
  8. +
+ +

總結

+ +

Jenkins 是一個功能強大且靈活的自動化伺服器,能夠幫助開發團隊實現持續整合和持續交付。通過使用 Jenkins,團隊可以顯著提高開發效率和軟體品質。如果你還沒有使用 Jenkins,現在就是開始的好時機!

+ +
+

想了解更多關於 Jenkins 的資訊,請參考 Jenkins 官方文件

+
+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/devops/jenkins-2-how-to-setup-jenkins-server/index.html b/devops/jenkins-2-how-to-setup-jenkins-server/index.html new file mode 100644 index 0000000..44b6a5b --- /dev/null +++ b/devops/jenkins-2-how-to-setup-jenkins-server/index.html @@ -0,0 +1,862 @@ + + + + + + +Jenkins (2) - 如何架設 Jenkins 伺服器 - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

如何架設 Jenkins 伺服器

+ +

在這篇文章中,我們將介紹如何使用 Docker 映像檔來架設 Jenkins 伺服器。這種方法不僅簡單快捷,還能確保環境的一致性。

+ +

步驟一:拉取 Docker 映像檔

+ +

首先,我們需要從 GitHub Container Registry 拉取 Jenkins 的 Docker 映像檔。打開終端機並執行以下指令:

+ +
docker pull jenkins/jenkins:lts-jdk17 # 單純 jenkins 環境
+
+ +

or

+ +
docker pull ghcr.io/nickhuangcyh/docker-jenkins-and-android-env:v1.0.0-jdk17 # jenkins 環境 + Android 建構環境
+
+ +

步驟二:運行 Jenkins 容器

+ +

接下來,我們將運行 Jenkins 容器。請確保替換 ${volume path} 為你希望 Jenkins 資料儲存的本地路徑。執行以下指令:

+ +
docker run -d -v ${volume path}:/var/jenkins_home -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts-jdk17 # 單純 jenkins 環境
+
+ +

or

+ +
docker run -d -v ${volume path}:/var/jenkins_home -p 8080:8080 -p 50000:50000 ghcr.io/nickhuangcyh/docker-jenkins-and-android-env:v1.0.0-jdk17 # jenkins 環境 + Android 建構環境
+
+ +

這個指令會在背景運行 Jenkins 容器,並將 Jenkins 的資料儲存在你指定的路徑中。同時,容器會綁定本地的 8080 端口和 50000 端口,分別用於 Jenkins 的 Web 介面和代理通訊。

+ +

步驟三:訪問 Jenkins

+ +

容器啟動後,你可以在瀏覽器中打開 http://localhost:8080 來訪問 Jenkins 的 Web 介面。首次訪問時,系統會要求你輸入初始管理員密碼。

+ +

jenkins_setup_initialAdminPassword

+ +

還記得我們剛剛在run container時有輸入 ${volume path} 嗎? 根據底下路徑 /var/jenkins_home/secrets/initialAdminPassword 去找到 initialAdminPassword 並貼上

+ +

按照指示安裝 Plugin 完成初始配置,就成功架起來拉~🎉。

+ +

jenkins_setup_main_page

+ +

總結

+ +

通過以上步驟,我們成功地使用 Docker 映像檔架設了一個 Jenkins 伺服器。這種方法不僅簡單快捷,還能確保環境的一致性,對於開發團隊來說是一個非常實用的工具。如果你還沒有使用 Jenkins,現在就是開始的好時機!

+ +
+

想了解更多關於 Jenkins 的資訊,請參考 Jenkins 官方文件

+
+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/devops/jenkins-3-configure-credentials-ssh/index.html b/devops/jenkins-3-configure-credentials-ssh/index.html new file mode 100644 index 0000000..9dccea4 --- /dev/null +++ b/devops/jenkins-3-configure-credentials-ssh/index.html @@ -0,0 +1,876 @@ + + + + + + +Jenkins (3) - 如何配置 Credentials 以透過 SSH 從 git 上拉取程式碼 - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Jenkins (3) - 如何配置 Credentials 以透過 SSH 從 git 上拉取程式碼 + + +

+ +

學習如何在 Jenkins 中配置憑證(Credentials),以便透過 SSH 安全地拉取程式碼。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

如何配置 Credentials 以透過 SSH 從 git 上拉取程式碼

+ +

在這篇文章中,我們將介紹如何在 Jenkins 中配置憑證(Credentials),以便 Jenkins 能夠透過 SSH 安全地從版本控制系統(如 GitHub 或 GitLab)拉取程式碼。

+ +

步驟一:生成 SSH 金鑰

+ +

首先,我們需要生成一對 SSH 金鑰。打開終端機並執行以下指令:

+ +
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
+
+ +

按照提示完成金鑰生成過程,並記下生成的公鑰和私鑰的路徑。

+ +

步驟二:將公鑰添加到版本控制系統

+ +

將生成的公鑰內容複製並添加到你的版本控制系統中。例如,在 GitHub 中,你可以按照以下步驟操作:

+ +
    +
  1. 登入 GitHub,進入「Settings」。
  2. +
  3. 在左側菜單中選擇「SSH and GPG keys」。
  4. +
  5. 點擊「New SSH key」,並將公鑰內容貼上,然後點擊「Add SSH key」。
  6. +
+ +

步驟三:在 Jenkins 中添加憑證

+ +
    +
  1. 打開 Jenkins 管理介面:在瀏覽器中打開 http://localhost:8080,並使用管理員帳戶登入。
  2. +
  3. 進入憑證管理:點擊左側菜單中的「Credentials」,然後選擇「System」。
  4. +
  5. 創建新的 Domain:點擊「Domains」旁邊的「Add domain」,並填寫 Domain 名稱(例如 GitHub 或 GitLab),然後點擊「OK」。
  6. +
  7. 新增憑證:選擇剛剛創建的 Domain,然後點擊「Add Credentials」。
  8. +
  9. 填寫憑證資訊:
  10. +
+ +
    +
  • Kind:選擇「SSH Username with private key」。
  • +
  • Scope:選擇「Global」。
  • +
  • ID:為這個憑證設定一個唯一的 ID(可選)。
  • +
  • Description:為這個憑證添加描述,方便日後管理。
  • +
  • Username:填寫你的 SSH 使用者名稱(通常是 git)。
  • +
  • Private Key:選擇「Enter directly」,並將生成的私鑰內容貼上。
  • +
+ +
    +
  1. 保存憑證:點擊「OK」保存憑證。
  2. +
+ +

步驟四:配置 Jenkins Job 使用憑證

+ +
    +
  1. 創建或編輯 Jenkins Job:進入 Jenkins 主頁,創建一個新的 Job 或編輯現有的 Job。
  2. +
  3. 配置版本控制系統:在「Source Code Management」部分,選擇「Git」。
  4. +
  5. 填寫 Repository URL:填寫你的 Git Repository 的 SSH URL,例如 git@github.com:username/repository.git。
  6. +
  7. 選擇憑證:在「Credentials」下拉選單中,選擇剛剛添加的 SSH 憑證。
  8. +
  9. 保存配置:點擊「Save」保存 Job 配置。
  10. +
+ +

總結

+ +

通過以上步驟,我們成功地在 Jenkins 中配置了憑證,使其能夠透過 SSH 安全地拉取程式碼。這樣可以確保你的程式碼在傳輸過程中的安全性,並且簡化了 Jenkins 的自動化流程。如果你還沒有使用 Jenkins,現在就是開始的好時機!

+ +

想了解更多關於 Jenkins 的資訊,請參考 Jenkins 官方文件

+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/feed.xml b/feed.xml new file mode 100644 index 0000000..e817e84 --- /dev/null +++ b/feed.xml @@ -0,0 +1,1773 @@ +Jekyll2024-12-26T16:23:07+00:00https://nickhuangcyh.github.io/blog/feed.xmlNick’s BlogAn amazing website.Nick HuangDesign Pattern (25) - Strategy Pattern (策略模式)2024-12-26T15:50:00+00:002024-12-26T15:50:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-25-strategy-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<hr /> + +<h2 id="需求">需求</h2> + +<p>在設計一個 <strong>電商運費計算系統</strong> 時,我們需要滿足以下需求:</p> + +<ol> + <li>支援多種運費計算方式,例如: + <ul> + <li><strong>一般配送</strong>:固定運費。</li> + <li><strong>快速配送</strong>:依重量計費。</li> + <li><strong>國際配送</strong>:根據地區與重量計費。</li> + </ul> + </li> + <li>系統需具備良好的擴展性: + <ul> + <li>能夠方便地新增新的運費計算方式。</li> + </ul> + </li> + <li><strong>避免使用大量的 if-else 或 switch-case</strong>。</li> + <li>使用者應能輕鬆切換運費計算方式。</li> +</ol> + +<hr /> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_strategy_pattern_uml_1.png" alt="strategy_pattern_uml_1" /></p> + +<h3 id="察覺-forces">察覺 Forces</h3> + +<p>如果未套用設計模式,我們可能會遇到以下問題:</p> + +<ol> + <li> + <p><strong>難以維護</strong></p> + + <ul> + <li>運費計算邏輯混雜在主程式內,新增或修改一種計算方式可能會影響其他部分。</li> + </ul> + </li> + <li> + <p><strong>違反開放關閉原則 (OCP)</strong></p> + + <ul> + <li>每次新增運費計算方式都需修改核心業務邏輯。</li> + </ul> + </li> + <li> + <p><strong>違反單一職責原則 (SRP)</strong></p> + <ul> + <li>主程式同時負責運費計算與核心業務邏輯,責任過於繁重。</li> + </ul> + </li> +</ol> + +<hr /> + +<h2 id="套用-strategy-pattern-solution-得到新的-context-resulting-context">套用 Strategy Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Strategy Pattern 解決這個問題</p> + +<p>先來看一下 Strategy Pattern 的 UML</p> + +<p><img src="/blog/assets/images/design_pattern_strategy_pattern_uml_2.png" alt="strategy_pattern_uml_2" /></p> + +<h3 id="strategy-pattern-的組件">Strategy Pattern 的組件</h3> + +<p>策略模式的核心組件包括:</p> + +<ol> + <li> + <p><strong>Strategy (策略介面)</strong><br /> +定義所有策略需要實現的行為。</p> + </li> + <li> + <p><strong>ConcreteStrategy (具體策略)</strong><br /> +每個具體策略類別實現特定的行為邏輯。</p> + </li> + <li> + <p><strong>Context (上下文)</strong><br /> +維護一個策略物件,並根據當前策略執行對應行為。</p> + </li> +</ol> + +<p>將 Strategy Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_strategy_pattern_uml_3.png" alt="strategy_pattern_uml_3" /></p> + +<hr /> + +<h2 id="物件導向設計-oop">物件導向設計 (OOP)</h2> + +<p>[Strategy: ShippingStrategy]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">ShippingStrategy</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">calculateShippingCost</span><span class="p">(</span><span class="n">weight</span><span class="p">:</span> <span class="nc">Double</span><span class="p">,</span> <span class="n">region</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Double</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteStrategies: RegularShipping, ExpressShipping, InternationalShipping]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">RegularShipping</span> <span class="p">:</span> <span class="nc">ShippingStrategy</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">calculateShippingCost</span><span class="p">(</span><span class="n">weight</span><span class="p">:</span> <span class="nc">Double</span><span class="p">,</span> <span class="n">region</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Double</span> <span class="p">{</span> + <span class="k">return</span> <span class="mf">50.0</span> <span class="c1">// 固定運費</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">ExpressShipping</span> <span class="p">:</span> <span class="nc">ShippingStrategy</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">calculateShippingCost</span><span class="p">(</span><span class="n">weight</span><span class="p">:</span> <span class="nc">Double</span><span class="p">,</span> <span class="n">region</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Double</span> <span class="p">{</span> + <span class="k">return</span> <span class="n">weight</span> <span class="p">*</span> <span class="mi">10</span> <span class="c1">// 每公斤 10 元</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">InternationalShipping</span> <span class="p">:</span> <span class="nc">ShippingStrategy</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">calculateShippingCost</span><span class="p">(</span><span class="n">weight</span><span class="p">:</span> <span class="nc">Double</span><span class="p">,</span> <span class="n">region</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Double</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">regionMultiplier</span> <span class="p">=</span> <span class="k">when</span> <span class="p">(</span><span class="n">region</span><span class="p">)</span> <span class="p">{</span> + <span class="s">"Asia"</span> <span class="p">-&gt;</span> <span class="mi">15</span> + <span class="s">"Europe"</span> <span class="p">-&gt;</span> <span class="mi">20</span> + <span class="s">"America"</span> <span class="p">-&gt;</span> <span class="mi">25</span> + <span class="k">else</span> <span class="p">-&gt;</span> <span class="mi">30</span> + <span class="p">}</span> + <span class="k">return</span> <span class="n">weight</span> <span class="p">*</span> <span class="n">regionMultiplier</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Context: ShippingCalculator]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ShippingCalculator</span><span class="p">(</span><span class="k">private</span> <span class="kd">var</span> <span class="py">strategy</span><span class="p">:</span> <span class="nc">ShippingStrategy</span><span class="p">)</span> <span class="p">{</span> + + <span class="k">fun</span> <span class="nf">setStrategy</span><span class="p">(</span><span class="n">strategy</span><span class="p">:</span> <span class="nc">ShippingStrategy</span><span class="p">)</span> <span class="p">{</span> + <span class="k">this</span><span class="p">.</span><span class="n">strategy</span> <span class="p">=</span> <span class="n">strategy</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">calculateCost</span><span class="p">(</span><span class="n">weight</span><span class="p">:</span> <span class="nc">Double</span><span class="p">,</span> <span class="n">region</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Double</span> <span class="p">{</span> + <span class="k">return</span> <span class="n">strategy</span><span class="p">.</span><span class="nf">calculateShippingCost</span><span class="p">(</span><span class="n">weight</span><span class="p">,</span> <span class="n">region</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> + +</code></pre></div></div> + +<p>[Client]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">calculator</span> <span class="p">=</span> <span class="nc">ShippingCalculator</span><span class="p">(</span><span class="nc">RegularShipping</span><span class="p">())</span> + + <span class="nf">println</span><span class="p">(</span><span class="s">"一般配送運費: ${calculator.calculateCost(5.0, "</span><span class="nc">Asia</span><span class="s">")} 元"</span><span class="p">)</span> <span class="c1">// 固定 50 元</span> + + <span class="n">calculator</span><span class="p">.</span><span class="nf">setStrategy</span><span class="p">(</span><span class="nc">ExpressShipping</span><span class="p">())</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"快速配送運費: ${calculator.calculateCost(5.0, "</span><span class="nc">Asia</span><span class="s">")} 元"</span><span class="p">)</span> <span class="c1">// 5.0 * 10 = 50 元</span> + + <span class="n">calculator</span><span class="p">.</span><span class="nf">setStrategy</span><span class="p">(</span><span class="nc">InternationalShipping</span><span class="p">())</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"國際配送運費 (Asia): ${calculator.calculateCost(5.0, "</span><span class="nc">Asia</span><span class="s">")} 元"</span><span class="p">)</span> <span class="c1">// 5.0 * 15 = 75 元</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Output]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">一般配送運費</span><span class="p">:</span> <span class="mf">50.0</span> <span class="err">元</span> +<span class="err">快速配送運費</span><span class="p">:</span> <span class="mf">50.0</span> <span class="err">元</span> +<span class="err">國際配送運費</span> <span class="p">(</span><span class="nc">Asia</span><span class="p">):</span> <span class="mf">75.0</span> <span class="err">元</span> +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過 Strategy Pattern,我們成功將運費計算邏輯與核心功能分離,並實現以下優勢:</p> + +<ol> + <li>易於擴展</li> +</ol> + +<ul> + <li>新增運費計算方式只需實作新的策略類別,無需修改現有程式碼。</li> +</ul> + +<ol> + <li>低耦合性</li> +</ol> + +<ul> + <li>運費計算邏輯與核心業務邏輯分離,各自負責自己的功能。</li> +</ul> + +<ol> + <li>符合設計原則</li> +</ol> + +<ul> + <li>單一職責原則 (SRP):每個策略類別專注於特定運費計算邏輯。</li> + <li>開放關閉原則 (OCP):策略模式允許在不修改現有程式碼的情況下,新增新功能。</li> +</ul> + +<p>策略模式非常適合處理需要根據條件執行不同行為的場景,例如:</p> + +<ul> + <li>不同的折扣策略 (滿額折扣、季節性優惠)。</li> + <li>不同的排序算法 (快速排序、合併排序)。</li> + <li>各類繳稅計算方式。</li> +</ul> + +<p>策略模式讓系統更具彈性,為複雜問題提供了一個優雅的解決方案。</p>Nick Huang策略模式提供了一種靈活的解決方案,讓系統能根據需求動態切換不同的行為邏輯,實現高可擴展性與低耦合性。Design Pattern (24) - State Pattern (狀態模式)2024-12-22T07:00:00+00:002024-12-22T07:00:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-24-state-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<hr /> + +<h2 id="需求">需求</h2> + +<p>我們的任務是設計一個 <strong>飲水機</strong>,需求如下:</p> + +<ul> + <li>飲水機有三種狀態: + <ul> + <li><strong>加熱中</strong>:提升水溫至熱水。</li> + <li><strong>冷卻中</strong>:降低水溫至冷水。</li> + <li><strong>待機中</strong>:維持現有水溫。</li> + </ul> + </li> + <li>使用者可透過按鈕切換飲水機的狀態。</li> + <li>飲水機需要根據當前狀態執行正確的行為,例如加熱狀態時加熱水,但不可冷卻。</li> +</ul> + +<hr /> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_state_pattern_uml_1.png" alt="state_pattern_uml_1" /></p> + +<h3 id="察覺-forces">察覺 Forces</h3> + +<p>在未使用設計模式的情況下,我們可能面臨以下挑戰:</p> + +<ol> + <li> + <p><strong>高耦合性 (High Coupling)</strong></p> + + <ul> + <li>狀態邏輯與飲水機核心功能混合在一起,導致程式碼難以維護。</li> + </ul> + </li> + <li> + <p><strong>違反單一職責原則 (SRP)</strong></p> + + <ul> + <li>飲水機類別需要同時處理狀態邏輯與主要功能,責任過於繁重。</li> + </ul> + </li> + <li> + <p><strong>難以擴展 (Hard to Extend)</strong></p> + <ul> + <li>新增或修改狀態行為需更改飲水機核心邏輯,違反開放關閉原則 (OCP)。</li> + </ul> + </li> +</ol> + +<hr /> + +<h2 id="套用-state-pattern-solution-得到新的-context-resulting-context">套用 State Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 State Pattern 解決這個問題</p> + +<p>察覺 Forces 後,我們可以套用 <strong>State Pattern</strong>,將狀態邏輯封裝成獨立的類別,達到以下效果:</p> + +<p><img src="/blog/assets/images/design_pattern_state_pattern_uml_2.png" alt="state_pattern_uml_2" /></p> + +<p>狀態模式有三個角色:</p> + +<ol> + <li> + <p><strong>State (狀態介面)</strong><br /> +定義所有具體狀態需要實現的行為。</p> + </li> + <li> + <p><strong>ConcreteState (具體狀態)</strong><br /> +每個具體狀態類別實現 State 介面,並負責該狀態下的具體行為邏輯。</p> + </li> + <li> + <p><strong>Context (上下文)</strong><br /> +負責維護當前狀態,並提供介面讓外部操作。在執行操作時,將請求委派給當前狀態物件。</p> + </li> +</ol> + +<ul> + <li>飲水機類別負責狀態管理,而非具體行為實現,降低耦合度。</li> + <li>每個狀態專注於自身行為,符合單一職責原則。</li> + <li>新增或修改狀態無需影響飲水機核心邏輯,符合開放關閉原則。</li> +</ul> + +<p>將 State Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_state_pattern_uml_3.png" alt="state_pattern_uml_3" /></p> + +<hr /> + +<h2 id="物件導向設計-oop">物件導向設計 (OOP)</h2> + +<p>[State: WaterDispenserState]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">WaterDispenserState</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">handleRequest</span><span class="p">()</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteStates: HeatingState, CoolingState, StandbyState]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">HeatingState</span> <span class="p">:</span> <span class="nc">WaterDispenserState</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">handleRequest</span><span class="p">()</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"加熱中:水溫正在提升,請稍候..."</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">CoolingState</span> <span class="p">:</span> <span class="nc">WaterDispenserState</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">handleRequest</span><span class="p">()</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"冷卻中:水溫正在降低,請稍候..."</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">StandbyState</span> <span class="p">:</span> <span class="nc">WaterDispenserState</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">handleRequest</span><span class="p">()</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"待機中:飲水機維持現有水溫,隨時可用。"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Context: WaterDispenser]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">WaterDispenser</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">var</span> <span class="py">currentState</span><span class="p">:</span> <span class="nc">WaterDispenserState</span> <span class="p">=</span> <span class="nc">StandbyState</span><span class="p">()</span> + + <span class="k">fun</span> <span class="nf">setState</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="nc">WaterDispenserState</span><span class="p">)</span> <span class="p">{</span> + <span class="n">currentState</span> <span class="p">=</span> <span class="n">state</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"狀態切換:${state::class.simpleName}"</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">pressButton</span><span class="p">()</span> <span class="p">{</span> + <span class="n">currentState</span><span class="p">.</span><span class="nf">handleRequest</span><span class="p">()</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Client]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">dispenser</span> <span class="p">=</span> <span class="nc">WaterDispenser</span><span class="p">()</span> + + <span class="c1">// 初始狀態為待機中</span> + <span class="n">dispenser</span><span class="p">.</span><span class="nf">pressButton</span><span class="p">()</span> + + <span class="c1">// 切換到加熱狀態</span> + <span class="n">dispenser</span><span class="p">.</span><span class="nf">setState</span><span class="p">(</span><span class="nc">HeatingState</span><span class="p">())</span> + <span class="n">dispenser</span><span class="p">.</span><span class="nf">pressButton</span><span class="p">()</span> + + <span class="c1">// 切換到冷卻狀態</span> + <span class="n">dispenser</span><span class="p">.</span><span class="nf">setState</span><span class="p">(</span><span class="nc">CoolingState</span><span class="p">())</span> + <span class="n">dispenser</span><span class="p">.</span><span class="nf">pressButton</span><span class="p">()</span> + + <span class="c1">// 回到待機狀態</span> + <span class="n">dispenser</span><span class="p">.</span><span class="nf">setState</span><span class="p">(</span><span class="nc">StandbyState</span><span class="p">())</span> + <span class="n">dispenser</span><span class="p">.</span><span class="nf">pressButton</span><span class="p">()</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Output]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">待機中:飲水機維持現有水溫,隨時可用。</span> +<span class="err">狀態切換:</span><span class="nc">HeatingState</span> +<span class="err">加熱中:水溫正在提升,請稍候</span><span class="o">..</span><span class="p">.</span> +<span class="err">狀態切換:</span><span class="nc">CoolingState</span> +<span class="err">冷卻中:水溫正在降低,請稍候</span><span class="o">..</span><span class="p">.</span> +<span class="err">狀態切換:</span><span class="nc">StandbyState</span> +<span class="err">待機中:飲水機維持現有水溫,隨時可用。</span> +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過 State Pattern,我們成功將飲水機的狀態邏輯與核心功能分離,實現以下優勢:</p> + +<ol> + <li>降低耦合度</li> +</ol> + +<ul> + <li>飲水機類別專注於狀態切換,具體行為由狀態類別負責。</li> +</ul> + +<ol> + <li>符合設計原則</li> +</ol> + +<ul> + <li>單一職責原則 (SRP):每個狀態類別專注於自身行為。</li> + <li>開放關閉原則 (OCP):新增狀態無需修改現有程式碼。</li> +</ul> + +<ol> + <li>易於擴展</li> +</ol> + +<ul> + <li>新增或修改狀態行為時,不影響其他部分。</li> +</ul> + +<p>此模式特別適合處理複雜的狀態轉換場景,例如:</p> + +<ul> + <li>ATM 機的插卡、操作、取卡狀態。</li> + <li>文檔編輯器的編輯、檢視、列印模式。</li> +</ul> + +<p>狀態模式讓程式結構更具彈性,是開發狀態機制應用的最佳選擇!</p>Nick Huang透過狀態模式,設計一個飲水機的運作機制,根據不同狀態執行加熱、冷卻或待機的行為。Design Pattern (20) - Iterator Pattern (迭代器模式)2024-12-22T06:00:00+00:002024-12-22T06:00:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-20-iterator-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<h2 id="需求">需求</h2> + +<p>我們的任務是設計一個檔案系統搜尋工具,需求如下:</p> + +<ul> + <li>使用者可以選擇不同的檔案搜尋方式,例如 <strong>廣度優先搜尋 (BFS)</strong> 或 <strong>深度優先搜尋 (DFS)</strong>。</li> + <li>客戶端不需要關心搜尋邏輯的實現細節,只需使用統一的迭代器介面來遍歷搜尋結果。</li> + <li>系統需要具備擴展性,方便新增其他搜尋法,例如基於檔案大小排序的搜尋。</li> +</ul> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_iterator_pattern_uml_1.png" alt="iterator_pattern_uml_1" /></p> + +<h2 id="察覺-forces">察覺 Forces</h2> + +<p>在未使用設計模式的情況下,我們可能面臨以下挑戰:</p> + +<ol> + <li> + <p><strong>高耦合性 (High Coupling)</strong>:</p> + + <ul> + <li>客戶端需要直接操作每種搜尋方式的實現細節,導致代碼臃腫且難以維護。</li> + </ul> + </li> + <li> + <p><strong>缺乏一致性 (Lack of Consistency)</strong>:</p> + + <ul> + <li>不同搜尋方式的結果訪問方式可能不一致。</li> + </ul> + </li> + <li> + <p><strong>違反開放關閉原則 (Violates OCP)</strong>:</p> + <ul> + <li>若新增搜尋法或更改現有搜尋邏輯,需要修改客戶端程式碼。</li> + </ul> + </li> +</ol> + +<h2 id="套用-iterator-pattern-solution-得到新的-context-resulting-context">套用 Iterator Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Iterator Pattern 解決這個問題。</p> + +<p>迭代器模式允許我們對搜尋結果進行順序訪問,而不需要暴露搜尋邏輯的細節。</p> + +<p>先來看一下 Iterator Pattern 的 UML:</p> + +<p><img src="/blog/assets/images/design_pattern_iterator_pattern_uml_2.png" alt="iterator_pattern_uml_2" /></p> + +<p>以下是 Iterator Pattern 的主要角色:</p> + +<ul> + <li><strong>Iterator (迭代器介面)</strong>:定義訪問搜尋結果的方法,例如 <code class="language-plaintext highlighter-rouge">hasNext()</code> 和 <code class="language-plaintext highlighter-rouge">next()</code>。</li> + <li><strong>ConcreteIterator (具體迭代器)</strong>:實現不同的搜尋邏輯,如 BFS 或 DFS。</li> + <li><strong>Aggregate (聚合介面)</strong>:定義方法來創建迭代器。</li> + <li><strong>ConcreteAggregate (具體聚合類別)</strong>:實現聚合介面,提供檔案系統資料的具體實現。</li> +</ul> + +<p>將 Iterator Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_iterator_pattern_uml_3.png" alt="iterator_pattern_uml_3" /></p> + +<h2 id="物件導向程式設計-oop">物件導向程式設計 (OOP)</h2> + +<p>[Iterator]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">Iterator</span><span class="p">&lt;</span><span class="nc">T</span><span class="p">&gt;</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">hasNext</span><span class="p">():</span> <span class="nc">Boolean</span> + <span class="k">fun</span> <span class="nf">next</span><span class="p">():</span> <span class="nc">T</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Aggregate: FileSystem]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">FileSystem</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">createIterator</span><span class="p">():</span> <span class="nc">Iterator</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteIterator: BFSIterator, DFSIterator]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">BFSIterator</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">root</span><span class="p">:</span> <span class="nc">File</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Iterator</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">queue</span> <span class="p">=</span> <span class="nc">ArrayDeque</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;()</span> + + <span class="nf">init</span> <span class="p">{</span> + <span class="n">queue</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">root</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">hasNext</span><span class="p">():</span> <span class="nc">Boolean</span> <span class="p">{</span> + <span class="k">return</span> <span class="n">queue</span><span class="p">.</span><span class="nf">isNotEmpty</span><span class="p">()</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">next</span><span class="p">():</span> <span class="nc">File</span> <span class="p">{</span> + <span class="k">if</span> <span class="p">(!</span><span class="nf">hasNext</span><span class="p">())</span> <span class="k">throw</span> <span class="nc">NoSuchElementException</span><span class="p">()</span> + <span class="kd">val</span> <span class="py">current</span> <span class="p">=</span> <span class="n">queue</span><span class="p">.</span><span class="nf">removeFirst</span><span class="p">()</span> + <span class="k">if</span> <span class="p">(</span><span class="n">current</span><span class="p">.</span><span class="n">isDirectory</span><span class="p">)</span> <span class="p">{</span> + <span class="n">queue</span><span class="p">.</span><span class="nf">addAll</span><span class="p">(</span><span class="n">current</span><span class="p">.</span><span class="nf">listFiles</span><span class="p">().</span><span class="nf">orEmpty</span><span class="p">())</span> + <span class="p">}</span> + <span class="k">return</span> <span class="n">current</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">DFSIterator</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">root</span><span class="p">:</span> <span class="nc">File</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Iterator</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">stack</span> <span class="p">=</span> <span class="nc">ArrayDeque</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;()</span> + + <span class="nf">init</span> <span class="p">{</span> + <span class="n">stack</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">root</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">hasNext</span><span class="p">():</span> <span class="nc">Boolean</span> <span class="p">{</span> + <span class="k">return</span> <span class="n">stack</span><span class="p">.</span><span class="nf">isNotEmpty</span><span class="p">()</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">next</span><span class="p">():</span> <span class="nc">File</span> <span class="p">{</span> + <span class="k">if</span> <span class="p">(!</span><span class="nf">hasNext</span><span class="p">())</span> <span class="k">throw</span> <span class="nc">NoSuchElementException</span><span class="p">()</span> + <span class="kd">val</span> <span class="py">current</span> <span class="p">=</span> <span class="n">stack</span><span class="p">.</span><span class="nf">removeLast</span><span class="p">()</span> + <span class="k">if</span> <span class="p">(</span><span class="n">current</span><span class="p">.</span><span class="n">isDirectory</span><span class="p">)</span> <span class="p">{</span> + <span class="n">stack</span><span class="p">.</span><span class="nf">addAll</span><span class="p">(</span><span class="n">current</span><span class="p">.</span><span class="nf">listFiles</span><span class="p">().</span><span class="nf">orEmpty</span><span class="p">())</span> + <span class="p">}</span> + <span class="k">return</span> <span class="n">current</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteAggregate: DefaultFileSystem]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">DefaultFileSystem</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">root</span><span class="p">:</span> <span class="nc">File</span><span class="p">,</span> <span class="k">private</span> <span class="kd">val</span> <span class="py">searchMethod</span><span class="p">:</span> <span class="nc">SearchMethod</span><span class="p">)</span> <span class="p">:</span> <span class="nc">FileSystem</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">createIterator</span><span class="p">():</span> <span class="nc">Iterator</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;</span> <span class="p">{</span> + <span class="k">return</span> <span class="k">when</span> <span class="p">(</span><span class="n">searchMethod</span><span class="p">)</span> <span class="p">{</span> + <span class="nc">SearchMethod</span><span class="p">.</span><span class="nc">BFS</span> <span class="p">-&gt;</span> <span class="nc">BFSIterator</span><span class="p">(</span><span class="n">root</span><span class="p">)</span> + <span class="nc">SearchMethod</span><span class="p">.</span><span class="nc">DFS</span> <span class="p">-&gt;</span> <span class="nc">DFSIterator</span><span class="p">(</span><span class="n">root</span><span class="p">)</span> + <span class="p">}</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="k">enum</span> <span class="kd">class</span> <span class="nc">SearchMethod</span> <span class="p">{</span> + <span class="nc">BFS</span><span class="p">,</span> <span class="nc">DFS</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[File]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">File</span><span class="p">(</span><span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="kd">val</span> <span class="py">isDirectory</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">,</span> <span class="kd">val</span> <span class="py">children</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">emptyList</span><span class="p">())</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">listFiles</span><span class="p">():</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">File</span><span class="p">&gt;</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">isDirectory</span><span class="p">)</span> <span class="n">children</span> <span class="k">else</span> <span class="nf">emptyList</span><span class="p">()</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Client]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">fileSystem</span> <span class="p">=</span> <span class="nc">DefaultFileSystem</span><span class="p">(</span> + <span class="n">root</span> <span class="p">=</span> <span class="nc">File</span><span class="p">(</span> + <span class="n">name</span> <span class="p">=</span> <span class="s">"root"</span><span class="p">,</span> + <span class="n">isDirectory</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span> + <span class="n">children</span> <span class="p">=</span> <span class="nf">listOf</span><span class="p">(</span> + <span class="nc">File</span><span class="p">(</span><span class="s">"file1.txt"</span><span class="p">,</span> <span class="k">false</span><span class="p">),</span> + <span class="nc">File</span><span class="p">(</span><span class="s">"folder1"</span><span class="p">,</span> <span class="k">true</span><span class="p">,</span> <span class="nf">listOf</span><span class="p">(</span> + <span class="nc">File</span><span class="p">(</span><span class="s">"file2.txt"</span><span class="p">,</span> <span class="k">false</span><span class="p">),</span> + <span class="nc">File</span><span class="p">(</span><span class="s">"file3.txt"</span><span class="p">,</span> <span class="k">false</span><span class="p">)</span> + <span class="p">)),</span> + <span class="nc">File</span><span class="p">(</span><span class="s">"folder2"</span><span class="p">,</span> <span class="k">true</span><span class="p">,</span> <span class="nf">listOf</span><span class="p">(</span> + <span class="nc">File</span><span class="p">(</span><span class="s">"file4.txt"</span><span class="p">,</span> <span class="k">false</span><span class="p">)</span> + <span class="p">))</span> + <span class="p">)</span> + <span class="p">),</span> + <span class="n">searchMethod</span> <span class="p">=</span> <span class="nc">SearchMethod</span><span class="p">.</span><span class="nc">BFS</span> + <span class="p">)</span> + + <span class="kd">val</span> <span class="py">iterator</span> <span class="p">=</span> <span class="n">fileSystem</span><span class="p">.</span><span class="nf">createIterator</span><span class="p">()</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Files:"</span><span class="p">)</span> + <span class="k">while</span> <span class="p">(</span><span class="n">iterator</span><span class="p">.</span><span class="nf">hasNext</span><span class="p">())</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"- ${iterator.next().name}"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Output]</p> + +<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Files: +- root +- file1.txt +- folder1 +- folder2 +- file2.txt +- file3.txt +- file4.txt +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過 Iterator Pattern,我們成功實現了不同搜尋法的整合,讓客戶端能以一致的方式訪問搜尋結果。此模式提升了系統的靈活性與擴展性,特別適合處理多種遍歷邏輯的場景,例如檔案搜尋、樹狀結構遍歷等。</p>Nick Huang了解迭代器模式如何提供一種順序來訪問集合內元素的方法,而不需要暴露集合的底層表示。Design Pattern (21) - Mediator Pattern (中介者模式)2024-12-22T06:00:00+00:002024-12-22T06:00:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-21-mediator-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<h2 id="需求">需求</h2> + +<p>我們的任務是設計一個 <strong>聊天室應用程式</strong>,需求如下:</p> + +<ul> + <li>使用者可以透過聊天室傳遞訊息。</li> + <li>每個使用者都不需要直接管理其他使用者的資訊。</li> + <li>新增或移除使用者不應影響其他使用者的運作。</li> +</ul> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,我們來快速分析:</p> + +<ul> + <li>若使用者彼此直接通信,會導致複雜的相依關係,增加維護成本。</li> + <li>我們需要一個集中管理的角色,來協調使用者之間的訊息傳遞。</li> +</ul> + +<p><img src="/blog/assets/images/design_pattern_mediator_pattern_uml_1.png" alt="mediator_pattern_uml_1" /></p> + +<h2 id="察覺-forces">察覺 Forces</h2> + +<p>在未使用設計模式的情況下,我們可能面臨以下挑戰:</p> + +<ol> + <li> + <p><strong>高耦合性 (High Coupling)</strong>:</p> + + <ul> + <li>使用者彼此之間直接通信,導致新增或移除使用者時需修改多處程式碼。</li> + </ul> + </li> + <li> + <p><strong>難以擴展 (Hard to Extend)</strong>:</p> + + <ul> + <li>若要增加新功能(如訊息過濾或廣播機制),需要修改多個使用者的邏輯。</li> + </ul> + </li> + <li> + <p><strong>複雜度上升 (Increased Complexity)</strong>:</p> + + <ul> + <li>使用者之間的關聯數量隨著使用者數量增長呈指數級增加。</li> + </ul> + </li> +</ol> + +<h2 id="套用-mediator-pattern-solution-得到新的-context-resulting-context">套用 Mediator Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Mediator Pattern 解決這個問題。</p> + +<p>中介者模式引入了一個中介者來負責協調使用者之間的交互。使用者只需與中介者通信,從而降低相互之間的耦合性。</p> + +<p>以下是 Mediator Pattern 的 UML 圖:</p> + +<p><img src="/blog/assets/images/design_pattern_mediator_pattern_uml_2.png" alt="mediator_pattern_uml_2" /></p> + +<p>Mediator Pattern 的主要角色:</p> + +<ul> + <li><strong>Mediator (中介者介面)</strong>:定義協調參與者的方法,例如傳遞訊息。</li> + <li><strong>ConcreteMediator (具體中介者)</strong>:實現中介者的行為,處理使用者之間的訊息傳遞。</li> + <li><strong>Colleague (同事類別)</strong>:表示參與者,所有訊息均通過中介者進行傳遞。</li> + <li><strong>ConcreteColleague (具體同事類別)</strong>:實現具體參與者的行為,並依賴中介者進行通信。</li> +</ul> + +<p>讓我們將 Mediator Pattern 套用到聊天室應用程式中。</p> + +<p><img src="/blog/assets/images/design_pattern_mediator_pattern_uml_3.png" alt="mediator_pattern_uml_3" /></p> + +<h2 id="物件導向程式設計-oop">物件導向程式設計 (OOP)</h2> + +<p>[Mediator]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">ChatMediator</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">sendMessage</span><span class="p">(</span><span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="nc">User</span><span class="p">)</span> + <span class="k">fun</span> <span class="nf">addUser</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="nc">User</span><span class="p">)</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteMediator]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ChatRoomMediator</span> <span class="p">:</span> <span class="nc">ChatMediator</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">users</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">User</span><span class="p">&gt;()</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">sendMessage</span><span class="p">(</span><span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">user</span><span class="p">:</span> <span class="nc">User</span><span class="p">)</span> <span class="p">{</span> + <span class="n">users</span><span class="p">.</span><span class="nf">filter</span> <span class="p">{</span> <span class="n">it</span> <span class="p">!=</span> <span class="n">user</span> <span class="p">}.</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="nf">receive</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> <span class="p">}</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">addUser</span><span class="p">(</span><span class="n">user</span><span class="p">:</span> <span class="nc">User</span><span class="p">)</span> <span class="p">{</span> + <span class="n">users</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Colleague]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">abstract</span> <span class="kd">class</span> <span class="nc">User</span><span class="p">(</span><span class="k">protected</span> <span class="kd">val</span> <span class="py">mediator</span><span class="p">:</span> <span class="nc">ChatMediator</span><span class="p">,</span> <span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="k">abstract</span> <span class="k">fun</span> <span class="nf">send</span><span class="p">(</span><span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> + <span class="k">abstract</span> <span class="k">fun</span> <span class="nf">receive</span><span class="p">(</span><span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteColleague]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ChatUser</span><span class="p">(</span><span class="n">mediator</span><span class="p">:</span> <span class="nc">ChatMediator</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">:</span> <span class="nc">User</span><span class="p">(</span><span class="n">mediator</span><span class="p">,</span> <span class="n">name</span><span class="p">)</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">send</span><span class="p">(</span><span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"$name 發送訊息:$message"</span><span class="p">)</span> + <span class="n">mediator</span><span class="p">.</span><span class="nf">sendMessage</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="k">this</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">receive</span><span class="p">(</span><span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"$name 收到訊息:$message"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Client]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">chatMediator</span> <span class="p">=</span> <span class="nc">ChatRoomMediator</span><span class="p">()</span> + + <span class="kd">val</span> <span class="py">user1</span> <span class="p">=</span> <span class="nc">ChatUser</span><span class="p">(</span><span class="n">chatMediator</span><span class="p">,</span> <span class="s">"Alice"</span><span class="p">)</span> + <span class="kd">val</span> <span class="py">user2</span> <span class="p">=</span> <span class="nc">ChatUser</span><span class="p">(</span><span class="n">chatMediator</span><span class="p">,</span> <span class="s">"Bob"</span><span class="p">)</span> + <span class="kd">val</span> <span class="py">user3</span> <span class="p">=</span> <span class="nc">ChatUser</span><span class="p">(</span><span class="n">chatMediator</span><span class="p">,</span> <span class="s">"Charlie"</span><span class="p">)</span> + + <span class="n">chatMediator</span><span class="p">.</span><span class="nf">addUser</span><span class="p">(</span><span class="n">user1</span><span class="p">)</span> + <span class="n">chatMediator</span><span class="p">.</span><span class="nf">addUser</span><span class="p">(</span><span class="n">user2</span><span class="p">)</span> + <span class="n">chatMediator</span><span class="p">.</span><span class="nf">addUser</span><span class="p">(</span><span class="n">user3</span><span class="p">)</span> + + <span class="n">user1</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="s">"Hello, everyone!"</span><span class="p">)</span> + <span class="n">user2</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="s">"Hi, Alice!"</span><span class="p">)</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Output]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Alice</span> <span class="err">發送訊息:</span><span class="nc">Hello</span><span class="p">,</span> <span class="n">everyone</span><span class="p">!</span> +<span class="nc">Bob</span> <span class="err">收到訊息:</span><span class="nc">Hello</span><span class="p">,</span> <span class="n">everyone</span><span class="p">!</span> +<span class="nc">Charlie</span> <span class="err">收到訊息:</span><span class="nc">Hello</span><span class="p">,</span> <span class="n">everyone</span><span class="p">!</span> +<span class="nc">Bob</span> <span class="err">發送訊息:</span><span class="nc">Hi</span><span class="p">,</span> <span class="nc">Alice</span><span class="p">!</span> +<span class="nc">Alice</span> <span class="err">收到訊息:</span><span class="nc">Hi</span><span class="p">,</span> <span class="nc">Alice</span><span class="p">!</span> +<span class="nc">Charlie</span> <span class="err">收到訊息:</span><span class="nc">Hi</span><span class="p">,</span> <span class="nc">Alice</span><span class="p">!</span> +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過中介者模式,我們成功降低了使用者之間的耦合性,並實現了靈活的訊息傳遞機制。此模式非常適合處理多物件之間的交互,例如聊天室、事件系統或 GUI 組件通信等場景。</p>Nick Huang了解中介者模式如何協調物件之間的交互,減少物件之間的耦合性並促進系統的可擴展性。Design Pattern (22) - Memento Pattern (備忘錄模式)2024-12-22T06:00:00+00:002024-12-22T06:00:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-22-memento-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<h2 id="需求">需求</h2> + +<p>我們的任務是設計一個文字編輯器,需求如下:</p> + +<ul> + <li>使用者可以輸入文字,並隨時按下 <code class="language-plaintext highlighter-rouge">Ctrl+Z</code> 回復上一步。</li> + <li>系統需要保存歷史狀態以供回復。</li> + <li>客戶端不需要了解狀態保存的實現細節,只需使用一個簡單的回復操作即可。</li> +</ul> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_memento_pattern_uml_1.png" alt="memento_pattern_uml_1" /></p> + +<h3 id="察覺-forces">察覺 Forces</h3> + +<p>在未使用設計模式的情況下,我們可能面臨以下挑戰:</p> + +<ol> + <li> + <p><strong>資料喪失風險 (Data Loss Risk)</strong>:</p> + + <ul> + <li>如果我們僅保留當前狀態,將無法回復到之前的狀態。</li> + </ul> + </li> + <li> + <p><strong>高耦合性 (High Coupling)</strong>:</p> + + <ul> + <li>客戶端需要直接操作狀態管理邏輯,導致複雜性增加。</li> + </ul> + </li> + <li> + <p><strong>難以擴展 (Hard to Extend)</strong>:</p> + + <ul> + <li>新增功能或改變狀態保存方式時,可能需要修改大量程式碼。</li> + </ul> + </li> +</ol> + +<h2 id="套用-memento-pattern-solution-得到新的-context-resulting-context">套用 Memento Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Memento Pattern 解決這個問題</p> + +<p>先來看一下 Memento Pattern 的 UML</p> + +<p><img src="/blog/assets/images/design_pattern_memento_pattern_uml_2.png" alt="memento_pattern_uml_2" /></p> + +<p>備忘錄模式引入了三個角色:</p> + +<ol> + <li><strong>Originator (發起者)</strong>:保存當前狀態到備忘錄,或從備忘錄中恢復狀態。</li> + <li><strong>Memento (備忘錄)</strong>:存儲 Originator 的內部狀態。</li> + <li><strong>Caretaker (管理者)</strong>:負責保存和恢復備忘錄,但不直接操作其內容。</li> +</ol> + +<p>將 Memento Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_memento_pattern_uml_3.png" alt="memento_pattern_uml_3" /></p> + +<h2 id="物件導向程式設計-oop">物件導向程式設計 (OOP)</h2> + +<h3 id="originator">Originator</h3> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">TextEditor</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">var</span> <span class="py">text</span><span class="p">:</span> <span class="nc">String</span> <span class="p">=</span> <span class="s">""</span> + + <span class="k">fun</span> <span class="nf">type</span><span class="p">(</span><span class="n">newText</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="n">text</span> <span class="p">+=</span> <span class="n">newText</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">getText</span><span class="p">():</span> <span class="nc">String</span> <span class="p">=</span> <span class="n">text</span> + + <span class="k">fun</span> <span class="nf">save</span><span class="p">():</span> <span class="nc">Memento</span> <span class="p">=</span> <span class="nc">Memento</span><span class="p">(</span><span class="n">text</span><span class="p">)</span> + + <span class="k">fun</span> <span class="nf">restore</span><span class="p">(</span><span class="n">memento</span><span class="p">:</span> <span class="nc">Memento</span><span class="p">)</span> <span class="p">{</span> + <span class="n">text</span> <span class="p">=</span> <span class="n">memento</span><span class="p">.</span><span class="nf">getText</span><span class="p">()</span> + <span class="p">}</span> + + <span class="kd">data class</span> <span class="nc">Memento</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">state</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">getText</span><span class="p">():</span> <span class="nc">String</span> <span class="p">=</span> <span class="n">state</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<h3 id="caretaker">Caretaker</h3> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">History</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">mementos</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">TextEditor</span><span class="p">.</span><span class="nc">Memento</span><span class="p">&gt;()</span> + + <span class="k">fun</span> <span class="nf">save</span><span class="p">(</span><span class="n">memento</span><span class="p">:</span> <span class="nc">TextEditor</span><span class="p">.</span><span class="nc">Memento</span><span class="p">)</span> <span class="p">{</span> + <span class="n">mementos</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">memento</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">undo</span><span class="p">():</span> <span class="nc">TextEditor</span><span class="p">.</span><span class="nc">Memento</span><span class="p">?</span> <span class="p">{</span> + <span class="k">if</span> <span class="p">(</span><span class="n">mementos</span><span class="p">.</span><span class="nf">isNotEmpty</span><span class="p">())</span> <span class="p">{</span> + <span class="k">return</span> <span class="n">mementos</span><span class="p">.</span><span class="nf">removeAt</span><span class="p">(</span><span class="n">mementos</span><span class="p">.</span><span class="n">size</span> <span class="p">-</span> <span class="mi">1</span><span class="p">)</span> + <span class="p">}</span> + <span class="k">return</span> <span class="k">null</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<h3 id="client">Client</h3> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">textEditor</span> <span class="p">=</span> <span class="nc">TextEditor</span><span class="p">()</span> + <span class="kd">val</span> <span class="py">history</span> <span class="p">=</span> <span class="nc">History</span><span class="p">()</span> + + <span class="c1">// Typing string</span> + <span class="n">textEditor</span><span class="p">.</span><span class="nf">type</span><span class="p">(</span><span class="s">"Hello"</span><span class="p">)</span> + <span class="n">history</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">textEditor</span><span class="p">.</span><span class="nf">save</span><span class="p">())</span> + + <span class="n">textEditor</span><span class="p">.</span><span class="nf">type</span><span class="p">(</span><span class="s">", World"</span><span class="p">)</span> + <span class="n">history</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">textEditor</span><span class="p">.</span><span class="nf">save</span><span class="p">())</span> + + <span class="n">textEditor</span><span class="p">.</span><span class="nf">type</span><span class="p">(</span><span class="s">"! This is Memento Pattern."</span><span class="p">)</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Current Text:${textEditor.getText()}"</span><span class="p">)</span> <span class="c1">// Output: Hello, World! This is Memento Pattern.</span> + + <span class="c1">// Pressed Ctrl+Z</span> + <span class="n">textEditor</span><span class="p">.</span><span class="nf">restore</span><span class="p">(</span><span class="n">history</span><span class="p">.</span><span class="nf">undo</span><span class="p">()</span><span class="o">!!</span><span class="p">)</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Excute undo Text:${textEditor.getText()}"</span><span class="p">)</span> <span class="c1">// Output: Hello, World!</span> + + <span class="c1">// Pressed Ctrl+Z again</span> + <span class="n">textEditor</span><span class="p">.</span><span class="nf">restore</span><span class="p">(</span><span class="n">history</span><span class="p">.</span><span class="nf">undo</span><span class="p">()</span><span class="o">!!</span><span class="p">)</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Excute undo Text:${textEditor.getText()}"</span><span class="p">)</span> <span class="c1">// Output: Hello</span> +<span class="p">}</span> +</code></pre></div></div> + +<h3 id="output">Output</h3> + +<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Current Text: Hello, World! This is Memento Pattern. +Excute undo Text: Hello, World! +Excute undo Text: Hello +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過備忘錄模式,我們成功實現了文字編輯器的狀態恢復功能,讓使用者能夠輕鬆地回復到之前的操作狀態。這種模式廣泛應用於文字處理器、遊戲保存系統以及其他需要狀態恢復的場景。</p>Nick Huang了解備忘錄模式如何幫助我們實現狀態恢復,像是常見的 Ctrl+Z 功能,讓我們回到之前的操作狀態。Design Pattern (23) - Observer Pattern (觀察者模式)2024-12-22T06:00:00+00:002024-12-22T06:00:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-23-observer-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<h2 id="需求">需求</h2> + +<p>我們的任務是設計一個 <strong>安全系統主機 (Panel)</strong>,需求如下:</p> + +<ul> + <li>主機負責監控不同的感測器,例如煙霧探測器或門窗感測器。</li> + <li>當警報觸發時,主機需要通知所有已註冊的設備,例如平板、iOS 和 Android 手機。</li> + <li>設備可以動態地加入或移除通知清單。</li> +</ul> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_observer_pattern_uml_1.png" alt="observer_pattern_uml_1" /></p> + +<h3 id="察覺-forces">察覺 Forces</h3> + +<p>在未使用設計模式的情況下,我們可能面臨以下挑戰:</p> + +<ol> + <li> + <p><strong>高耦合性 (High Coupling)</strong></p> + + <ul> + <li>如果主機直接與每一個設備互動,程式碼會變得難以維護,每次新增或移除設備都需要修改主機邏輯。</li> + </ul> + </li> + <li> + <p><strong>缺乏彈性 (Lack of Flexibility)</strong></p> + + <ul> + <li>新增設備需要修改現有程式碼,違反開放關閉原則 (OCP)。</li> + </ul> + </li> + <li> + <p><strong>通知不一致 (Inconsistent Notifications)</strong></p> + <ul> + <li>當警報觸發時,難以確保每個設備都能正確接收到通知。</li> + </ul> + </li> +</ol> + +<hr /> + +<h2 id="套用-observer-pattern-solution-得到新的-context-resulting-context">套用 Observer Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Observer Pattern 解決這個問題</p> + +<p>先來看一下 Memento Pattern 的 UML</p> + +<p><img src="/blog/assets/images/design_pattern_observer_pattern_uml_2.png" alt="observer_pattern_uml_2" /></p> + +<p>觀察者模式提供了一個一對多的通知機制,當主機的狀態改變時,會自動通知所有已訂閱的設備。</p> + +<ul> + <li><strong>Subject (主體)</strong>:安全系統主機,負責管理所有設備並在警報觸發時發送通知。</li> + <li><strong>Observer (觀察者)</strong>:設備,例如平板、iOS 和 Android 手機,接收通知並根據警報執行操作。</li> + <li><strong>ConcreteSubject (具體主體)</strong>:實際的安全系統主機,包含警報邏輯。</li> + <li><strong>ConcreteObserver (具體觀察者)</strong>:具體的設備實現,例如 Android 設備或 iOS 設備。</li> +</ul> + +<p>將 Observer Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_observer_pattern_uml_3.png" alt="observer_pattern_uml_3" /></p> + +<h2 id="實作">實作</h2> + +<p>[Subject: AlarmSystem]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">AlarmSystem</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">addObserver</span><span class="p">(</span><span class="n">observer</span><span class="p">:</span> <span class="nc">Device</span><span class="p">)</span> + <span class="k">fun</span> <span class="nf">removeObserver</span><span class="p">(</span><span class="n">observer</span><span class="p">:</span> <span class="nc">Device</span><span class="p">)</span> + <span class="k">fun</span> <span class="nf">notifyObservers</span><span class="p">(</span><span class="n">alarmMessage</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Observer: Device]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">Device</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">onAlarmTriggered</span><span class="p">(</span><span class="n">alarmMessage</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteSubject: SecurityPanel]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">SecurityPanel</span> <span class="p">:</span> <span class="nc">AlarmSystem</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">devices</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">Device</span><span class="p">&gt;()</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">addObserver</span><span class="p">(</span><span class="n">observer</span><span class="p">:</span> <span class="nc">Device</span><span class="p">)</span> <span class="p">{</span> + <span class="n">devices</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">observer</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">removeObserver</span><span class="p">(</span><span class="n">observer</span><span class="p">:</span> <span class="nc">Device</span><span class="p">)</span> <span class="p">{</span> + <span class="n">devices</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">observer</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">notifyObservers</span><span class="p">(</span><span class="n">alarmMessage</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="k">for</span> <span class="p">(</span><span class="n">device</span> <span class="k">in</span> <span class="n">devices</span><span class="p">)</span> <span class="p">{</span> + <span class="n">device</span><span class="p">.</span><span class="nf">onAlarmTriggered</span><span class="p">(</span><span class="n">alarmMessage</span><span class="p">)</span> + <span class="p">}</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">triggerAlarm</span><span class="p">(</span><span class="n">zone</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">message</span> <span class="p">=</span> <span class="s">"警報觸發於 $zone!"</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"主機通知: $message"</span><span class="p">)</span> + <span class="nf">notifyObservers</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteObserver: Devices]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Tablet</span> <span class="p">:</span> <span class="nc">Device</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">onAlarmTriggered</span><span class="p">(</span><span class="n">alarmMessage</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"平板收到通知: $alarmMessage"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">IOSDevice</span> <span class="p">:</span> <span class="nc">Device</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">onAlarmTriggered</span><span class="p">(</span><span class="n">alarmMessage</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"iOS 設備收到通知: $alarmMessage"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">AndroidDevice</span> <span class="p">:</span> <span class="nc">Device</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">onAlarmTriggered</span><span class="p">(</span><span class="n">alarmMessage</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Android 設備收到通知: $alarmMessage"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Client]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">securityPanel</span> <span class="p">=</span> <span class="nc">SecurityPanel</span><span class="p">()</span> + + <span class="kd">val</span> <span class="py">tablet</span> <span class="p">=</span> <span class="nc">Tablet</span><span class="p">()</span> + <span class="kd">val</span> <span class="py">iosDevice</span> <span class="p">=</span> <span class="nc">IOSDevice</span><span class="p">()</span> + <span class="kd">val</span> <span class="py">androidDevice</span> <span class="p">=</span> <span class="nc">AndroidDevice</span><span class="p">()</span> + + <span class="c1">// add observers</span> + <span class="n">securityPanel</span><span class="p">.</span><span class="nf">addObserver</span><span class="p">(</span><span class="n">tablet</span><span class="p">)</span> + <span class="n">securityPanel</span><span class="p">.</span><span class="nf">addObserver</span><span class="p">(</span><span class="n">iosDevice</span><span class="p">)</span> + <span class="n">securityPanel</span><span class="p">.</span><span class="nf">addObserver</span><span class="p">(</span><span class="n">androidDevice</span><span class="p">)</span> + + <span class="c1">// trigger alarm</span> + <span class="n">securityPanel</span><span class="p">.</span><span class="nf">triggerAlarm</span><span class="p">(</span><span class="s">"客廳"</span><span class="p">)</span> + <span class="n">securityPanel</span><span class="p">.</span><span class="nf">triggerAlarm</span><span class="p">(</span><span class="s">"廚房"</span><span class="p">)</span> + + <span class="c1">// remove observer</span> + <span class="n">securityPanel</span><span class="p">.</span><span class="nf">removeObserver</span><span class="p">(</span><span class="n">androidDevice</span><span class="p">)</span> + <span class="n">securityPanel</span><span class="p">.</span><span class="nf">triggerAlarm</span><span class="p">(</span><span class="s">"臥室"</span><span class="p">)</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Output]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">主機通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">客廳</span><span class="p">!</span> +<span class="err">平板收到通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">客廳</span><span class="p">!</span> +<span class="n">iOS</span> <span class="err">設備收到通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">客廳</span><span class="p">!</span> +<span class="nc">Android</span> <span class="err">設備收到通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">客廳</span><span class="p">!</span> + +<span class="err">主機通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">廚房</span><span class="p">!</span> +<span class="err">平板收到通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">廚房</span><span class="p">!</span> +<span class="n">iOS</span> <span class="err">設備收到通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">廚房</span><span class="p">!</span> +<span class="nc">Android</span> <span class="err">設備收到通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">廚房</span><span class="p">!</span> + +<span class="err">主機通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">臥室</span><span class="p">!</span> +<span class="err">平板收到通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">臥室</span><span class="p">!</span> +<span class="n">iOS</span> <span class="err">設備收到通知</span><span class="p">:</span> <span class="err">警報觸發於</span> <span class="err">臥室</span><span class="p">!</span> +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過 Observer Pattern,我們構建了一個靈活的安全系統通知機制,設備可以動態地加入或移除,且主機與設備之間的耦合度降低,遵循開放關閉原則 (OCP)。此模式適用於任何需要實現通知機制的場景,例如:</p> + +<ul> + <li>即時警報系統</li> + <li>訊息推送系統</li> + <li>事件分發系統</li> +</ul>Nick Huang透過觀察者模式,實現安全系統主機的警報通知機制,當警報觸發時,主機自動通知平板、iOS 和 Android 手機。Design Pattern (19) - Command Pattern (命令模式)2024-12-21T07:00:00+00:002024-12-21T07:00:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-19-command-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<h2 id="需求">需求</h2> + +<p>我們需要一個音樂播放器控制系統,需求如下:</p> + +<ul> + <li>使用者可以透過遙控器控制音樂播放器執行「播放」、「暫停」和「停止」操作。</li> + <li>支援撤銷 (Undo) 功能,例如撤銷暫停會恢復播放。</li> + <li>按鈕行為應保持靈活,方便未來擴充新功能,例如「下一首」或「重播」。</li> +</ul> + +<hr /> + +<h2 id="物件導向分析-ooa"><strong>物件導向分析 (OOA)</strong></h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_command_pattern_uml_1.png" alt="command_pattern_uml_1" /></p> + +<h2 id="察覺-forces">察覺 Forces</h2> + +<p>在未使用設計模式的情況下,我們可能面臨以下挑戰:</p> + +<ol> + <li> + <p><strong>高耦合性 (High Coupling)</strong>:</p> + + <ul> + <li>客戶端需要直接操作每個具體設備的功能,導致耦合度過高,不利於系統擴展。</li> + </ul> + </li> + <li> + <p><strong>缺乏靈活性 (Lack of Flexibility)</strong>:</p> + + <ul> + <li>如果需要新增設備或操作,客戶端需要修改大量程式碼,增加了維護成本。</li> + </ul> + </li> + <li> + <p><strong>撤銷/重做困難 (Undo/Redo Complexity)</strong>:</p> + <ul> + <li>系統沒有統一的方式處理操作歷史,導致撤銷和重做功能難以實現。</li> + </ul> + </li> +</ol> + +<h2 id="套用-command-pattern-solution-得到新的-context-resulting-context">套用 Command Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Command Pattern 解決這個問題。</p> + +<p>先來看一下 Command Pattern 的 UML:</p> + +<p><img src="/blog/assets/images/design_pattern_command_pattern_uml_2.png" alt="command_pattern_uml_2" /></p> + +<p>以下是 Command Pattern 的主要角色:</p> + +<h3 id="角色與職責"><strong>角色與職責</strong></h3> + +<ol> + <li> + <p><strong>Receiver (接收者)</strong><br /> +實際執行音樂播放邏輯的物件,例如播放、暫停和停止操作。</p> + </li> + <li> + <p><strong>Command (命令介面)</strong><br /> +定義命令的共同介面,保證命令的可執行性 (Execute) 與可撤銷性 (Undo)。</p> + </li> + <li> + <p><strong>ConcreteCommand (具體命令)</strong><br /> +將具體的播放控制操作封裝到命令物件中,例如「播放命令」、「暫停命令」和「停止命令」。</p> + </li> + <li> + <p><strong>Invoker (呼叫者)</strong><br /> +遙控器,負責執行命令並追蹤命令歷史,以支援撤銷操作。</p> + </li> + <li> + <p><strong>Client (客戶端)</strong><br /> +負責初始化命令、接收者與遙控器之間的對應關係。</p> + </li> +</ol> + +<p>將 Command Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_command_pattern_uml_3.png" alt="command_pattern_uml_3" /></p> + +<hr /> + +<h2 id="物件導向程式設計-oop"><strong>物件導向程式設計 (OOP)</strong></h2> + +<h3 id="receiver-音樂播放器">[Receiver: 音樂播放器]</h3> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MusicPlayer</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">play</span><span class="p">()</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Music is playing"</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">pause</span><span class="p">()</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Music is paused"</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">stop</span><span class="p">()</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Music is stopped"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<h3 id="command-命令介面">[Command: 命令介面]</h3> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">Command</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">execute</span><span class="p">()</span> + <span class="k">fun</span> <span class="nf">undo</span><span class="p">()</span> +<span class="p">}</span> +</code></pre></div></div> + +<h3 id="concretecommand-具體命令">[ConcreteCommand: 具體命令]</h3> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">PlayCommand</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">player</span><span class="p">:</span> <span class="nc">MusicPlayer</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Command</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">execute</span><span class="p">()</span> <span class="p">{</span> + <span class="n">player</span><span class="p">.</span><span class="nf">play</span><span class="p">()</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">undo</span><span class="p">()</span> <span class="p">{</span> + <span class="n">player</span><span class="p">.</span><span class="nf">pause</span><span class="p">()</span> <span class="c1">// 撤銷播放則暫停</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">PauseCommand</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">player</span><span class="p">:</span> <span class="nc">MusicPlayer</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Command</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">execute</span><span class="p">()</span> <span class="p">{</span> + <span class="n">player</span><span class="p">.</span><span class="nf">pause</span><span class="p">()</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">undo</span><span class="p">()</span> <span class="p">{</span> + <span class="n">player</span><span class="p">.</span><span class="nf">play</span><span class="p">()</span> <span class="c1">// 撤銷暫停則播放</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">StopCommand</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">player</span><span class="p">:</span> <span class="nc">MusicPlayer</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Command</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">execute</span><span class="p">()</span> <span class="p">{</span> + <span class="n">player</span><span class="p">.</span><span class="nf">stop</span><span class="p">()</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">undo</span><span class="p">()</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Cannot undo stop"</span><span class="p">)</span> <span class="c1">// 撤銷停止通常無法恢復</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<h3 id="invoker-遙控器">[Invoker: 遙控器]</h3> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">RemoteControl</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">commandHistory</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">Command</span><span class="p">&gt;()</span> + + <span class="k">fun</span> <span class="nf">pressButton</span><span class="p">(</span><span class="n">command</span><span class="p">:</span> <span class="nc">Command</span><span class="p">)</span> <span class="p">{</span> + <span class="n">command</span><span class="p">.</span><span class="nf">execute</span><span class="p">()</span> + <span class="n">commandHistory</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">command</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">pressUndo</span><span class="p">()</span> <span class="p">{</span> + <span class="k">if</span> <span class="p">(</span><span class="n">commandHistory</span><span class="p">.</span><span class="nf">isNotEmpty</span><span class="p">())</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">lastCommand</span> <span class="p">=</span> <span class="n">commandHistory</span><span class="p">.</span><span class="nf">removeLast</span><span class="p">()</span> + <span class="n">lastCommand</span><span class="p">.</span><span class="nf">undo</span><span class="p">()</span> + <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"No command to undo"</span><span class="p">)</span> + <span class="p">}</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<h3 id="client-客戶端">[Client: 客戶端]</h3> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">player</span> <span class="p">=</span> <span class="nc">MusicPlayer</span><span class="p">()</span> + + <span class="kd">val</span> <span class="py">playCommand</span> <span class="p">=</span> <span class="nc">PlayCommand</span><span class="p">(</span><span class="n">player</span><span class="p">)</span> + <span class="kd">val</span> <span class="py">pauseCommand</span> <span class="p">=</span> <span class="nc">PauseCommand</span><span class="p">(</span><span class="n">player</span><span class="p">)</span> + <span class="kd">val</span> <span class="py">stopCommand</span> <span class="p">=</span> <span class="nc">StopCommand</span><span class="p">(</span><span class="n">player</span><span class="p">)</span> + + <span class="kd">val</span> <span class="py">remoteControl</span> <span class="p">=</span> <span class="nc">RemoteControl</span><span class="p">()</span> + + <span class="c1">// Play music</span> + <span class="n">remoteControl</span><span class="p">.</span><span class="nf">pressButton</span><span class="p">(</span><span class="n">playCommand</span><span class="p">)</span> + + <span class="c1">// Pause music</span> + <span class="n">remoteControl</span><span class="p">.</span><span class="nf">pressButton</span><span class="p">(</span><span class="n">pauseCommand</span><span class="p">)</span> + + <span class="c1">// Undo</span> + <span class="n">remoteControl</span><span class="p">.</span><span class="nf">pressUndo</span><span class="p">()</span> + + <span class="c1">// Stop music</span> + <span class="n">remoteControl</span><span class="p">.</span><span class="nf">pressButton</span><span class="p">(</span><span class="n">stopCommand</span><span class="p">)</span> + + <span class="c1">// Undo</span> + <span class="n">remoteControl</span><span class="p">.</span><span class="nf">pressUndo</span><span class="p">()</span> +<span class="p">}</span> +</code></pre></div></div> + +<h3 id="output">Output</h3> + +<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Music is playing +Music is paused +Music is playing +Music is stopped +Cannot undo stop +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過 Command Pattern,我們成功解除了客戶端與具體設備的耦合,讓系統更具靈活性。此外,命令模式還方便了操作的撤銷與重做功能的實現,大大提升了系統的擴展性與維護性。此模式特別適用於需要排程請求、記錄操作歷史或提供撤銷/重做功能的場景。</p>Nick Huang了解命令模式如何將操作與執行解耦,讓程式具備更高的靈活性與可擴展性。Design Pattern (18) - Chain of Responsibility Pattern (責任鏈模式)2024-12-16T15:00:00+00:002024-12-16T15:00:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-18-chain-of-responsibility-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<h2 id="需求">需求</h2> + +<p>我們的任務是建立一個日誌處理系統,需求如下:</p> + +<ul> + <li>系統支持多層次日誌處理(如 Console、File、Database 等)。</li> + <li>請求可以被多個處理器處理,且處理器的組合應具備動態調整能力。</li> + <li>確保每層處理器的責任彼此獨立,並能擴展新處理器而不影響既有邏輯。</li> +</ul> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_chain_of_responsibility_pattern_uml_1.png" alt="chain_of_responsibility_pattern_uml_1" /></p> + +<h2 id="察覺-forces">察覺 Forces</h2> + +<p>在未使用設計模式的情況下,我們可能面臨以下挑戰:</p> + +<ol> + <li> + <p><strong>高耦合性 (High Coupling)</strong>:</p> + + <ul> + <li>如果客戶端需要直接控制每個日誌處理器,將導致代碼過於複雜且難以維護。</li> + </ul> + </li> + <li> + <p><strong>缺乏靈活性 (Lack of Flexibility)</strong>:</p> + + <ul> + <li>無法輕鬆地調整處理器的執行順序或新增處理器。</li> + </ul> + </li> + <li> + <p><strong>違反開放關閉原則 (Violates OCP)</strong>:</p> + <ul> + <li>若需支持新的日誌處理方式,必須修改客戶端邏輯,導致系統穩定性下降。</li> + </ul> + </li> +</ol> + +<h2 id="套用-chain-of-responsibility-pattern-solution-得到新的-context-resulting-context">套用 Chain of Responsibility Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>先來看一下 Chain of Responsibility Pattern 的 UML:</p> + +<p><img src="/blog/assets/images/design_pattern_chain_of_responsibility_pattern_uml_2.png" alt="chain_of_responsibility_pattern_uml_2" /></p> + +<p>責任鏈模式提供了解決方案,通過將處理器鏈接成一條動態的責任鏈,使請求能被多個處理器依次處理,降低耦合性並提升系統的靈活性與可擴展性。</p> + +<p>以下是 Chain of Responsibility Pattern 的主要角色:</p> + +<ul> + <li><strong>Handler (處理者介面)</strong>:定義處理請求的介面,並包含指向下一個處理者的引用。</li> + <li><strong>ConcreteHandler (具體處理者)</strong>:實現處理邏輯,並根據條件決定是否將請求傳遞給下一個處理者。</li> + <li><strong>Client (客戶端)</strong>:發送請求,並設定處理者的責任鏈結構。</li> +</ul> + +<p>將 Chain of Responsibility Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_chain_of_responsibility_pattern_uml_3.png" alt="chain_of_responsibility_pattern_uml_3" /></p> + +<h2 id="物件導向程式設計-oop">物件導向程式設計 (OOP)</h2> + +<p>[Handler: Logger]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">abstract</span> <span class="kd">class</span> <span class="nc">Logger</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">nextLogger</span><span class="p">:</span> <span class="nc">Logger</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> + + <span class="k">abstract</span> <span class="k">fun</span> <span class="nf">log</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">LogLevel</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> + + <span class="k">protected</span> <span class="k">fun</span> <span class="nf">passToNext</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">LogLevel</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="n">nextLogger</span><span class="o">?.</span><span class="nf">log</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[LogLevel Enum]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">enum</span> <span class="kd">class</span> <span class="nc">LogLevel</span> <span class="p">{</span> + <span class="nc">INFO</span><span class="p">,</span> <span class="nc">WARNING</span><span class="p">,</span> <span class="nc">ERROR</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteHandler: ConsoleLogger]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ConsoleLogger</span><span class="p">(</span><span class="n">nextLogger</span><span class="p">:</span> <span class="nc">Logger</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Logger</span><span class="p">(</span><span class="n">nextLogger</span><span class="p">)</span> <span class="p">{</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">log</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">LogLevel</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="k">if</span> <span class="p">(</span><span class="n">level</span> <span class="p">==</span> <span class="nc">LogLevel</span><span class="p">.</span><span class="nc">INFO</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"ConsoleLogger: $message"</span><span class="p">)</span> + <span class="p">}</span> + <span class="nf">passToNext</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteHandler: FileLogger]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">FileLogger</span><span class="p">(</span><span class="n">nextLogger</span><span class="p">:</span> <span class="nc">Logger</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Logger</span><span class="p">(</span><span class="n">nextLogger</span><span class="p">)</span> <span class="p">{</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">log</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">LogLevel</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="k">if</span> <span class="p">(</span><span class="n">level</span> <span class="p">==</span> <span class="nc">LogLevel</span><span class="p">.</span><span class="nc">WARNING</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"FileLogger: $message"</span><span class="p">)</span> + <span class="p">}</span> + <span class="nf">passToNext</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[ConcreteHandler: DatabaseLogger]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">DatabaseLogger</span><span class="p">(</span><span class="n">nextLogger</span><span class="p">:</span> <span class="nc">Logger</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span> <span class="p">:</span> <span class="nc">Logger</span><span class="p">(</span><span class="n">nextLogger</span><span class="p">)</span> <span class="p">{</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">log</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="nc">LogLevel</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="k">if</span> <span class="p">(</span><span class="n">level</span> <span class="p">==</span> <span class="nc">LogLevel</span><span class="p">.</span><span class="nc">ERROR</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"DatabaseLogger: $message"</span><span class="p">)</span> + <span class="p">}</span> + <span class="nf">passToNext</span><span class="p">(</span><span class="n">level</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Client]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">loggerChain</span> <span class="p">=</span> <span class="nc">ConsoleLogger</span><span class="p">(</span><span class="nc">FileLogger</span><span class="p">(</span><span class="nc">DatabaseLogger</span><span class="p">()))</span> + + <span class="nf">println</span><span class="p">(</span><span class="s">"Sending INFO log..."</span><span class="p">)</span> + <span class="n">loggerChain</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nc">LogLevel</span><span class="p">.</span><span class="nc">INFO</span><span class="p">,</span> <span class="s">"This is an informational message."</span><span class="p">)</span> + + <span class="nf">println</span><span class="p">(</span><span class="s">"\nSending WARNING log..."</span><span class="p">)</span> + <span class="n">loggerChain</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nc">LogLevel</span><span class="p">.</span><span class="nc">WARNING</span><span class="p">,</span> <span class="s">"This is a warning message."</span><span class="p">)</span> + + <span class="nf">println</span><span class="p">(</span><span class="s">"\nSending ERROR log..."</span><span class="p">)</span> + <span class="n">loggerChain</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nc">LogLevel</span><span class="p">.</span><span class="nc">ERROR</span><span class="p">,</span> <span class="s">"This is an error message."</span><span class="p">)</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Output]</p> + +<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sending INFO log... +ConsoleLogger: This is an informational message. + +Sending WARNING log... +FileLogger: This is a warning message. + +Sending ERROR log... +DatabaseLogger: This is an error message. +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過 <strong>Chain of Responsibility Pattern</strong>,我們成功實現了動態的責任鏈結構,讓請求能被多個處理器依次處理。這不僅降低了客戶端與處理器之間的耦合,還提供了高度靈活性與擴展性,使系統更具彈性。此模式特別適合需要多層次處理的場景,例如日誌處理、請求驗證、事件處理等,為系統設計提供了強大的工具。</p>Nick Huang了解責任鏈模式如何讓請求能被多個對象動態處理,提升系統靈活性與可擴展性。Design Pattern (17) - Proxy Pattern (代理模式)2024-12-15T13:30:00+00:002024-12-15T13:30:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-17-proxy-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<h2 id="需求">需求</h2> + +<p>我們的任務是建立一個影片播放系統,需求如下:</p> + +<ul> + <li>應用能播放多個影片,但避免每次都重複下載相同的影片。</li> + <li>影片需要在用戶第一次訪問時下載,之後從快取中取得以節省資源。</li> + <li>提供一個透明的介面,無需讓客戶端知道影片是透過代理取得的。</li> +</ul> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_proxy_pattern_uml_1.png" alt="proxy_pattern_uml_1" /></p> + +<h2 id="察覺-forces">察覺 Forces</h2> + +<p>在未使用設計模式的情況下,我們可能面臨以下挑戰:</p> + +<ol> + <li> + <p><strong>高頻寬消耗 (High Bandwidth Usage)</strong>:</p> + + <ul> + <li>如果每次播放影片都重新下載,將導致不必要的頻寬浪費。</li> + </ul> + </li> + <li> + <p><strong>延遲時間 (High Latency)</strong>:</p> + + <ul> + <li>每次下載影片會增加播放前的等待時間,影響用戶體驗。</li> + </ul> + </li> + <li> + <p><strong>客戶端耦合 (Client Coupling)</strong>:</p> + <ul> + <li>如果客戶端需要處理影片的下載邏輯,會增加不必要的複雜性。</li> + </ul> + </li> +</ol> + +<h2 id="套用-proxy-pattern-solution-得到新的-context-resulting-context">套用 Proxy Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Proxy Pattern 解決這個問題。</p> + +<p>Proxy Pattern 提供了解決方案,通過引入 Proxy 物件來控制對核心物件的訪問,實現快取功能並提升效能。</p> + +<p>先來看一下 Proxy Pattern 的 UML:</p> + +<p><img src="/blog/assets/images/design_pattern_proxy_pattern_uml_2.png" alt="proxy_pattern_uml_2" /></p> + +<p>以下是 Proxy Pattern 的主要角色:</p> + +<ul> + <li><strong>Subject (主題介面)</strong>:定義核心物件與代理物件的共同介面。</li> + <li><strong>RealSubject (具體主題)</strong>:核心物件,負責實際下載與播放影片。</li> + <li><strong>Proxy (代理)</strong>:代理物件,控制對核心物件的訪問,實現快取功能。</li> +</ul> + +<p>將 Proxy Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_proxy_pattern_uml_3.png" alt="proxy_pattern_uml_3" /></p> + +<h2 id="物件導向程式設計-oop">物件導向程式設計 (OOP)</h2> + +<p>[Subject: VideoPlayer]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">VideoPlayer</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">download</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">String</span> + <span class="k">fun</span> <span class="nf">play</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[RealSubject: YoutubeVideoPlayer]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">YoutubeVideoPlayer</span> <span class="p">:</span> <span class="nc">VideoPlayer</span> <span class="p">{</span> + <span class="k">override</span> <span class="k">fun</span> <span class="nf">download</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Downloading video from YouTube: $name"</span><span class="p">)</span> + <span class="c1">// 模擬下載結果返回的影片資料</span> + <span class="k">return</span> <span class="s">"VideoData($name)"</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">play</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Playing video: $data"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Proxy: ProxyVideoPlayer]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ProxyVideoPlayer</span><span class="p">(</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">player</span><span class="p">:</span> <span class="nc">YoutubeVideoPlayer</span> +<span class="p">)</span> <span class="p">:</span> <span class="nc">VideoPlayer</span> <span class="p">{</span> + + <span class="k">private</span> <span class="kd">val</span> <span class="py">cacheVideoList</span> <span class="p">=</span> <span class="n">mutableMapOf</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">String</span><span class="p">&gt;()</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">download</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span> + <span class="k">return</span> <span class="k">if</span> <span class="p">(</span><span class="n">cacheVideoList</span><span class="p">.</span><span class="nf">containsKey</span><span class="p">(</span><span class="n">name</span><span class="p">))</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Fetching video from cache: $name"</span><span class="p">)</span> + <span class="n">cacheVideoList</span><span class="p">[</span><span class="n">name</span><span class="p">]</span><span class="o">!!</span> + <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"First time download for: $name"</span><span class="p">)</span> + <span class="kd">val</span> <span class="py">videoData</span> <span class="p">=</span> <span class="n">player</span><span class="p">.</span><span class="nf">download</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> + <span class="n">cacheVideoList</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="p">=</span> <span class="n">videoData</span> + <span class="n">videoData</span> + <span class="p">}</span> + <span class="p">}</span> + + <span class="k">override</span> <span class="k">fun</span> <span class="nf">play</span><span class="p">(</span><span class="n">data</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="n">player</span><span class="p">.</span><span class="nf">play</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Client: VideoPlayerManager]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">VideoPlayerManager</span><span class="p">(</span><span class="k">private</span> <span class="kd">val</span> <span class="py">player</span><span class="p">:</span> <span class="nc">VideoPlayer</span><span class="p">)</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">playVideo</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Request to play video: $name"</span><span class="p">)</span> + <span class="kd">val</span> <span class="py">videoData</span> <span class="p">=</span> <span class="n">player</span><span class="p">.</span><span class="nf">download</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> + <span class="n">player</span><span class="p">.</span><span class="nf">play</span><span class="p">(</span><span class="n">videoData</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="c1">// Using ProxyVideoPlayer</span> + <span class="kd">val</span> <span class="py">youtubePlayer</span> <span class="p">=</span> <span class="nc">YoutubeVideoPlayer</span><span class="p">()</span> + <span class="kd">val</span> <span class="py">proxyPlayer</span> <span class="p">=</span> <span class="nc">ProxyVideoPlayer</span><span class="p">(</span><span class="n">youtubePlayer</span><span class="p">)</span> + <span class="kd">val</span> <span class="py">manager</span> <span class="p">=</span> <span class="nc">VideoPlayerManager</span><span class="p">(</span><span class="n">proxyPlayer</span><span class="p">)</span> + + <span class="c1">// Play video</span> + <span class="n">manager</span><span class="p">.</span><span class="nf">playVideo</span><span class="p">(</span><span class="s">"funny_cats.mp4"</span><span class="p">)</span> + <span class="n">manager</span><span class="p">.</span><span class="nf">playVideo</span><span class="p">(</span><span class="s">"funny_cats.mp4"</span><span class="p">)</span> <span class="c1">// using cache</span> + <span class="n">manager</span><span class="p">.</span><span class="nf">playVideo</span><span class="p">(</span><span class="s">"epic_fail.mp4"</span><span class="p">)</span> + <span class="n">manager</span><span class="p">.</span><span class="nf">playVideo</span><span class="p">(</span><span class="s">"funny_cats.mp4"</span><span class="p">)</span> <span class="c1">// using cache</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Output]</p> + +<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request to play video: funny_cats.mp4 +First <span class="nb">time </span>download <span class="k">for</span>: funny_cats.mp4 +Downloading video from YouTube: funny_cats.mp4 +Playing video: VideoData<span class="o">(</span>funny_cats.mp4<span class="o">)</span> + +Request to play video: funny_cats.mp4 +Fetching video from cache: funny_cats.mp4 +Playing video: VideoData<span class="o">(</span>funny_cats.mp4<span class="o">)</span> + +Request to play video: epic_fail.mp4 +First <span class="nb">time </span>download <span class="k">for</span>: epic_fail.mp4 +Downloading video from YouTube: epic_fail.mp4 +Playing video: VideoData<span class="o">(</span>epic_fail.mp4<span class="o">)</span> + +Request to play video: funny_cats.mp4 +Fetching video from cache: funny_cats.mp4 +Playing video: VideoData<span class="o">(</span>funny_cats.mp4<span class="o">)</span> +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>透過 <strong>Proxy Pattern</strong>,我們成功實現了影片快取的功能,解決了頻寬消耗與延遲時間過長的問題。此外,代理物件與核心物件共享相同的介面,對客戶端保持透明性,進一步降低耦合性。此模式特別適用於需要控制對資源訪問的場景,例如遠端代理、安全代理與智慧代理,為系統提供了靈活性與可擴展性。</p>Nick Huang了解代理模式如何通過控制對物件的訪問來提升系統的安全性、效能及靈活性。Design Pattern (16) - Flyweight Pattern (享元模式)2024-12-14T07:00:00+00:002024-12-14T07:00:00+00:00https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-16-flyweight-pattern<blockquote> + <p>您可於此 <a href="https://github.com/nickhuangcyh/design_pattern">design_pattern repo</a> 下載 Design Pattern 系列程式碼。</p> +</blockquote> + +<h2 id="需求">需求</h2> + +<p>假設我們正在開發一個森林場景的渲染系統,該系統需要顯示數百棵甚至數千棵樹木。</p> + +<p>每棵樹包含兩類資料:</p> + +<ol> + <li>內部狀態 (Intrinsic State):不隨環境改變的資料,例如樹的種類、顏色、紋理等。</li> + <li>外部狀態 (Extrinsic State):因環境而異的資料,例如樹的座標 (x, y)。</li> +</ol> + +<p>如果為每棵樹都建立完整的物件,將導致記憶體消耗過大。因此,我們需要一種共享內部狀態的方式來優化記</p> + +<h2 id="物件導向分析-ooa">物件導向分析 (OOA)</h2> + +<p>理解需求後,讓我們來快速實作物件導向分析吧!</p> + +<p><img src="/blog/assets/images/design_pattern_flyweight_pattern_uml_1.png" alt="flyweight_pattern_uml_1" /></p> + +<h2 id="察覺-forces">察覺 Forces</h2> + +<p>在設計階段,我們注意到以下設計難題:</p> + +<ol> + <li>大量重複資料:每棵樹都包含相同的種類、顏色和紋理資料。</li> + <li>性能問題:對於數千棵樹的場景渲染,過多的物件會導致記憶體不足或性能瓶頸。</li> + <li>共享與獨立的平衡:如何在共享資料的同時,保留每棵樹的獨立外部狀態。</li> +</ol> + +<p>為解決這些問題,我們採用了享元模式。</p> + +<h2 id="套用-flyweight-pattern-solution-得到新的-context-resulting-context">套用 Flyweight Pattern (Solution) 得到新的 Context (Resulting Context)</h2> + +<p>做完 OOA,察覺 Forces,看清楚整個 Context 後,就可以來套用 Flyweight Pattern 解決這個問題。</p> + +<p>先來看一下 flyweight Pattern 的 UML:</p> + +<p><img src="/blog/assets/images/design_pattern_flyweight_pattern_uml_2.png" alt="flyweight_pattern_uml_2" /></p> + +<ul> + <li><strong>Flyweight (享元介面)</strong>:定義共享物件的操作。</li> + <li><strong>ConcreteFlyweight (具體享元類別)</strong>:實作共享物件的功能,儲存可以共享的狀態。</li> + <li><strong>FlyweightFactory (享元工廠)</strong>:用於創建和管理共享物件,確保相同的物件只創建一次。</li> + <li><strong>Client (客戶端)</strong>:使用享元物件,並管理不能共享的狀態。</li> +</ul> + +<p>將 flyweight Pattern 套用到我們的應用吧</p> + +<p><img src="/blog/assets/images/design_pattern_flyweight_pattern_uml_3.png" alt="flyweight_pattern_uml_3" /></p> + +<h2 id="物件導向程式設計-oop">物件導向程式設計 (OOP)</h2> + +<p>[FFlyweight: Tree &amp; TreeType (樹類別)]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Tree</span><span class="p">(</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">x</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">y</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">type</span><span class="p">:</span> <span class="nc">TreeType</span> +<span class="p">)</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">draw</span><span class="p">()</span> <span class="p">{</span> + <span class="n">type</span><span class="p">.</span><span class="nf">draw</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> + +<span class="kd">class</span> <span class="nc">TreeType</span><span class="p">(</span> + <span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> + <span class="kd">val</span> <span class="py">color</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> + <span class="kd">val</span> <span class="py">texture</span><span class="p">:</span> <span class="nc">String</span> +<span class="p">)</span> <span class="p">{</span> + <span class="k">fun</span> <span class="nf">draw</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">y</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Drawing tree: $name, color: $color, texture: $texture at ($x, $y)"</span><span class="p">)</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[FlyweightFactory: TreeFactory (樹工廠類別)]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">object</span> <span class="nc">TreeFactory</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">treeTypes</span> <span class="p">=</span> <span class="n">mutableMapOf</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">TreeType</span><span class="p">&gt;()</span> + + <span class="k">fun</span> <span class="nf">getTreeType</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">color</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">texture</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">TreeType</span> <span class="p">{</span> + <span class="k">return</span> <span class="n">treeTypes</span><span class="p">.</span><span class="nf">computeIfAbsent</span><span class="p">(</span><span class="n">name</span><span class="p">)</span> <span class="p">{</span> + <span class="nf">println</span><span class="p">(</span><span class="s">"Creating new TreeType: $name"</span><span class="p">)</span> + <span class="nc">TreeType</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">color</span><span class="p">,</span> <span class="n">texture</span><span class="p">)</span> + <span class="p">}</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Client: Forest (森林類別)]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Forest</span> <span class="p">{</span> + <span class="k">private</span> <span class="kd">val</span> <span class="py">trees</span> <span class="p">=</span> <span class="n">mutableListOf</span><span class="p">&lt;</span><span class="nc">Tree</span><span class="p">&gt;()</span> + + <span class="k">fun</span> <span class="nf">plantTree</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">y</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">color</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">texture</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">treeType</span> <span class="p">=</span> <span class="nc">TreeFactory</span><span class="p">.</span><span class="nf">getTreeType</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">color</span><span class="p">,</span> <span class="n">texture</span><span class="p">)</span> + <span class="kd">val</span> <span class="py">tree</span> <span class="p">=</span> <span class="nc">Tree</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">treeType</span><span class="p">)</span> + <span class="n">trees</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">tree</span><span class="p">)</span> + <span class="p">}</span> + + <span class="k">fun</span> <span class="nf">draw</span><span class="p">()</span> <span class="p">{</span> + <span class="k">for</span> <span class="p">(</span><span class="n">tree</span> <span class="k">in</span> <span class="n">trees</span><span class="p">)</span> <span class="p">{</span> + <span class="n">tree</span><span class="p">.</span><span class="nf">draw</span><span class="p">()</span> + <span class="p">}</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Main Function]</p> + +<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> + <span class="kd">val</span> <span class="py">forest</span> <span class="p">=</span> <span class="nc">Forest</span><span class="p">()</span> + + <span class="c1">// Planting trees in the forest</span> + <span class="n">forest</span><span class="p">.</span><span class="nf">plantTree</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="s">"Oak"</span><span class="p">,</span> <span class="s">"Green"</span><span class="p">,</span> <span class="s">"Rough"</span><span class="p">)</span> + <span class="n">forest</span><span class="p">.</span><span class="nf">plantTree</span><span class="p">(</span><span class="mi">15</span><span class="p">,</span> <span class="mi">25</span><span class="p">,</span> <span class="s">"Pine"</span><span class="p">,</span> <span class="s">"Dark Green"</span><span class="p">,</span> <span class="s">"Smooth"</span><span class="p">)</span> + <span class="n">forest</span><span class="p">.</span><span class="nf">plantTree</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="s">"Oak"</span><span class="p">,</span> <span class="s">"Green"</span><span class="p">,</span> <span class="s">"Rough"</span><span class="p">)</span> <span class="c1">// Reuses the same TreeType as the first Oak</span> + + <span class="c1">// Draw all trees</span> + <span class="n">forest</span><span class="p">.</span><span class="nf">draw</span><span class="p">()</span> +<span class="p">}</span> +</code></pre></div></div> + +<p>[Output]</p> + +<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Creating new TreeType: Oak +Creating new TreeType: Pine +Drawing tree: Oak, color: Green, texture: Rough at <span class="o">(</span>10, 20<span class="o">)</span> +Drawing tree: Pine, color: Dark Green, texture: Smooth at <span class="o">(</span>15, 25<span class="o">)</span> +Drawing tree: Oak, color: Green, texture: Rough at <span class="o">(</span>10, 20<span class="o">)</span> +</code></pre></div></div> + +<h2 id="結論">結論</h2> + +<p>享元模式通過共享技術,有效降低了系統的記憶體使用量,提升了效能。它特別適用於需要大量重複物件的情境,例如文字編輯器、遊戲開發等場景。然而,在使用時需要小心區分內部與外部狀態,以確保系統設計的正確性與靈活性。</p>Nick Huang探索享元模式如何透過共享技術有效減少記憶體使用,提升應用效能。 \ No newline at end of file diff --git a/google/google-adsense/index.html b/google/google-adsense/index.html new file mode 100644 index 0000000..5d0a80c --- /dev/null +++ b/google/google-adsense/index.html @@ -0,0 +1,866 @@ + + + + + + +Google AdSense - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + Google AdSense + + +

+ +

如何透過 Google AdSense 爲我們的網站加入廣告賺取收益 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

我平時喜歡研究各種不同的技術,最近在想如何一邊分享技術幫助他人,一邊又能獲得一些收益用來再投入其他 Side Project 上,我還是想把我的內容免費提供給需要幫助的開發者,所以想採用置入一些廣告在網頁上的方式我想比較合適

+ +

什麼是 Google AdSense?

+ +

Google AdSense 是一個廣告推送平台,讓網站擁有者可以在自己的網站上展示與內容相關的廣告,並透過點擊或展示量賺取收益。這是一種被動收入的好方式,適合部落客、內容創作者或網站管理員。

+ +

為什麼選擇 Google AdSense?

+ +
    +
  1. 免費使用:註冊與使用 AdSense 完全免費。
  2. +
  3. 自動化廣告投放:系統會自動根據您的網站內容匹配相關廣告。
  4. +
  5. 多樣化廣告形式:可選擇文字、圖片、影片等多種形式的廣告。
  6. +
  7. 透明收益報告:提供詳細的報表分析,幫助您了解廣告效益。
  8. +
+ +

開始使用 Google AdSense 的步驟

+ +

1. 註冊 AdSense 帳戶

+ +
    +
  1. 前往 Google AdSense 官方網站
  2. +
  3. 使用您的 Google 帳號登入並申請 AdSense 帳戶。
  4. +
  5. 進入 AdSense 主頁面
  6. +
+ +

google_adsense_main

+ +

2. 輸入資訊

+ +
    +
  1. 點擊”輸入資訊”提供資訊
  2. +
  3. 填寫你的網站 domain、國家以及接受協議
  4. +
+ +

google_adsense_infomation

+ +

3. 選擇廣告呈現樣式

+ +
    +
  1. 點擊”探索”廣告在網站上的呈現模樣,並選擇
  2. +
+ +

google_adsense_ad_style

+ +

4. 將 AdSense 廣告程式碼加入網站並提交

+ +
    +
  1. 點擊”開始使用”,將你的網站連結到 AdSense
  2. +
  3. 將提供的程式碼插入 <head></head> 程式碼之間
  4. +
  5. 提交申請後,Google 會審核您的網站是否符合條件,通常需要幾天時間。
  6. +
+ +

google_adsense_code

+ +

結語

+ +

Google AdSense 是一個簡單而有效的網路變現工具,只要專注於創作高質量內容,就能為你的網站帶來穩定的廣告收入。快來試試,將您的網站流量轉化為實際收益吧!

+ + +
+ +
+ + + + + + + +

+ Tags: + + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/googlea8f8d0632c7c4644.html b/googlea8f8d0632c7c4644.html new file mode 100644 index 0000000..e010e73 --- /dev/null +++ b/googlea8f8d0632c7c4644.html @@ -0,0 +1 @@ +google-site-verification: googlea8f8d0632c7c4644.html \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3ae7fce --- /dev/null +++ b/index.html @@ -0,0 +1,2489 @@ + + + + + + +Nick's Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + + + +
+ +

+ + + + +

Recent posts

+ + + + +
+ + + + + +
+
+ +

+ + Design Pattern (25) - Strategy Pattern (策略模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

策略模式提供了一種靈活的解決方案,讓系統能根據需求動態切換不同的行為邏輯,實現高可擴展性與低耦合性。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (24) - State Pattern (狀態模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過狀態模式,設計一個飲水機的運作機制,根據不同狀態執行加熱、冷卻或待機的行為。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (23) - Observer Pattern (觀察者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過觀察者模式,實現安全系統主機的警報通知機制,當警報觸發時,主機自動通知平板、iOS 和 Android 手機。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (22) - Memento Pattern (備忘錄模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解備忘錄模式如何幫助我們實現狀態恢復,像是常見的 Ctrl+Z 功能,讓我們回到之前的操作狀態。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (20) - Iterator Pattern (迭代器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解迭代器模式如何提供一種順序來訪問集合內元素的方法,而不需要暴露集合的底層表示。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (17) - Proxy Pattern (代理模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解代理模式如何通過控制對物件的訪問來提升系統的安全性、效能及靈活性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (15) - Facade Pattern (外觀模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索外觀模式如何簡化系統複雜性,提供一個統一的介面來訪問子系統的功能,提升程式碼的可讀性與維護性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (13) - Composite Pattern (組合模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解組合模式如何以一致的方式操作單個物件與物件集合,實現對樹狀結構的靈活管理。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (12) - Bridge Pattern (橋接模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解橋接模式如何解耦抽象與實現,打造更靈活且易於擴展的系統設計,滿足複雜需求的同時降低維護成本。 +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (11) - Adapter Pattern (轉接器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解如何使用轉接器模式來解決介面不兼容問題,讓不同類別無縫合作,增強程式設計靈活性。 +

+
+
+ + + + + + +
+
+ +

+ + Google AdSense + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

如何透過 Google AdSense 爲我們的網站加入廣告賺取收益 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (9) - Prototype Pattern (原型模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入原型模式:探索如何透過物件複製技術,有效提升軟體開發中的資源管理與設計模式的靈活性。 +

+
+
+ + + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + + + + + +
+
+ +

+ + Design Pattern (8) - Builder Pattern (建造者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索建造者模式,學習如何分步構建複雜對象,使程式碼更加靈活和易於維護。通過實例展示如何使用建造者模式簡化對象創建過程,提升程式碼的可讀性和可擴展性。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (4) - UML (統一建模語言) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解UML,學習如何用UML圖清晰展現設計模式,提升軟體設計能力。 +

+
+
+ + + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Design Pattern (2) - Design Principles (設計原則) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

學習如何透過單一職責和開放封閉等設計原則提升程式碼質量,打造靈活、可維護的軟體系統。 +

+
+
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ + + + +
+
+ +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/3d_graphic_tips/index.html b/mobile/3d_graphic_tips/index.html new file mode 100644 index 0000000..f9af632 --- /dev/null +++ b/mobile/3d_graphic_tips/index.html @@ -0,0 +1,877 @@ + + + + + + +3D Graphic Engine Tips - 三角形 x UV mapping x Vertices & Indices - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 3D Graphic Engine Tips - 三角形 x UV mapping x Vertices & Indices + + +

+ +

寫 3D 繪圖程式必需要知道的知識 +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

前陣子在 iOS, Android 上開發 AR 相關的功能,因為本身沒有 3D 繪圖的相關知識,後來仔細研究,終於有一些成果,這篇用來紀錄有關 3D 繪圖的小小知識,希望可以幫助到其他人.

+ +

3D 圖形世界所有物件都是由小三角形所構成

+ +

3d_mesh

+ +
+

https://www.researchgate.net/figure/3D-mesh-triangles-with-different-resolution-3D-Modelling-for-programmers-Available-at_fig2_322096576

+
+ +

UV mapping

+ +
+

將 2D 圖像投影到 3D 模型表面進行 Texture 映射的 3D 建模過程

+
+ +

uv_mapping

+ +
+

https://en.wikipedia.org/wiki/UV_mapping

+
+ +

以立方體為例,將各面 Texture 壓平為 2D,在映射到 UV 座標中 +UV 座標,U 為橫軸,V 為縱軸,左上為(0,0),右上為(1,1)

+ +

uv_box_coordinate

+ +
+

https://wiki.povray.org/content/Reference:UV_Mapping

+
+ +

Vertices and Indices

+ +

vertices_and_indices

+ +
+

https://www.oreilly.com/library/view/real-time-3d-graphics/9781788629690/0e5b1b24-f1a7-414d-868b-37df694749ad.xhtml

+
+ +

順著頂點標示出頂點的 Index,根據要畫出的三角形,以逆時鐘方向依序填入 Index

+ +

另一種方式可以用安培右手定則,旋轉方向為 index 的順序,大拇指指向方向為面朝向的方位

+ +

因此在 3D 世界要畫出雙面三角形,code 裡的 index 必須為 [0, 2, 1, 0, 1, 2][0, 2, 1] 為正面,[0, 1, 2] 為背面

+ +
+

https://www.oreilly.com/library/view/real-time-3d-graphics/9781788629690/0e5b1b24-f1a7-414d-868b-37df694749ad.xhtml

+
+ +

總結

+ +

不管是在開發 iOS, Android, Web,只要與 3D 相關,3D 遊戲, AR, VR 等等都會用到,是非常實用的觀念!

+ +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/p2p/aws/p2p-tech-3-webrtc-kvs/index.html b/p2p/aws/p2p-tech-3-webrtc-kvs/index.html new file mode 100644 index 0000000..a15991f --- /dev/null +++ b/p2p/aws/p2p-tech-3-webrtc-kvs/index.html @@ -0,0 +1,956 @@ + + + + + + +搞懂 P2P 技術 (3) - WebRTC x AWS x KVS - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 搞懂 P2P 技術 (3) - WebRTC x AWS x KVS + + +

+ +

WebRTC x AWS x KVS +

+ + + +

+ + + + + + + + + + + + + + + + + + + 2 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

WebRTC

+ +

全名 Web Real-Time Communication,是一個支援網頁瀏覽器進行即時語音對話或影片對話的 API

+ +
+

WebRTC Wiki

+
+ +

WebRTC 的底層就是使用 ICE 來進行 P2P 打洞

+ +

Signaling Server

+ +

信令伺服器,用來交換雙方的 SDP 及 Ice candidate 來完成 P2P 打洞

+ +

實作 Signaling Server

+ +

WebRTC 沒有明確定義如何實作 Signaling Server,主要原因在於如果雙方一開始就知道對方的資訊,那其實就不需要 Signaling Server 來交換資訊

+ +

實作一個 Signaling Server 方式有很多種,可以用 HTTP 協議也可以用 WebSocket 協議,只要能順利將雙方的資訊做交換即可

+ +

SDP (Session Description Protocol)

+ +

會話描述協議(Session Description Protocol 或簡寫 SDP)描述的是流媒體的初始化參數。此協議由 IETF 發表為 RFC 2327

+ +

SDP 格式

+ +
v=0
+o=mhandley 2890844526 2890842807 IN IP4 126.16.64.4
+s=SDP Seminar
+i=A Seminar on the session description protocol
+u=http://www.cs.ucl.ac.uk/staff/M.Handley/sdp.03.ps
+e=mjh@isi.edu (Mark Handley)
+c=IN IP4 224.2.17.12/127
+t=2873397496 2873404696
+a=recvonly
+m=audio 49170 RTP/AVP 0
+m=video 51372 RTP/AVP 31
+m=application 32416 udp wb
+a=orient:portrait
+
+ +
    +
  • v=協議版本
  • +
  • o=發起者的 Session、Session ID 及 Session 版本
  • +
  • s=Session 名字
  • +
  • i=Session 資訊
  • +
  • u=有關會議資訊的 url
  • +
  • e=Email
  • +
  • p=手機號碼
  • +
  • c=連線資訊
  • +
  • t = Session 活動時間
  • +
  • m = 媒體資訊 ((media) (port) (transport) (fmt list))
  • +
  • a = 媒體屬性
  • +
+ +

Ice Candidate

+ +

Ice Candidate 描述 WebRTC 能與 遠程設備通訊所需的協議和路由,啟動 WebRTC P2P 後,通常會在連接的每一端提供多個 IceCandidate,直到絕定最佳線路達成為止.

+ +
{
+  "sdpMLineIndex": 0,
+  "sdpMid": "",
+  "candidate": "a=candidate:2999745851 1 udp 2113937151 192.168.56.1 51411 typ host generation 0"
+}
+
+ +

WebRTC Flow

+ +

p2p_webrtc

+ +
    +
  1. 雙方 Peer 先連上 Signaling Server
  2. +
  3. PeerA 取得自身 SDP 並呼叫 setLocalDescription
  4. +
  5. PeerA 將 SDP 傳給 Signaling Server
  6. +
  7. Signaling Server 將 PeerA 的 SDP 送給 PeerB
  8. +
  9. PeerB 呼叫 setRemoteDescription 將 PeerA 的 SDP 寫入
  10. +
  11. PeerB 取得自身 SDP 並呼叫 setLocalDescription
  12. +
  13. PeerB 將 SDP 傳給 Signaling Server
  14. +
  15. Signaling Server 將 PeerB 的 SDP 送給 PeerA
  16. +
  17. PeerA 呼叫 setRemoteDescription 將 PeerB 的 SDP 寫入
  18. +
  19. PeerA 向 Stun server 詢問 public IP
  20. +
  21. Stun server 回應 public IP
  22. +
  23. PeerA 向 TURN server 詢問 relay 資訊 (relay ip/port)
  24. +
  25. TURN server 回應 relay 資訊
  26. +
  27. PeerA 將 Ice candidates 傳給 Signaling Server
  28. +
  29. Signaling Server 將 PeerA 的 Ice candidates 送給 PeerB
  30. +
  31. PeerB 呼叫 setRemoteIceCandidate 將 PeerA 的 Ice candidates 寫入
  32. +
  33. PeerB 向 Stun server 詢問 public IP
  34. +
  35. Stun server 回應 public IP
  36. +
  37. PeerB 向 TURN server 詢問 relay 資訊 (relay ip/port)
  38. +
  39. TURN server 回應 relay 資訊
  40. +
  41. PeerB 將 Ice candidates 傳給 Signaling Server
  42. +
  43. Signaling Server 將 PeerB 的 Ice candidates 送給 PeerA
  44. +
  45. PeerA 呼叫 setRemoteIceCandidate 將 PeerB 的 Ice candidates 寫入
  46. +
  47. P2P 通道建立完成
  48. +
+ +

AWS KVS (Amazon Kinesis Video Streams)

+ +

Amazon Kinesis Video Streams 以全受管功能提供符合標準的 WebRTC 實作。您可以使用 Amazon Kinesis Video Streams and WebRTC 安全地即時串流媒體,或在任何攝影機 IoT 裝置與符合 WebRTC 標準的行動或 Web 播放器之間,執行雙向音訊或視訊互動。因為是全受管功能,您不需要建置、執行或擴展任何與 WebRTC 相關的雲端基礎設施,例如訊號或媒體轉送伺服器,即可在應用程式和裝置之間安全地串流媒體。

+ +
+

簡單來說 KVS 就是幫你把 STUN, TURN, Signaling Server 加密權限驗證等等都實作了,WebRTC 的部分跟 KVS 是完全獨立的,你也可以選擇自己架設 STUN, TURN, Signaling Server 搭配 Google WebRTC 也能成功串流.

+
+ +

KVS 的 Signaling server 是用 WebSocket 去實作的

+ +
+

注意: WebSocket 與 Socket.IO 是不是一樣的,Socket.IO 是根據 Websocket 協議去實作,Socket.IO 有自己的通訊格式,請不要拿 Socket.IO 套件去串接 KVS,會失敗,有興趣可以參考這篇 【筆記】Socket,Websocket,Socket.io 的差異

+
+ +

成果

+ +

理解這些 P2P 相關的知識,我們就可以很清楚的實作每一步驟,出錯時也能清楚哪一部分出錯,甚至是替換 Stun / TURN / signaling server 都沒問題,下面貼一下成果圖

+ +

ios_webrtc +android_webrtc

+ +

踩雷補充

+ +
    +
  • 在串接 AWS KVS 時,AWS Android 的 sample code 是使用 tyrus 套件連 signaling server,但此套件在舊版有一些 SSL 問題有機會失敗,所以我把 tyrus 換成 okhttp 去連 signaling server,但一直連不上 403 Forbidden,iOS 用 starscream 連線正常,後來比對兩平台 URL 發現,Android 的會多做一次 url encode,在 tyrus 沒問題,但在 okhttp 上會有問題 (解法請參考 : https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-android/issues/74)
  • +
+ +

總結

+ +

P2P 的基本觀念大致就到這邊,如果想要更深入的理解 P2P 的朋友,可以看參考資源欄中的文章,或直接看 RFC ,裡面會說到每個協議的格式細節.

+ +

參考資源

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + , + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/p2p/p2p-tech-1-ipv4-nat/index.html b/p2p/p2p-tech-1-ipv4-nat/index.html new file mode 100644 index 0000000..8a2ce17 --- /dev/null +++ b/p2p/p2p-tech-1-ipv4-nat/index.html @@ -0,0 +1,1024 @@ + + + + + + +搞懂 P2P 技術 (1) - P2P x IPv4 x NAT - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 搞懂 P2P 技術 (1) - P2P x IPv4 x NAT + + +

+ +

P2P x IPv4 x NAT +

+ + + +

+ + + + + + + + + + + + + + + + + + + 2 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

之前工作上遇到需要將自家 IPCam 與 iOS/Android 手機做 P2P 串流影音,研究了許多有關 P2P 的技術,因此想寫一系列有關 P2P 技術的文章.

+ +

P2P

+ +

為什麼會需要 P2P

+ +

在了解一門技術前,我們先來看看為什麼會需要這門技術,這門技術是為了解決什麼問題

+ +

Centralized vs Decentralized vs Distributed

+ +

中心化網路 (Centralized)

+ +

p2p_centralized

+ +

所有 client 都連接到同一台 Server,Server 擁有所有 client 的數據訊息

+ +
+

可以想成是國家央行發行貨幣,所有人的錢都來自國家央行

+
+ +
    +
  • 優點 +
      +
    • 部署簡單好維護
    • +
    • 集中管理數據
    • +
    +
  • +
  • 缺點 +
      +
    • 安全及隱私風險 (中心 server 遭破解串改,全部都會受影響)
    • +
    • 離 Server 較遠的 client 拿資料的時間會更長
    • +
    • server 一但故障,其他 client 都不可使用
    • +
    +
  • +
+ +

去中心化網路 (Decentralized)

+ +

p2p_decentralized

+ +

顧名思義就是不只有一台 server,會有多台 server ,所以當一台或多台 server 故障時,client 還是可以繼續從其他 server 存取數據

+ +
+

可以想成是很多銀行可以存錢領錢,有美銀、高盛、摩根、富國等等銀行,且每家都能互相轉錢

+
+ +
    +
  • 優點 +
      +
    • 中心化網路有較高的容錯率 (因為有多個 server)
    • +
    • 更好的性能 (在一些用戶多的地區新增節點)
    • +
    +
  • +
  • 缺點 +
      +
    • 安全及隱私風險 (與中心化一樣)
    • +
    • 較高維護成本 (系統設計較複雜多變)
    • +
    • 較難部署
    • +
    +
  • +
+ +

分佈式網路 (Distributed)

+ +

p2p_distributed

+ +

類似 Decentralized , 沒有唯一的中心 server,用戶間可以共享資料所有權,消除了中心 server 的概念,如此可以避免中心 server 故障導致所有 user 無法使用的問題,internet 就是一個分佈式網路,p2p 也屬於此類

+ +
+

可以想成是目前的區塊鏈比特幣,並沒有像央銀銀行的角色,貨幣是由鏈上的所有主機幫你做驗證交易紀錄等等,必須要串改超過 51% 以上的主機才能成功串改資訊,較安全

+
+ +
    +
  • 優點 +
      +
    • 極高的容錯率
    • +
    • 資料透明安全,不易遭串改
    • +
    • 資源共享
    • +
    +
  • +
  • 缺點 +
      +
    • 較高維護成本 (系統設計較複雜多變)
    • +
    • 較難部署
    • +
    +
  • +
+ +
+

三種網路方式皆有各自優點,可以依照遇到的情況去設計.

+
+ +

在 IoT 應用中,假如你要控制家中的 IPCam,就會有以下兩種做法

+ +
    +
  • 中心化方式為透過 Server 轉發 command 給 IPCam +
      +
    • p2p_centralized_connect
    • +
    • 這種作法的優點在於好部署修改,有新增功能時只要 Server 改完部署,其他設備就可更新支援此功能,缺點就是 Server 只要故障就完全不能控制 IoT 裝置,且 Server 的硬體效能要很好才足以應對龐大的 client,需要每月一筆租 Server 機器及流量的費用
    • +
    +
  • +
  • 分佈式方式為不透過 Server 直接向 IPCam 下達指令 +
      +
    • p2p_distributed_connect
    • +
    • 這種作法的優點在於不需要每月一筆租 server 機器及流量的費用,不會有 Server 故障導致無法操作 IoT 裝置的問題,硬體效能也不需要很好,缺點就是部署程式變得複雜,因為不是每個人都會定期更新手機 app 及 IoT Firmware,還有程式碼會變得複雜,需要處理 p2p tunnel 斷線重新 p2p 流程,也叫容易發生斷線重連,e.g. 手機由 4G/5G 環境走到有 Wifi 環境,網卡自動切換成 Wifi 就會導致斷線需重新 p2p.
    • +
    +
  • +
+ +
+

疑問 : 分佈式方式在複雜的 Internet 下,如何讓手機與 IoT 裝置溝通,就是接下來我們要講解 P2P 技術的部分

+
+ +

什麼是 P2P (Peer to Peer)

+ +

P2P 是一種無中心化的技術,每一個 client 既是 client 也是 server,在每台主機皆存在共享的數據,以此降低資料遺失安全性等等的風險.

+ +

IPv4

+ +

Internet 能夠如此成功進入人類的生活中,IP 的設計可以說是非常重要的一個基礎,可以把 IP 想成你在 Internet 世界裡的地址,任何人要寄信給你都需要知道你的地址才能與你通信,IPv4 的設計就是希望可以容納大家上網,IPv4 總共有 4 * 8(bits) = 32 bits,大約為 43 億,在當時認為已經非常足夠使用,但如今在 Internet 高速發展下,IPv4 已經不夠用了,所以網路專家一方面尋找 IPv4 替代方案(也就是 IPv6),一方面研究如何減緩 IPv4 耗盡的問題,於是 NAT 誕生了.

+ +

NAT (Network Address Translation)

+ +

就如同字面上的意思,他會在私有網域及公有網域之間做將封包的 IP 做轉換,根據 RFC1918 保留了三段 IP 地址給內部網路做使用,10.0.0.0-10.255.255.255;172.16.0.0-172.31.255.255;192.168.0.0-192.168.255.255,這些地址在公有網路上是沒有意義的,且不需像 IANA 做申請,如此就大大增加了 IPv4 環境可以容納的上網裝置,因為一個組織只需要一個公有 IP 即可讓所有內部裝置連上 Internet.

+ +

p2p_nat_1

+ +

但是 NAT 這樣的設計是有缺點的,外部網路主機無法直接與在 NAT 路由器內的內網主機進行連線

+ +

一般沒有 NAT 情況下,Internet 上的兩台主機只要知道彼此 IP 就能進行通訊

+ +

p2p_nat_2

+ +

在有一方於 NAT 情況下,外部網路主機 B 要與內部網路主機 A 通訊,就會遇到 NAT 不知道要將來自 B 的封包轉給哪一台內部主機

+ +

p2p_nat_3

+ +

除非 A 已經先訪問過 B 主機的情況下,NAT 會將 A 的轉址紀錄在 mapping table 中,之後由 B 發給 A 的封包,NAT 就會透過 mapping table 轉址將封包正確轉發給 A

+ +

p2p_nat_4

+ +

但如果兩台主機皆在 NAT 下,此時不管是由 A 或 B 發起 (A → B 或 B → A),都無法連接到對方,因為兩邊的 mapping table 都是空的沒有紀錄

+ +

p2p_nat_5

+ +

此時就必須要由任一方先發起封包,假設由 A 發起 (1),此時 A 的 NAT mapping table 會留下 A 的轉換 IP (2),但封包到了 B 的 NAT 時,因 B 的 NAT mapping table 為空,所以會先失敗 (3),再來由 B 發起封包給 A (4),會在 B NAT mapping table 留下 B 的轉換 IP (5),封包到了 A NAT 後 (6),因 A 的 NAT mapping table 存在 A 的 IP,所以封包可以順利到達 A (7),A 在發送封包到 B (8),此時雙方的 NAT mapping table 皆有紀錄(9, 10),在 NAT 裡的 Client 就能順利相互通訊,此時 P2P 就成功了,反之亦然

+ +

p2p_nat_6

+ +

NAT 類型

+ +

Full-cone NAT (完全錐型 NAT)

+ +
    +
  • 一旦內部位址(iAddr:iPort)對映到外部位址(eAddr:ePort),所有發自 iAddr:iPort 的封包都經由 eAddr:ePort 向外傳送。
  • +
  • 任意外部主機都能經由發送封包給 eAddr:ePort 到達 iAddr:iPort。
  • +
+ +

p2p_full_cone_nat

+ +

Restricted cone NAT (受限錐型 NAT)

+ +
    +
  • 一旦內部位址(iAddr:iPort)對映到外部位址(eAddr:ePort),所有發自 iAddr:iPort 的封包都經由 eAddr:ePort 向外傳送。
  • +
  • 唯 iAddr:iPort 曾經發送封包到外部主機(nAddr:any),外部主機才能經由發送封包給 eAddr:ePort 到達 iAddr:iPort。(註:any 指外部主機源埠不受限制。)
  • +
+ +

p2p_full_cone_nat

+ +

Port-Restricted cone NAT (端口受限錐型 NAT)

+ +

類似受限制錐形 NAT(Restricted cone NAT),但是還有埠限制。

+ +
    +
  • 一旦內部位址(iAddr:iPort)對映到外部位址(eAddr:ePort),所有發自 iAddr:iPort 的封包都經由 eAddr:ePort 向外傳送。
  • +
  • 在受限圓錐型 NAT 基礎上增加了外部主機源埠必須是固定的。
  • +
+ +

p2p_full_cone_nat

+ +

Symmetric NAT (對稱型 NAT)

+ +
    +
  • 每一個來自相同內部 IP 與埠,到一個特定目的地 IP 和埠的請求,都對映到一個獨特的外部 IP 和埠。 +同一內部 IP 與埠發到不同的目的地和埠的資訊包,都使用不同的對映
  • +
  • 只有曾經收到過內部主機資料的外部主機,才能夠把封包發回
  • +
+ +

p2p_full_cone_nat

+ +
+

Symmetric NAT 無法實現 P2P ,原因就在於對稱型每次的請求都會對應到不同的外部 IP 和 Port

+
+ +

結論

+ +

有關 P2P、IPv4、NAT 就先介紹到此,下一篇會介紹實現 P2P 需要哪些協定工具,STUN、TURN、ICE 是什麼

+ +

參考資源

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/p2p/p2p-tech-2-stun-turn-ice/index.html b/p2p/p2p-tech-2-stun-turn-ice/index.html new file mode 100644 index 0000000..b585c8f --- /dev/null +++ b/p2p/p2p-tech-2-stun-turn-ice/index.html @@ -0,0 +1,884 @@ + + + + + + +搞懂 P2P 技術 (2) - STUN x TURN x ICE - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 搞懂 P2P 技術 (2) - STUN x TURN x ICE + + +

+ +

STUN x TURN x ICE +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

上一篇介紹完中心化、去中心化、分佈式網路以及 IPv4、NAT、NAT 類型,但我們依舊還有些問題未解決

+ +
    +
  • A, B 兩端要如何知道彼此的內部網路 IP 及外部網路 IP (STUN)
  • +
  • 上篇有提到 NAT 類型如果是對稱型時,會無法打通 P2P,當遇到此情形時該 (TURN)
  • +
  • 有沒有一種框架整合這整個 P2P 流程 (ICE)
  • +
+ +

STUN

+ +

STUN(Session Traversal Utilities for NAT,NAT 對談穿越應用程式)是一種網路協定,它允許位於 NAT(或多重 NAT)後的客戶端找出自己的公網位址,查出自己位於哪種類型的 NAT 之後以及 NAT 為某一個本地埠所繫結的 Internet 端埠。這些資訊被用來在兩個同時處於 NAT 路由器之後的主機之間建立 UDP 通信。該協定由 RFC 5389 定義。

+ +
+

STUN Wiki

+
+ +

p2p_stun

+ +

TURN

+ +

TURN(全名 Traversal Using Relay NAT),是一種資料傳輸協定(data-transfer protocol)。允許在 TCP 或 UDP 的連線上跨越 NAT 或防火牆。

+ +

TURN 是一個 client-server 協定。TURN 的 NAT 穿透方法與 STUN 類似,都是通過取得應用層中的公有位址達到 NAT 穿透。但實現 TURN client 的終端必須在通訊開始前與 TURN server 進行互動,並要求 TURN server 產生”relay port”,也就是 relayed-transport-address。這時 TURN server 會建立 peer,即遠端端點(remote endpoints),開始進行中繼(relay)的動作,TURN client 利用 relay port 將資料傳送至 peer,再由 peer 轉傳到另一方的 TURN client。

+ +

當無法打通 NAT 時,就會需要用 TURN 的方式來解決 NAT 穿透

+ +
+

TURN Wiki

+
+ +

p2p_turn

+ +

ICE

+ +

ICE(Interactive Connectivity Establishment),一種綜合性的 NAT 穿越的技術。

+ +

互動式連接建立是由 IETF 的 MMUSIC 工作組開發出來的一種 framework,可整合各種 NAT 穿透技術,如 STUN、TURN(Traversal Using Relay NAT,中繼 NAT 實現的穿透)、RSIP(Realm Specific IP,特定域 IP)等。該 framework 可以讓 SIP 的客戶端利用各種 NAT 穿透方式打穿遠程的防火牆。

+ +
+

ICE Wiki

+
+ +

p2p_ice

+ +

總結

+ +

簡單來說

+ +
    +
  • STUN Server 是讓 client 端詢問自身的 Public IP,雙方交換 Public IP 來做 P2P
  • +
  • TURN Server 是當 NAT 穿透失敗時(通常是 Symmetric NAT)的替代方案,透過 TURN server 做資料的轉傳,也稱為 Relay,走 TURN 時 TURN Server 傳輸流量等等就會需要額外費用
  • +
  • ICE 是一種框架,結合了 STUN 及 TURN 等等協議,解決單一協議缺陷的問題,列如遇上 Symmetric NAT 但沒有實作 TURN,雙方就會無法通訊.
  • +
+ +

這篇介紹了 STUN、TURN 及 ICE,下一篇將介紹 Signaling Server, WebRTC 以及 AWS KVS for WebRTC.

+ +

參考資源

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pay/technology/google-wallet-smart-tap-exploring/index.html b/pay/technology/google-wallet-smart-tap-exploring/index.html new file mode 100644 index 0000000..ddb4398 --- /dev/null +++ b/pay/technology/google-wallet-smart-tap-exploring/index.html @@ -0,0 +1,1649 @@ + + + + + + +深入解析 Google Wallet Smart Tap:未來的支付方式 - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+ + + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

最近因工作之需,深入研究了 Google Wallet Smart Tap 相關技術,因此想撰寫這篇文章來記錄所學。這不僅能幫助我自己進行複習,也希望能對其他開發者提供幫助。🙂

+ +

什麼是 NFC

+ +

NFC(Near Field Communication,近場通訊)是一種使兩個裝置在幾厘米距離內進行通訊的短距離無線通訊技術。它被廣泛應用於支付、票務、資料交換等領域。

+ +

Google Wallet Smart Tap 簡介

+ +

Smart Tap 是 Google 利用 NFC 技術開發的一種專有通訊協議。它允許用戶通過移動裝置在支持的終端機上進行快速且安全的交易和資料交換。

+ +
+

如果你們公司是實作 Terminal 端,必須獲得認證才能使用此協議,這部分我有寫信問 Google 得到如下回覆,需要提供資訊簽署協議,Google 才會提供機密文件讓你實作。

+ +

If you are a terminal provider and would like to certify your terminal for use with Google Wallet, please provide more details about your terminal, intended functionality and target country/region. The documentation needed for Smart Tap certification is locked behind an NDA.

+ +

Once I have this information, my team and I will review and if the decision is to move forward with your request, we will begin the process of onboarding, starting with an NDA.

+
+ +

事前條件

+ +

我們需要完成兩個條件才能開始設置 smart tap

+ +
    +
  1. 創建 pass class 跟 pass object(s)
  2. +
  3. 與支援 Smart Tap 的終端供應商建立合作關係
  4. +
+ +

目前有支援 Smart Tap 的供應商如下

+ +
    +
  • Advanced Card Systems Ltd.
  • +
  • Axess AG
  • +
  • Castles Technology
  • +
  • Contactless Technologies B. V.
  • +
  • Dot Origin
  • +
  • Embed International
  • +
  • Equinox
  • +
  • HID
  • +
  • IDTech
  • +
  • Infinite Peripherals
  • +
  • Ingenico
  • +
  • Intercard Inc.
  • +
  • Janam
  • +
  • On Track Innovations
  • +
  • Pax
  • +
  • PinvAccess
  • +
  • Radius Network
  • +
  • Skidata
  • +
  • Socket Mobile
  • +
  • SpringCard
  • +
  • Techsigno SRL
  • +
  • UIC Payworld Inc.
  • +
  • Verifone
  • +
  • XAC
  • +
  • Zebra
  • +
+ +

Identifiers

+ +

在創建 pass class 跟 pass object(s) 之前,我們需要先知道一些 Smart Tap protocol 所使用的 Identifiers

+ +
    +
  • Redemption Issuer ID (兌換核發機構 ID)
  • +
  • Collector ID
  • +
  • Pass class ID
  • +
+ +

Issuer ID (核發機構 ID)

+ +

Issuer ID 是 Google Wallet 發卡機構唯一識別碼,可以在 Google Pay & Wallet Console 中找到

+ +

Redemption Issuer ID (兌換核發機構 ID)

+ +

Redemption Issuer ID 是一種特定的 Issuer ID,可以把 Redemption Issuer ID 當成是單一商家,Issuer 是儲存多個商家的 pass class 平台的”集合商家”

+ +

Issuer ID 可以代表商家、優惠提供方、商場(e.g. SOGO)、終端機製造商等等…,pass class & object(s) 開發出來後,會與 Redemption Issuer ID 相關聯,Issuer ID 包含了 pass class IDs 跟 object IDs

+ + + + + + + + + + + + + + + + + + + + + +
IDFormatNotes
Class IDissuerId.classSuffixThe classSuffix is a unique, developer-defined value for a specific pass class (e.g. a loyalty tier)
Object IDissuerId.objectSuffixThe objectSuffix is a unique, developer-defined value for a specific pass object (such as a user ID)
+ +

Collector ID (收款方 ID)

+ +
    +
  • 當商家的終端機支持 Smart Tap, 則 Redemption Issuer 將有一組 Collector ID
  • +
  • Collector ID 是一個 8 位數字的 ID
  • +
  • 當 User 將設備觸碰支援 Smart Tap 的終端時,終端會將 Collector ID 發送到 User 的設備。然後設備使用該 Collector ID 的 public key 向終端進行身份驗證(後面等等會提到通訊流程)
  • +
+ +
+
    +
  1. 一個 Issuer ID 只能分配一組 Collector ID
  2. +
  3. Collector ID 在所有 Issuer IDs 都是唯一的
  4. +
+
+ +

Pass Class ID (票證類 ID)

+ +

用於標示特定層級或票證類型。採用以下格式

+ +
+

issuerId.classSuffix

+
+ +

classSuffix 是由開發者定義此 pass class 獨有的值,透過此 pass class 所建立出來的 object class 可以被保存到 Google Wallet App 中。

+ +
+

Pass Class ID 為單一的 Issuer 帳號所有,但可以與多個 Redemption Issuer 相關聯

+
+ +

Communication Flow (通訊流程)

+ +

終端會使用 Collector ID 來標示自身,Collector ID 會 mapping 到一個 Redemption Issuer ID,當發生 Smart Tap 時,終端會將自身的 Collector ID 傳輸給 User 的設備。然後 Google Wallet App 會檢查每個儲存在設備中的 pass class ID 及 Collector ID。找到一個或多個匹配的票證,然後 Google Wallet App 會將這些匹配的票證傳輸到終端。

+ +

範例 1: 一個 Redemption Issuer

+ +

google_wallet_smart_tap_communication_flow_example1

+ +

上圖有兩個不同的 Issuer

+ +
    +
  • Issuer 2018 是 pass 開發者 (Aggregator)
  • +
  • Issuer 1990 是商家 fooPizza (Redemption Issuer)
  • +
+ +

Redemption Issuer - fooPizza 想要為他們的票證啟用 Smart Tap 功能,Aggregator 及 Redemption Issuer 必須完成下面的步驟,才能為商家終端啟用 Smart Tap

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
步驟角色說明
1Aggregator創建票證類和對象(在圖中分別為 abc 和 123)。
2Aggregator將 Redemption Issuer 的 ID 添加到票證類的 redemptionIssuers 屬性中。這會告知 Google 錢包,發卡機構 ID 1990 可以兌換引用此類的票證對象。
3Redemption Issuer獲取 Collector ID(在圖中,12345678)。
4Redemption Issuer在要使用的每個支持 Smart Tap 的終端上設置 Collector ID 12345678。任何類 ID 為 abc 且 Collector ID 為 12345678 的物件都將傳送給 Reader
+ +

範例 2: 多個 Redemption Issuer

+ +

一個 pass class 可以有多個 Redemption Issuer,為了能夠兌換特定 pass class,Redemption Issuer ID 必須包含在 pass class 創建時的 redemptionIssuers 屬性中,然後每個 Redemption Issuer 都有自己的 Collector ID,此 ID 會在終端上配置

+ +

google_wallet_smart_tap_communication_flow_example2

+ +

上圖有三個不同的 Issuer

+ +
    +
  • Issuer 8088 是 pass 開發者 (Aggregator)
  • +
  • Issuer 1990 是商家 fooPizza (Redemption Issuer)
  • +
  • Issuer 2018 是商家 yumPie (Redemption Issuer)
  • +
+ +

Aggregator 跟 Redemption Issuers 必須完成下面的步驟,才能為商家終端啟用 Smart Tap

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
步驟角色說明
1Aggregator建立票證類別和物件 (分別在圖中為 abc 和 123)。
2Aggregator請在票證類別的 redemptionIssuers 屬性中加入 Redemption Issuer ID。這會讓 Google 錢包瞭解 Issuer ID 1990 和 2018 可兌換參照這個類別的票證物件。
3Redemption Issuer取得 Collector ID (在圖表中,是 fooPizza 的 12345678,以及 yumPie 的 18802001)。
4Redemption Issuer在支援 Smart Tap 的每個終端機上設定對應的 Collector ID。只要物件具有類別 ID abc 且有相符的 Collector ID,系統就會將該物件傳達給 Reader。
+ +

範例 3: 沒有集合商家 (No Aggregator)

+ +

我們也可以使用同一個 Issuer 帳戶開發及核發票證類,沒有 Aggregator 管理多個 Redemption Issuer 的票證類。如果要兌換特定票證類,開發者必須在類別的 redemptionIssuers 屬性中加入 Issuer ID。開發人員必須取得 Collector ID,並在 Smart Tap 終端上設定 Collector ID。

+ +

google_wallet_smart_tap_communication_flow_example3

+ +

票證開發者必須完成下列步驟,為商家終端機啟用 Smart Tap。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
步驟角色說明
1Pass Developer建立票證類別和物件 (分別在圖中為 abc 和 123)。
2Pass Developer在票證類別的 redemptionIssuers 屬性中加入他們的 Issuer ID。這會讓 Google Wallet 瞭解 Issuer ID 2018 已獲準兌換參照這個類別的票證物件。
3Pass Developer取得 Collector ID (圖中的 12345678)。
4Pass Developer在支援 Smart Tap 的每個終端機上設定對應的 Collector ID。只要物件具有類別 ID abc 且有相符的 Collector ID,系統就會將該物件傳達給 Reader。
+ +

User experience and behavior

+ +

終端機和 Google 錢包應用程式之間傳輸的內容行為,視使用者和他們當下與 Google 錢包應用程式互動的方式而定。

+ +

情境 1:使用者開啟特定票證

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
步驟角色說明
1User在 Google 錢包應用程式中選取特定票證。
2User輕觸支援 Smart Tap 的感應式刷卡機。
3Terminal(Collector ID 相符) 票證會傳送至終端機。
(Collector ID 不相符) 票證不會傳送至終端機。
+ +
+

如果票證上的 Collector ID 與終端機的 Collector ID 相符,無論票證是否有效 (例如票證物件已過期),系統依然會傳送票證。

+
+ +

情境 2:Google 錢包首頁分頁或解鎖的螢幕檢視畫面

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
步驟角色說明
1User在 Google 錢包應用程式中開啟「首頁」分頁,或解鎖裝置的螢幕。
2User輕觸支援 Smart Tap 的感應式刷卡機。
3Terminal(單一有效的 Collector ID 相符) 票證會傳送至終端機。
(多個有效的 Collector ID 相符) 顯示有效票證的輪轉介面,並傳送使用者選取的項目。
+ +

Setup

+ +

Enable the Google Wallet API

+ +
    +
  1. 登入 Google Cloud console
  2. +
  3. 如果你沒有 Google Cloud project, 創建一個新的 (see Creating and managing projects for more information)
  4. +
  5. 為你的專案啟動 Google Wallet API (also referred to as Google Pay for Passes API)
  6. +
+ +

Create a service account and key

+ +

要調用 Google Wallet API,需要一個 service account 和一個 service account key。service account 是調用 Google Wallet API 的身份識別。service account key 包含一個 private key,用於將您的應用程式識別為該 service account。這個 key 非常敏感,因此需要妥善保管。

+ +

Create a service account

+ +
    +
  1. 在 Google Cloud console 中打開 Service Accounts
  2. +
  3. 為你的 service account 輸入名稱、ID、描述。
  4. +
  5. 選擇 CREATE AND CONTINUE
  6. +
  7. 選擇 DONE
  8. +
+ +

Create a service account key

+ +
    +
  1. 選擇你的 service account
  2. +
  3. 選擇 KEYS menu
  4. +
  5. 選擇 ADD KEY, then Create new key
  6. +
  7. 選擇 the JSON key type
  8. +
  9. 選擇 CREATE
  10. +
+ +

Set the GOOGLE_APPLICATION_CREDENTIALS environment variable

+ +

GOOGLE_APPLICATION_CREDENTIALS 環境變數被 Google SDK 使用來作為 Service Account 的身份驗證以及訪問 Google Cloud 項目的不同 API。

+ +
    +
  1. 按照 Google Cloud Service account 密鑰文件 中的指示設置 GOOGLE_APPLICATION_CREDENTIALS 環境變數。
  2. +
+ +

可以在 terminal 輸入下面指令,或直接加到 .bashrc or .zshrc 即可

+ +
export GOOGLE_APPLICATION_CREDENTIALS="/Users/nickhuang/Documents/wallet_serviceaccount_key.json"
+
+ +
    +
  1. 在 Terminal 輸入下面指令來確定有正確配置
  2. +
+ +
echo $GOOGLE_APPLICATION_CREDENTIALS
+
+ +

Authorize the service account

+ +

最後,我們還需要授權 service account 來管理 Google Wallet passes。

+ +
    +
  1. 打開 Google Pay & Wallet Console
  2. +
  3. 選擇 Users
  4. +
  5. 選擇 Invite a user
  6. +
  7. 輸入 service account 的 email(例如:test-svc@myproject.iam.gserviceaccount.com)
  8. +
  9. Access level 下拉菜單中選擇 DeveloperAdmin
  10. +
  11. 選擇 Invite
  12. +
+ +

Issuer account configuration (核發機構帳戶設定)

+ +

Create a new Issuer account

+ +
    +
  1. 打開 Google Pay & Wallet Console
  2. +
  3. 按照螢幕上的指示創建一個發行者帳戶
  4. +
  5. 選擇 Google Wallet API
  6. +
  7. 確認您已瞭解服務條款和隱私政策
  8. +
  9. 將 Issuer ID 值複製到記事本或其他位置
  10. +
  11. Manager 標籤下選擇 Set up test accounts
  12. +
  13. 添加您要參加測試的任何 email address
  14. +
+ +

為 Issuer account 上傳 public key

+ +
+

注意: 如果感應式刷卡機供應商需要您先提供 Collector ID,才會給您 Public Key,您可以使用下方的示範金鑰產生 Public Key。感應式刷卡機供應商向您提供實際的公開金鑰之後,請務必移除這組示範金鑰。

+
+ +
-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEchyXj869zfmKhRi9xP7f2AK07kEo
+4lE7ZlWTN14jh4YBTny+hRGRXcUzevV9zSSPJlPHpqqu5pEwlv1xyFvE1w==
+-----END PUBLIC KEY-----
+
+ +

🗂️ [add_a_smart_tap_key.js]

+ +
const {
+    GoogleAuth
+} = require("google-auth-library");
+
+const keyFilePath =
+    process.env.GOOGLE_APPLICATION_CREDENTIALS || "/path/to/key.json";
+const baseUrl = "https://walletobjects.googleapis.com/walletobjects/v1";
+const credentials = require(keyFilePath);
+
+const httpClient = new GoogleAuth({
+    credentials: credentials,
+    scopes: "https://www.googleapis.com/auth/wallet_object.issuer",
+});
+
+(async () => {
+    /**
+     * Add a new public key to an Issuer account.
+     *
+     * @param {string} issuerId The issuer ID being used for this request.
+     */
+    async function addSmartTapKey(issuerId) {
+        // New smart tap key information
+        let patchBody = {
+            smartTapMerchantData: {
+                authenticationKeys: [{
+                    id: 1,
+                    publicKeyPem: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEchyXj869zfmKhRi9xP7f2AK07kEo\n4lE7ZlWTN14jh4YBTny+hRGRXcUzevV9zSSPJlPHpqqu5pEwlv1xyFvE1w==\n-----END PUBLIC KEY-----",
+                }, ],
+            },
+        };
+
+        try {
+            let response = await httpClient.request({
+                url: `${baseUrl}/issuer/${issuerId}`,
+                method: "PATCH",
+                data: patchBody,
+            });
+
+            console.log("Issuer patch response");
+            console.log(response);
+        } catch (err) {
+            console.error("Error adding Smart Tap key:", err);
+        }
+    }
+
+    // 使用您要測試的 issuerId 調用 addSmartTapKey 函數
+    let issuerId = "Your issuer ID";
+    await addSmartTapKey(issuerId);
+})();
+
+ +
node add_a_smart_tap_key.js
+
+ +

到 Google Pay & Wallet Console 的 Google Wallet API -> 其他功能,就會看到 public key 已成功上傳且產生出 Collector ID

+ +

google_wallet_smart_tap_public_key

+ +

取得 Collector ID

+ +

🗂️ [get_collector_id.js]

+ +
const {
+    GoogleAuth
+} = require("google-auth-library");
+
+// TODO: Define issuer ID
+let issuerId = "Your issuer ID";
+const keyFilePath =
+    process.env.GOOGLE_APPLICATION_CREDENTIALS || "/path/to/key.json";
+
+const baseUrl = "https://walletobjects.googleapis.com/walletobjects/v1";
+
+const credentials = require(keyFilePath);
+
+const httpClient = new GoogleAuth({
+    credentials: credentials,
+    scopes: "https://www.googleapis.com/auth/wallet_object.issuer",
+});
+
+async function getCollectorId(issuerId) {
+    try {
+        let response = await httpClient.request({
+            url: `${baseUrl}/issuer/${issuerId}`,
+            method: "GET",
+        });
+
+        console.log("Issuer response");
+        console.log(response);
+
+        return response.data.smartTapMerchantData.smartTapMerchantId;
+    } catch (error) {
+        console.error("Error fetching collector ID:", error);
+        throw error;
+    }
+}
+
+// Call the function and log the result
+getCollectorId(issuerId)
+    .then((collectorId) => {
+        console.log("Collector ID:", collectorId);
+    })
+    .catch((error) => {
+        console.error("Failed to get Collector ID:", error);
+    });
+
+ +
node get_collector_id.js
+
+ +

Merchant configuration (商家設定)

+ +
    +
  • 特定商家的 Issuer account ID
  • +
  • 要啟用智慧感應功能的 Redemption Issuer ID
  • +
  • 已設定 smart tap 的票證類別
  • +
+ +

Merchant configuration (商家設定)

+ +

下列步驟概述商家設定智慧感應功能的設定:

+ +
    +
  1. 向感應式刷卡機供應商索取公開金鑰和金鑰版本。終端機供應商可能會先要求收集器 ID。在這種情況下,請完成核發者設定中所述的步驟產生收集器 ID,然後返回這個頁面。
  2. +
  3. 將收集器 ID 提供給感應式刷卡機供應商。
  4. +
  5. 將以下資訊提供給商家: +
      +
    • Redemption Issuer ID,以及 Google Pay & Wallet Console 的 Link
    • +
    • Collector ID
    • +
    +
  6. +
  7. 通知商家應永久儲存 Redemption Issuer ID 和 Collector ID。如果票證開發人員需要啟用類別以進行智慧感應兌換功能,商家就必須提供 Redemption Issuer ID。
  8. +
+ +

終端機設定

+ +

感應式刷卡機供應商必須負責為商家位置使用的所有終端機設定下列屬性。這些值是由 Redemption Issuer 提供。

+ +
    +
  • Collector ID
  • +
  • Key version
  • +
  • Private key
  • +
+ +

為感應式刷卡機設定智慧感應功能後,票證開發人員就能讓更多票證類別在商家的感應式刷卡機兌換。如要新增票證類別的支援,您不需要進行額外的終端機設定。

+ +

Pass configuration (票證設定)

+ +

Pass class configuration (票證類別設定)

+ +

您必須在票證類別上設定下列屬性:

+ +
    +
  • enableSmartTap 設為 True
  • +
  • redemptionIssuers 設為將透過 Smart Tap 兌換與這個類別相關聯的票證物件
  • +
+ +
const {
+    GoogleAuth
+} = require("google-auth-library");
+
+// TODO: Define issuer ID
+let issuerId = "Your issuer ID";
+let classSuffix = "Your classSuffix";
+const classId = `${issuerId}.${classSuffix}`;
+const keyFilePath =
+    process.env.GOOGLE_APPLICATION_CREDENTIALS || "/path/to/key.json";
+
+const baseUrl = "https://walletobjects.googleapis.com/walletobjects/v1";
+
+const credentials = require(keyFilePath);
+
+const httpClient = new GoogleAuth({
+    credentials: credentials,
+    scopes: "https://www.googleapis.com/auth/wallet_object.issuer",
+});
+
+// Create a Loyalty SmartTap pass class
+let loyaltyClass = {
+    id: `${classId}`,
+    issuerName: "Climax technology",
+    programName: "Climax Loyalty SmartTap 2 Program Test",
+    enableSmartTap: true, // Enable Smart Tap
+    redemptionIssuers: [
+        // Add any Redemption Issuer IDs
+        "Your Redemption issuer ID",
+    ],
+    reviewStatus: "underReview",
+    programLogo: {
+        sourceUri: {
+            uri: "https://www.sourcesecurity.com/img/companies/300/climax-logo_1560425415.jpg",
+        },
+        contentDescription: {
+            defaultValue: {
+                language: "en-US",
+                value: "Program Logo",
+            },
+        },
+    },
+    textModulesData: [{
+        header: "Welcome to Your Loyalty SmartTap 2 Program",
+        body: "Thank you for joining our loyalty SmartTap 2 program. Enjoy exclusive rewards and benefits.",
+        id: "welcome_message",
+    }, ],
+    linksModuleData: {
+        uris: [{
+            uri: "https://www.climax.com.tw/",
+            description: "Visit our loyalty SmartTap 2 program",
+            id: "website",
+        }, ],
+    },
+    imageModulesData: [{
+        mainImage: {
+            sourceUri: {
+                uri: "https://www.sourcesecurity.com/img/companies/300/climax-logo_1560425415.jpg",
+            },
+            contentDescription: {
+                defaultValue: {
+                    language: "en-US",
+                    value: "Loyalty SmartTap 2 Program Banner",
+                },
+            },
+        },
+        id: "loyalty_banner",
+    }, ],
+    messages: [{
+        header: "Welcome",
+        body: "Thanks for joining our loyalty SmartTap 2 program!",
+        id: "welcome_message",
+    }, ],
+};
+
+// Check if the class exists already
+httpClient
+    .request({
+        url: `${baseUrl}/loyaltyClass/${classId}`,
+        method: "GET",
+    })
+    .then((response) => {
+        console.log("Class already exists");
+        console.log(response);
+
+        console.log("Class ID");
+        console.log(response.data.id);
+    })
+    .catch((err) => {
+        if (err.response && err.response.status === 404) {
+            // Class does not exist
+            // Create it now
+            httpClient
+                .request({
+                    url: `${baseUrl}/loyaltyClass`,
+                    method: "POST",
+                    data: loyaltyClass,
+                })
+                .then((response) => {
+                    console.log("Class insert response");
+                    console.log(response);
+
+                    console.log("Class ID");
+                    console.log(response.data.id);
+                });
+        } else {
+            // Something else went wrong
+            console.log(err);
+        }
+    });
+
+ +

Pass object configuration (票證物件設定)

+ +

若是傳遞物件,則必須設定 smartTapRedemptionValue

+ +
const {
+    GoogleAuth
+} = require("google-auth-library");
+const jwt = require("jsonwebtoken");
+
+// TODO: Define issuer ID
+let issuerId = "Your issuer ID";
+let classSuffix = "Your classSuffix"; // Use the loyalty class ID
+let objectSuffix = "Your objectSuffix";
+const objectId = `${issuerId}.${objectSuffix}`;
+const keyFilePath =
+    process.env.GOOGLE_APPLICATION_CREDENTIALS || "/path/to/key.json";
+
+const baseUrl = "https://walletobjects.googleapis.com/walletobjects/v1";
+
+const credentials = require(keyFilePath);
+
+const httpClient = new GoogleAuth({
+    credentials: credentials,
+    scopes: "https://www.googleapis.com/auth/wallet_object.issuer",
+});
+
+// Create a Loyalty SmartTap pass object with Smart Tap support
+let loyaltyObject = {
+    id: `${objectId}`,
+    classId: `${issuerId}.${classSuffix}`,
+    state: "active",
+    accountId: "123",
+    accountName: "Nick Huang",
+    textModulesData: [{
+        header: "Your Loyalty Points",
+        body: "You have 500 points.",
+        id: "loyalty_points",
+    }, ],
+    locations: [{
+        latitude: 37.422,
+        longitude: -122.084,
+    }, ],
+    smartTapRedemptionValue: "500",
+    infoModuleData: {
+        labelValueRows: [{
+            columns: [{
+                label: "Smart Tap ID",
+                value: "1234567890",
+            }, ],
+        }, ],
+    },
+};
+
+// Check if the object exists already
+httpClient
+    .request({
+        url: `${baseUrl}/loyaltyObject/${objectId}`,
+        method: "GET",
+    })
+    .then((response) => {
+        console.log("Object already exists");
+        console.log(response);
+
+        console.log("Object ID");
+        console.log(response.data.id);
+    })
+    .catch((err) => {
+        if (err.response && err.response.status === 404) {
+            // Object does not exist
+            // Create it now
+            httpClient
+                .request({
+                    url: `${baseUrl}/loyaltyObject`,
+                    method: "POST",
+                    data: loyaltyObject,
+                })
+                .then((response) => {
+                    console.log("Object insert response");
+                    console.log(response);
+
+                    console.log("Object ID");
+                    console.log(response.data.id);
+
+                    // Generate the "Add to Google Wallet" link
+                    generateAddToWalletLink(objectId);
+                });
+        } else {
+            // Something else went wrong
+            console.log(err);
+        }
+    });
+
+function generateAddToWalletLink(objectId) {
+    const payload = {
+        iss: credentials.client_email, // `client_email` in service account file.
+        aud: "google",
+        origins: ["http://localhost:3000"],
+        typ: "savetowallet",
+        payload: {
+            loyaltyObjects: [{
+                id: objectId,
+            }, ],
+        },
+    };
+
+    const token = jwt.sign(payload, credentials.private_key, {
+        algorithm: "RS256",
+    });
+    const addToWalletLink = `https://pay.google.com/gp/v/save/${token}`;
+
+    console.log("Add to Google Wallet link:");
+    console.log(addToWalletLink);
+}
+
+ +

執行完成會得到一組 JWT,在網頁上填入 https://pay.google.com/gp/v/save/{JWT} 即可將卡券加入你的 Google account 中

+ +

展示

+ +

下載 Smart tap sample app,並將 sample code 中的 Collector ID 改為自己的 Collector ID 當成刷卡機.

+ +

google_wallet_smart_tap_result1

+ +

google_wallet_smart_tap_result2

+ +

google_wallet_smart_tap_result3

+ +

參考

+ + + +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長 🙂

+ + +
+ + + + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..b545cad --- /dev/null +++ b/robots.txt @@ -0,0 +1 @@ +Sitemap: https://nickhuangcyh.github.io/blog/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..cc78fea --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,435 @@ + + + +https://nickhuangcyh.github.io/blog/blog/octopress-setup/ +2020-09-10T15:18:16+00:00 + + +https://nickhuangcyh.github.io/blog/blog/how-to-add-your-octopress-blog-website-to-google-search-console/ +2020-09-10T15:29:22+00:00 + + +https://nickhuangcyh.github.io/blog/blog/creating_a_github_pages_with_jekyll_and_minimal_mistakes/ +2021-12-29T07:45:03+00:00 + + +https://nickhuangcyh.github.io/blog/blog/how-to-add-your-jekyll-blog-website-to-google-search-console/ +2021-12-31T03:26:00+00:00 + + +https://nickhuangcyh.github.io/blog/mobile/3d_graphic_tips/ +2022-01-02T02:04:00+00:00 + + +https://nickhuangcyh.github.io/blog/p2p/p2p-tech-1-ipv4-nat/ +2022-01-03T15:45:03+00:00 + + +https://nickhuangcyh.github.io/blog/p2p/p2p-tech-2-stun-turn-ice/ +2022-01-04T07:09:00+00:00 + + +https://nickhuangcyh.github.io/blog/p2p/aws/p2p-tech-3-webrtc-kvs/ +2022-01-04T15:13:00+00:00 + + +https://nickhuangcyh.github.io/blog/tools/how-to-capture-network-packet-on-android-using-tcpdump/ +2022-11-06T15:30:00+00:00 + + +https://nickhuangcyh.github.io/blog/tools/how-to-capture-network-packet-on-ios/ +2022-11-09T03:30:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-1-object-oriented-concepts/ +2024-07-02T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-2-design-principle/ +2024-07-03T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-3-design-pattern/ +2024-07-04T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/pay/technology/google-wallet-smart-tap-exploring/ +2024-07-05T12:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-4-uml/ +2024-07-05T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-5-simple-factory-pattern/ +2024-07-06T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-6-factory-method-pattern/ +2024-07-07T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-7-abstract-factory-pattern/ +2024-07-08T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-8-builder-pattern/ +2024-07-09T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/tools/how-to-build-chiptool-for-android/ +2024-07-16T11:50:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-9-prototype-pattern/ +2024-07-21T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/devops/getting-started-with-github-container-registry/ +2024-07-23T10:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/cryptography/openssh/security/how-to-enable-rsa-encryption-algorithm-key-in-openssh-8.8/ +2024-08-02T11:50:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-10-singleton-pattern/ +2024-08-10T07:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/devops/jenkins-1-what-is-jenkins/ +2024-08-15T07:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/devops/jenkins-2-how-to-setup-jenkins-server/ +2024-08-15T09:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/devops/jenkins-3-configure-credentials-ssh/ +2024-08-16T09:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/google/google-adsense/ +2024-12-01T07:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-11-adapter-pattern/ +2024-12-07T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-12-bridge-pattern/ +2024-12-08T12:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/devops/jenkins-3-configure-credentials-ssh/ +2024-12-09T12:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-13-composite-pattern/ +2024-12-10T14:28:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-14-decorator-pattern/ +2024-12-11T15:30:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-15-facade-pattern/ +2024-12-12T15:30:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-16-flyweight-pattern/ +2024-12-14T07:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-17-proxy-pattern/ +2024-12-15T13:30:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-18-chain-of-responsibility-pattern/ +2024-12-16T15:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-19-command-pattern/ +2024-12-21T07:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-20-iterator-pattern/ +2024-12-22T06:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-21-mediator-pattern/ +2024-12-22T06:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-22-memento-pattern/ +2024-12-22T06:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-23-observer-pattern/ +2024-12-22T06:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-24-state-pattern/ +2024-12-22T07:00:00+00:00 + + +https://nickhuangcyh.github.io/blog/design%20pattern/design-pattern-25-strategy-pattern/ +2024-12-26T15:50:00+00:00 + + +https://nickhuangcyh.github.io/blog/categories/ + + +https://nickhuangcyh.github.io/blog/ + + +https://nickhuangcyh.github.io/blog/tags/ + + +https://nickhuangcyh.github.io/blog/tags/octopress/ + + +https://nickhuangcyh.github.io/blog/tags/jekyll/ + + +https://nickhuangcyh.github.io/blog/tags/minimal-mistakes/ + + +https://nickhuangcyh.github.io/blog/tags/theme/ + + +https://nickhuangcyh.github.io/blog/tags/google/ + + +https://nickhuangcyh.github.io/blog/tags/ios/ + + +https://nickhuangcyh.github.io/blog/tags/android/ + + +https://nickhuangcyh.github.io/blog/tags/3d/ + + +https://nickhuangcyh.github.io/blog/tags/opengl/ + + +https://nickhuangcyh.github.io/blog/tags/arkit/ + + +https://nickhuangcyh.github.io/blog/tags/arcore/ + + +https://nickhuangcyh.github.io/blog/tags/sceneform/ + + +https://nickhuangcyh.github.io/blog/tags/scenekit/ + + +https://nickhuangcyh.github.io/blog/tags/ipv4/ + + +https://nickhuangcyh.github.io/blog/tags/nat/ + + +https://nickhuangcyh.github.io/blog/tags/stun/ + + +https://nickhuangcyh.github.io/blog/tags/turn/ + + +https://nickhuangcyh.github.io/blog/tags/ice/ + + +https://nickhuangcyh.github.io/blog/tags/webrtc/ + + +https://nickhuangcyh.github.io/blog/tags/kvs/ + + +https://nickhuangcyh.github.io/blog/tags/network/ + + +https://nickhuangcyh.github.io/blog/tags/packet/ + + +https://nickhuangcyh.github.io/blog/tags/wireshark/ + + +https://nickhuangcyh.github.io/blog/tags/rvictl/ + + +https://nickhuangcyh.github.io/blog/tags/object-oriented-concepts/ + + +https://nickhuangcyh.github.io/blog/tags/design-principle/ + + +https://nickhuangcyh.github.io/blog/tags/design-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/google-wallet/ + + +https://nickhuangcyh.github.io/blog/tags/smart-tap/ + + +https://nickhuangcyh.github.io/blog/tags/nfc/ + + +https://nickhuangcyh.github.io/blog/tags/payment-systems/ + + +https://nickhuangcyh.github.io/blog/tags/uml/ + + +https://nickhuangcyh.github.io/blog/tags/simple-factory-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/factory-method-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/abstract-factory-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/builder-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/chip/ + + +https://nickhuangcyh.github.io/blog/tags/matter/ + + +https://nickhuangcyh.github.io/blog/tags/prototype-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/docker/ + + +https://nickhuangcyh.github.io/blog/tags/container-registry/ + + +https://nickhuangcyh.github.io/blog/tags/github-actions/ + + +https://nickhuangcyh.github.io/blog/tags/ci-cd/ + + +https://nickhuangcyh.github.io/blog/tags/devops-tools/ + + +https://nickhuangcyh.github.io/blog/tags/rsa-encryption/ + + +https://nickhuangcyh.github.io/blog/tags/openssh-8-8/ + + +https://nickhuangcyh.github.io/blog/tags/encryption-support/ + + +https://nickhuangcyh.github.io/blog/tags/singleton-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/jenkins/ + + +https://nickhuangcyh.github.io/blog/tags/devops/ + + +https://nickhuangcyh.github.io/blog/tags/credentials/ + + +https://nickhuangcyh.github.io/blog/tags/ssh/ + + +https://nickhuangcyh.github.io/blog/tags/adsense/ + + +https://nickhuangcyh.github.io/blog/tags/adapter-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/bridge-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/composite-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/decorator-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/facade-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/flyweight-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/proxy-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/chain-of-responsibility-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/command-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/iterator-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/mediator-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/memento-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/observer-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/state-pattern/ + + +https://nickhuangcyh.github.io/blog/tags/strategy-pattern/ + + +https://nickhuangcyh.github.io/blog/categories/blog/ + + +https://nickhuangcyh.github.io/blog/categories/mobile/ + + +https://nickhuangcyh.github.io/blog/categories/p2p/ + + +https://nickhuangcyh.github.io/blog/categories/aws/ + + +https://nickhuangcyh.github.io/blog/categories/tools/ + + +https://nickhuangcyh.github.io/blog/categories/design-pattern/ + + +https://nickhuangcyh.github.io/blog/categories/pay/ + + +https://nickhuangcyh.github.io/blog/categories/technology/ + + +https://nickhuangcyh.github.io/blog/categories/devops/ + + +https://nickhuangcyh.github.io/blog/categories/cryptography/ + + +https://nickhuangcyh.github.io/blog/categories/openssh/ + + +https://nickhuangcyh.github.io/blog/categories/security/ + + +https://nickhuangcyh.github.io/blog/categories/google/ + + +https://nickhuangcyh.github.io/blog/googlea8f8d0632c7c4644.html +2024-12-26T16:22:46+00:00 + + diff --git a/tags/3d/index.html b/tags/3d/index.html new file mode 100644 index 0000000..78abf15 --- /dev/null +++ b/tags/3d/index.html @@ -0,0 +1,324 @@ + + + + + + +3D - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

3D

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/abstract-factory-pattern/index.html b/tags/abstract-factory-pattern/index.html new file mode 100644 index 0000000..1fe3c5b --- /dev/null +++ b/tags/abstract-factory-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Abstract Factory Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Abstract Factory Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/adapter-pattern/index.html b/tags/adapter-pattern/index.html new file mode 100644 index 0000000..3a4d7d8 --- /dev/null +++ b/tags/adapter-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Adapter Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Adapter Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (11) - Adapter Pattern (轉接器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解如何使用轉接器模式來解決介面不兼容問題,讓不同類別無縫合作,增強程式設計靈活性。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/adsense/index.html b/tags/adsense/index.html new file mode 100644 index 0000000..36ca58c --- /dev/null +++ b/tags/adsense/index.html @@ -0,0 +1,324 @@ + + + + + + +AdSense - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

AdSense

+ + + + + + +
+
+ +

+ + Google AdSense + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

如何透過 Google AdSense 爲我們的網站加入廣告賺取收益 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/android/index.html b/tags/android/index.html new file mode 100644 index 0000000..de0fdd5 --- /dev/null +++ b/tags/android/index.html @@ -0,0 +1,559 @@ + + + + + + +Android - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Android

+ + + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/arcore/index.html b/tags/arcore/index.html new file mode 100644 index 0000000..2ff452d --- /dev/null +++ b/tags/arcore/index.html @@ -0,0 +1,324 @@ + + + + + + +ARCore - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

ARCore

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/arkit/index.html b/tags/arkit/index.html new file mode 100644 index 0000000..2a67bdb --- /dev/null +++ b/tags/arkit/index.html @@ -0,0 +1,324 @@ + + + + + + +ARKit - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

ARKit

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/bridge-pattern/index.html b/tags/bridge-pattern/index.html new file mode 100644 index 0000000..9f5387f --- /dev/null +++ b/tags/bridge-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Bridge Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Bridge Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (12) - Bridge Pattern (橋接模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解橋接模式如何解耦抽象與實現,打造更靈活且易於擴展的系統設計,滿足複雜需求的同時降低維護成本。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/builder-pattern/index.html b/tags/builder-pattern/index.html new file mode 100644 index 0000000..125b936 --- /dev/null +++ b/tags/builder-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Builder Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Builder Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (8) - Builder Pattern (建造者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索建造者模式,學習如何分步構建複雜對象,使程式碼更加靈活和易於維護。通過實例展示如何使用建造者模式簡化對象創建過程,提升程式碼的可讀性和可擴展性。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/chain-of-responsibility-pattern/index.html b/tags/chain-of-responsibility-pattern/index.html new file mode 100644 index 0000000..a8c3228 --- /dev/null +++ b/tags/chain-of-responsibility-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Chain of Responsibility Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Chain of Responsibility Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/chip/index.html b/tags/chip/index.html new file mode 100644 index 0000000..f2023ca --- /dev/null +++ b/tags/chip/index.html @@ -0,0 +1,324 @@ + + + + + + +CHIP - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

CHIP

+ + + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/ci-cd/index.html b/tags/ci-cd/index.html new file mode 100644 index 0000000..247f854 --- /dev/null +++ b/tags/ci-cd/index.html @@ -0,0 +1,512 @@ + + + + + + +CI/CD - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

CI/CD

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/command-pattern/index.html b/tags/command-pattern/index.html new file mode 100644 index 0000000..912c61c --- /dev/null +++ b/tags/command-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Command Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Command Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/composite-pattern/index.html b/tags/composite-pattern/index.html new file mode 100644 index 0000000..968d0c9 --- /dev/null +++ b/tags/composite-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Composite Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Composite Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (13) - Composite Pattern (組合模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解組合模式如何以一致的方式操作單個物件與物件集合,實現對樹狀結構的靈活管理。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/container-registry/index.html b/tags/container-registry/index.html new file mode 100644 index 0000000..99b1563 --- /dev/null +++ b/tags/container-registry/index.html @@ -0,0 +1,324 @@ + + + + + + +Container Registry - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Container Registry

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/credentials/index.html b/tags/credentials/index.html new file mode 100644 index 0000000..7c9799b --- /dev/null +++ b/tags/credentials/index.html @@ -0,0 +1,371 @@ + + + + + + +Credentials - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Credentials

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/decorator-pattern/index.html b/tags/decorator-pattern/index.html new file mode 100644 index 0000000..c6c81a9 --- /dev/null +++ b/tags/decorator-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Decorator Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Decorator Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/design-pattern/index.html b/tags/design-pattern/index.html new file mode 100644 index 0000000..73fd74a --- /dev/null +++ b/tags/design-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Design Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Design Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/design-principle/index.html b/tags/design-principle/index.html new file mode 100644 index 0000000..56d5bf0 --- /dev/null +++ b/tags/design-principle/index.html @@ -0,0 +1,324 @@ + + + + + + +Design Principle - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Design Principle

+ + + + + + +
+
+ +

+ + Design Pattern (2) - Design Principles (設計原則) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

學習如何透過單一職責和開放封閉等設計原則提升程式碼質量,打造靈活、可維護的軟體系統。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/devops-tools/index.html b/tags/devops-tools/index.html new file mode 100644 index 0000000..50757fa --- /dev/null +++ b/tags/devops-tools/index.html @@ -0,0 +1,324 @@ + + + + + + +DevOps Tools - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

DevOps Tools

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/devops/index.html b/tags/devops/index.html new file mode 100644 index 0000000..2fa110a --- /dev/null +++ b/tags/devops/index.html @@ -0,0 +1,465 @@ + + + + + + +DevOps - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

DevOps

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/docker/index.html b/tags/docker/index.html new file mode 100644 index 0000000..28aa601 --- /dev/null +++ b/tags/docker/index.html @@ -0,0 +1,371 @@ + + + + + + +Docker - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Docker

+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/encryption-support/index.html b/tags/encryption-support/index.html new file mode 100644 index 0000000..1566d37 --- /dev/null +++ b/tags/encryption-support/index.html @@ -0,0 +1,324 @@ + + + + + + +Encryption Support - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Encryption Support

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/facade-pattern/index.html b/tags/facade-pattern/index.html new file mode 100644 index 0000000..9b023e2 --- /dev/null +++ b/tags/facade-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Facade Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Facade Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (15) - Facade Pattern (外觀模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索外觀模式如何簡化系統複雜性,提供一個統一的介面來訪問子系統的功能,提升程式碼的可讀性與維護性。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/factory-method-pattern/index.html b/tags/factory-method-pattern/index.html new file mode 100644 index 0000000..6ef7f6c --- /dev/null +++ b/tags/factory-method-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Factory Method Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Factory Method Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/flyweight-pattern/index.html b/tags/flyweight-pattern/index.html new file mode 100644 index 0000000..4937b3c --- /dev/null +++ b/tags/flyweight-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Flyweight Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Flyweight Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/github-actions/index.html b/tags/github-actions/index.html new file mode 100644 index 0000000..10556b2 --- /dev/null +++ b/tags/github-actions/index.html @@ -0,0 +1,324 @@ + + + + + + +GitHub Actions - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

GitHub Actions

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/google-wallet/index.html b/tags/google-wallet/index.html new file mode 100644 index 0000000..1da6109 --- /dev/null +++ b/tags/google-wallet/index.html @@ -0,0 +1,324 @@ + + + + + + +Google Wallet - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Google Wallet

+ + + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/google/index.html b/tags/google/index.html new file mode 100644 index 0000000..9d7bb05 --- /dev/null +++ b/tags/google/index.html @@ -0,0 +1,324 @@ + + + + + + +Google - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Google

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/ice/index.html b/tags/ice/index.html new file mode 100644 index 0000000..d965a3e --- /dev/null +++ b/tags/ice/index.html @@ -0,0 +1,324 @@ + + + + + + +ICE - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

ICE

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 0000000..533f5f5 --- /dev/null +++ b/tags/index.html @@ -0,0 +1,7492 @@ + + + + + + +Posts by Tag - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + + + +
+ +

Posts by Tag

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+

Android

+
+ + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

iOS

+
+ + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

CI/CD

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Jenkins

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

DevOps

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Octopress

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Jekyll

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Network

+
+ + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Packet

+
+ + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Wireshark

+
+ + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Docker

+
+ + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+

Credentials

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

SSH

+
+ + + + + +
+ +
+ + + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Minimal-Mistakes

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Theme

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Google

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + +
+

3D

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

OpenGL

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

ARKit

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

ARCore

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Sceneform

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

SceneKit

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

IPv4

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

NAT

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

STUN

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

TURN

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

ICE

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

WebRTC

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

KVS

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + +
+

rvictl

+
+ + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Object-Oriented Concepts

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Design Principle

+
+ + + + + +
+
+ +

+ + Design Pattern (2) - Design Principles (設計原則) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

學習如何透過單一職責和開放封閉等設計原則提升程式碼質量,打造靈活、可維護的軟體系統。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Design Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Google Wallet

+
+ + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Smart Tap

+
+ + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

NFC

+
+ + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Payment Systems

+
+ + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

UML

+
+ + + + + +
+
+ +

+ + Design Pattern (4) - UML (統一建模語言) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解UML,學習如何用UML圖清晰展現設計模式,提升軟體設計能力。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Simple Factory Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Factory Method Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Abstract Factory Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Builder Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (8) - Builder Pattern (建造者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索建造者模式,學習如何分步構建複雜對象,使程式碼更加靈活和易於維護。通過實例展示如何使用建造者模式簡化對象創建過程,提升程式碼的可讀性和可擴展性。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

CHIP

+
+ + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Matter

+
+ + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Prototype Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (9) - Prototype Pattern (原型模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入原型模式:探索如何透過物件複製技術,有效提升軟體開發中的資源管理與設計模式的靈活性。 +

+
+
+ + +
+ Back to top ↑ +
+ + + + + +
+

Container Registry

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

GitHub Actions

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + +
+

DevOps Tools

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

RSA Encryption

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

OpenSSH 8.8

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Encryption Support

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Singleton Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + + + + + + + + + +
+

AdSense

+
+ + + + + +
+
+ +

+ + Google AdSense + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

如何透過 Google AdSense 爲我們的網站加入廣告賺取收益 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Adapter Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (11) - Adapter Pattern (轉接器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解如何使用轉接器模式來解決介面不兼容問題,讓不同類別無縫合作,增強程式設計靈活性。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Bridge Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (12) - Bridge Pattern (橋接模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解橋接模式如何解耦抽象與實現,打造更靈活且易於擴展的系統設計,滿足複雜需求的同時降低維護成本。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Composite Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (13) - Composite Pattern (組合模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解組合模式如何以一致的方式操作單個物件與物件集合,實現對樹狀結構的靈活管理。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Decorator Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Facade Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (15) - Facade Pattern (外觀模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

探索外觀模式如何簡化系統複雜性,提供一個統一的介面來訪問子系統的功能,提升程式碼的可讀性與維護性。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Flyweight Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Proxy Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (17) - Proxy Pattern (代理模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解代理模式如何通過控制對物件的訪問來提升系統的安全性、效能及靈活性。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Chain of Responsibility Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Command Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Iterator Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (20) - Iterator Pattern (迭代器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解迭代器模式如何提供一種順序來訪問集合內元素的方法,而不需要暴露集合的底層表示。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Mediator Pattern

+
+ + + + + +
+ +
+ + +
+ Back to top ↑ +
+ + + +
+

Memento Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (22) - Memento Pattern (備忘錄模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解備忘錄模式如何幫助我們實現狀態恢復,像是常見的 Ctrl+Z 功能,讓我們回到之前的操作狀態。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Observer Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (23) - Observer Pattern (觀察者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過觀察者模式,實現安全系統主機的警報通知機制,當警報觸發時,主機自動通知平板、iOS 和 Android 手機。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

State Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (24) - State Pattern (狀態模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過狀態模式,設計一個飲水機的運作機制,根據不同狀態執行加熱、冷卻或待機的行為。 +

+
+
+ + +
+ Back to top ↑ +
+ + + +
+

Strategy Pattern

+
+ + + + + +
+
+ +

+ + Design Pattern (25) - Strategy Pattern (策略模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

策略模式提供了一種靈活的解決方案,讓系統能根據需求動態切換不同的行為邏輯,實現高可擴展性與低耦合性。 +

+
+
+ + +
+ Back to top ↑ +
+ + + + +
+
+ +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/ios/index.html b/tags/ios/index.html new file mode 100644 index 0000000..8afbe57 --- /dev/null +++ b/tags/ios/index.html @@ -0,0 +1,512 @@ + + + + + + +iOS - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

iOS

+ + + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/ipv4/index.html b/tags/ipv4/index.html new file mode 100644 index 0000000..e9e6701 --- /dev/null +++ b/tags/ipv4/index.html @@ -0,0 +1,324 @@ + + + + + + +IPv4 - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

IPv4

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/iterator-pattern/index.html b/tags/iterator-pattern/index.html new file mode 100644 index 0000000..90d0b65 --- /dev/null +++ b/tags/iterator-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Iterator Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Iterator Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (20) - Iterator Pattern (迭代器模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解迭代器模式如何提供一種順序來訪問集合內元素的方法,而不需要暴露集合的底層表示。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/jekyll/index.html b/tags/jekyll/index.html new file mode 100644 index 0000000..425137e --- /dev/null +++ b/tags/jekyll/index.html @@ -0,0 +1,371 @@ + + + + + + +Jekyll - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Jekyll

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/jenkins/index.html b/tags/jenkins/index.html new file mode 100644 index 0000000..f8242d3 --- /dev/null +++ b/tags/jenkins/index.html @@ -0,0 +1,465 @@ + + + + + + +Jenkins - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Jenkins

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +

+ + Jenkins (2) - 如何架設 Jenkins 伺服器 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

學習如何使用 Docker 映像檔來架設 Jenkins 伺服器,提升開發團隊的自動化能力。 +

+
+
+ + + + + + +
+
+ +

+ + Jenkins (1) - 什麼是 Jenkins + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

了解Jenkins這個強大的自動化伺服器,如何幫助開發團隊實現持續整合與持續交付,提升軟體開發效率。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/kvs/index.html b/tags/kvs/index.html new file mode 100644 index 0000000..c6a2c86 --- /dev/null +++ b/tags/kvs/index.html @@ -0,0 +1,324 @@ + + + + + + +KVS - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

KVS

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/matter/index.html b/tags/matter/index.html new file mode 100644 index 0000000..d244ca2 --- /dev/null +++ b/tags/matter/index.html @@ -0,0 +1,324 @@ + + + + + + +Matter - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Matter

+ + + + + + +
+
+ +

+ + How to build CHIPTool for Android + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/mediator-pattern/index.html b/tags/mediator-pattern/index.html new file mode 100644 index 0000000..727870b --- /dev/null +++ b/tags/mediator-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Mediator Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Mediator Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/memento-pattern/index.html b/tags/memento-pattern/index.html new file mode 100644 index 0000000..5441858 --- /dev/null +++ b/tags/memento-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Memento Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Memento Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (22) - Memento Pattern (備忘錄模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解備忘錄模式如何幫助我們實現狀態恢復,像是常見的 Ctrl+Z 功能,讓我們回到之前的操作狀態。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/minimal-mistakes/index.html b/tags/minimal-mistakes/index.html new file mode 100644 index 0000000..b5a445a --- /dev/null +++ b/tags/minimal-mistakes/index.html @@ -0,0 +1,324 @@ + + + + + + +Minimal-Mistakes - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Minimal-Mistakes

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/nat/index.html b/tags/nat/index.html new file mode 100644 index 0000000..e47c81e --- /dev/null +++ b/tags/nat/index.html @@ -0,0 +1,324 @@ + + + + + + +NAT - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

NAT

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/network/index.html b/tags/network/index.html new file mode 100644 index 0000000..ea13161 --- /dev/null +++ b/tags/network/index.html @@ -0,0 +1,371 @@ + + + + + + +Network - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Network

+ + + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/nfc/index.html b/tags/nfc/index.html new file mode 100644 index 0000000..ca4c545 --- /dev/null +++ b/tags/nfc/index.html @@ -0,0 +1,324 @@ + + + + + + +NFC - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

NFC

+ + + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/object-oriented-concepts/index.html b/tags/object-oriented-concepts/index.html new file mode 100644 index 0000000..a24a096 --- /dev/null +++ b/tags/object-oriented-concepts/index.html @@ -0,0 +1,324 @@ + + + + + + +Object-Oriented Concepts - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Object-Oriented Concepts

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/observer-pattern/index.html b/tags/observer-pattern/index.html new file mode 100644 index 0000000..38c5f9d --- /dev/null +++ b/tags/observer-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Observer Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Observer Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (23) - Observer Pattern (觀察者模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過觀察者模式,實現安全系統主機的警報通知機制,當警報觸發時,主機自動通知平板、iOS 和 Android 手機。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/octopress/index.html b/tags/octopress/index.html new file mode 100644 index 0000000..0fe32d8 --- /dev/null +++ b/tags/octopress/index.html @@ -0,0 +1,371 @@ + + + + + + +Octopress - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Octopress

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/opengl/index.html b/tags/opengl/index.html new file mode 100644 index 0000000..0ac780e --- /dev/null +++ b/tags/opengl/index.html @@ -0,0 +1,324 @@ + + + + + + +OpenGL - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

OpenGL

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/openssh-8-8/index.html b/tags/openssh-8-8/index.html new file mode 100644 index 0000000..915bca0 --- /dev/null +++ b/tags/openssh-8-8/index.html @@ -0,0 +1,324 @@ + + + + + + +OpenSSH 8.8 - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

OpenSSH 8.8

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/packet/index.html b/tags/packet/index.html new file mode 100644 index 0000000..b4f200b --- /dev/null +++ b/tags/packet/index.html @@ -0,0 +1,371 @@ + + + + + + +Packet - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Packet

+ + + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/payment-systems/index.html b/tags/payment-systems/index.html new file mode 100644 index 0000000..35d0509 --- /dev/null +++ b/tags/payment-systems/index.html @@ -0,0 +1,324 @@ + + + + + + +Payment Systems - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Payment Systems

+ + + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/prototype-pattern/index.html b/tags/prototype-pattern/index.html new file mode 100644 index 0000000..b89e595 --- /dev/null +++ b/tags/prototype-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Prototype Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Prototype Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (9) - Prototype Pattern (原型模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入原型模式:探索如何透過物件複製技術,有效提升軟體開發中的資源管理與設計模式的靈活性。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/proxy-pattern/index.html b/tags/proxy-pattern/index.html new file mode 100644 index 0000000..e0ae888 --- /dev/null +++ b/tags/proxy-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Proxy Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Proxy Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (17) - Proxy Pattern (代理模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

了解代理模式如何通過控制對物件的訪問來提升系統的安全性、效能及靈活性。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/rsa-encryption/index.html b/tags/rsa-encryption/index.html new file mode 100644 index 0000000..a59f512 --- /dev/null +++ b/tags/rsa-encryption/index.html @@ -0,0 +1,324 @@ + + + + + + +RSA Encryption - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

RSA Encryption

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/rvictl/index.html b/tags/rvictl/index.html new file mode 100644 index 0000000..744cc13 --- /dev/null +++ b/tags/rvictl/index.html @@ -0,0 +1,324 @@ + + + + + + +rvictl - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

rvictl

+ + + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/sceneform/index.html b/tags/sceneform/index.html new file mode 100644 index 0000000..b6e0e3c --- /dev/null +++ b/tags/sceneform/index.html @@ -0,0 +1,324 @@ + + + + + + +Sceneform - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Sceneform

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/scenekit/index.html b/tags/scenekit/index.html new file mode 100644 index 0000000..017591c --- /dev/null +++ b/tags/scenekit/index.html @@ -0,0 +1,324 @@ + + + + + + +SceneKit - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

SceneKit

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/simple-factory-pattern/index.html b/tags/simple-factory-pattern/index.html new file mode 100644 index 0000000..5a11f94 --- /dev/null +++ b/tags/simple-factory-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Simple Factory Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Simple Factory Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/singleton-pattern/index.html b/tags/singleton-pattern/index.html new file mode 100644 index 0000000..3ea21be --- /dev/null +++ b/tags/singleton-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Singleton Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Singleton Pattern

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/smart-tap/index.html b/tags/smart-tap/index.html new file mode 100644 index 0000000..137cbef --- /dev/null +++ b/tags/smart-tap/index.html @@ -0,0 +1,324 @@ + + + + + + +Smart Tap - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Smart Tap

+ + + + + + +
+
+ +

+ + 深入解析 Google Wallet Smart Tap:未來的支付方式 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 9 minute read + + + +

+ + +

探索 Google Wallet Smart Tap 的運作原理和它如何改變我們的支付習慣。本文將帶你了解其背後的技術,以及它對未來支付生態系統的影響。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/ssh/index.html b/tags/ssh/index.html new file mode 100644 index 0000000..094041c --- /dev/null +++ b/tags/ssh/index.html @@ -0,0 +1,371 @@ + + + + + + +SSH - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

SSH

+ + + + + + +
+ +
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/state-pattern/index.html b/tags/state-pattern/index.html new file mode 100644 index 0000000..a1c90f7 --- /dev/null +++ b/tags/state-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +State Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

State Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (24) - State Pattern (狀態模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

透過狀態模式,設計一個飲水機的運作機制,根據不同狀態執行加熱、冷卻或待機的行為。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/strategy-pattern/index.html b/tags/strategy-pattern/index.html new file mode 100644 index 0000000..8803bbe --- /dev/null +++ b/tags/strategy-pattern/index.html @@ -0,0 +1,324 @@ + + + + + + +Strategy Pattern - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Strategy Pattern

+ + + + + + +
+
+ +

+ + Design Pattern (25) - Strategy Pattern (策略模式) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

策略模式提供了一種靈活的解決方案,讓系統能根據需求動態切換不同的行為邏輯,實現高可擴展性與低耦合性。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/stun/index.html b/tags/stun/index.html new file mode 100644 index 0000000..21d6dfd --- /dev/null +++ b/tags/stun/index.html @@ -0,0 +1,324 @@ + + + + + + +STUN - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

STUN

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/theme/index.html b/tags/theme/index.html new file mode 100644 index 0000000..3a87d79 --- /dev/null +++ b/tags/theme/index.html @@ -0,0 +1,324 @@ + + + + + + +Theme - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Theme

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/turn/index.html b/tags/turn/index.html new file mode 100644 index 0000000..77050c1 --- /dev/null +++ b/tags/turn/index.html @@ -0,0 +1,324 @@ + + + + + + +TURN - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

TURN

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/uml/index.html b/tags/uml/index.html new file mode 100644 index 0000000..3525c8f --- /dev/null +++ b/tags/uml/index.html @@ -0,0 +1,324 @@ + + + + + + +UML - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

UML

+ + + + + + +
+
+ +

+ + Design Pattern (4) - UML (統一建模語言) + + +

+ + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + +

深入了解UML,學習如何用UML圖清晰展現設計模式,提升軟體設計能力。 +

+
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/webrtc/index.html b/tags/webrtc/index.html new file mode 100644 index 0000000..7b56632 --- /dev/null +++ b/tags/webrtc/index.html @@ -0,0 +1,324 @@ + + + + + + +WebRTC - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

WebRTC

+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tags/wireshark/index.html b/tags/wireshark/index.html new file mode 100644 index 0000000..50967ff --- /dev/null +++ b/tags/wireshark/index.html @@ -0,0 +1,371 @@ + + + + + + +Wireshark - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + +
+ + + +
+ +

Wireshark

+ + + + + + +
+
+ +

+ + 如何抓取 iOS 的網路封包 + + +

+ + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+
+
+ + + + + + +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/how-to-build-chiptool-for-android/index.html b/tools/how-to-build-chiptool-for-android/index.html new file mode 100644 index 0000000..0933f13 --- /dev/null +++ b/tools/how-to-build-chiptool-for-android/index.html @@ -0,0 +1,969 @@ + + + + + + +How to build CHIPTool for Android - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + How to build CHIPTool for Android + + +

+ +

本篇文章我將介紹如何按照步驟 Build 出 CHIPTool apk +

+ + + +

+ + + + + + + + + + + + + + + + + + + 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

最近因工作之需,研究了一下如何 Build CHIPTool 的 apk,因為按照官方文件步驟實作會有錯誤,自己一步一步解決最後終於成功,因此想寫一篇文章紀錄,幫助我自己進行複習,也希望能對其他開發者提供幫助。🙂

+ +

簡介

+ +

Matter(前稱為Project CHIP,即Connected Home over IP)是一個統一的開源連接標準,旨在增強智能家居設備之間的互操作性和兼容性。這個標準由連接標準聯盟(CSA)開發,成員包括亞馬遜、蘋果、谷歌和Zigbee聯盟等主要行業參與者。

+ +

Matter的目標是簡化製造商的開發過程,確保智能家居產品的安全性、可靠性和易用性。它利用互聯網協議(IP)來實現各種設備、移動應用和雲服務之間的通信,支持Thread和Wi-Fi網絡傳輸。

+ +

Matter的目標是創建一個無縫且互操作的智能家居生態系統,使不同製造商的設備能夠順利協同工作。

+ +

事前準備

+ +

因為在個人開發環境 Build 容易破壞環境,會需要修改 ANDROID_HOME & ANDROID_NDK_HOME 等等…,因為 CHIP 有提供 build 環境的 image,所以這邊我選擇用 docker 使用 CHIP 的 image 啟 container 來 build.

+ +
    +
  • Docker
  • +
+ +

Pull Docker Image

+ +

這個步驟會需要一點時間,可以去喝杯咖啡

+ +
docker pull ghcr.io/project-chip/chip-build-android:latest
+
+ +

Run container

+ +
docker run -it -v ~/workspace/connectedhomeip:/connectedhomeip ghcr.io/project-chip/chip-build-android:latest
+
+ +

上面簡單的兩個步驟,我們就將乾淨可 Build CHIPTool 的環境建構好了.

+ +

將目錄標示為安全,方便git操作

+ +
git config --global --add safe.directory /connectedhomeip
+git config --global --add safe.directory /connectedhomeip/third_party/pigweed/repo
+
+ +

Download source code & sync submodule

+ +

這個步驟會超級久,因為資料量非常大,可以先去做其他事或睡一覺😂

+ +
git clone https://github.com/project-chip/connectedhomeip.git
+
+ +
git submodule sync && git submodule update --init
+
+ +

同意 Android sdk 的 licenses

+ +

我們需要先同意 Android sdk licenses,不然等等 build 的時候會出現錯誤(如下),因為我們沒有同意 licenses

+ +
FAILURE: Build failed with an exception.
+2024-07-16 09:31:40 WARNING 
+2024-07-16 09:31:40 WARNING * What went wrong:
+2024-07-16 09:31:40 WARNING Could not determine the dependencies of task ':app:compileDebugJavaWithJavac'.
+2024-07-16 09:31:40 WARNING > Failed to install the following Android SDK packages as some licences have not been accepted.
+2024-07-16 09:31:40 WARNING      build-tools;30.0.2 Android SDK Build-Tools 30.0.2
+2024-07-16 09:31:40 WARNING      platforms;android-31 Android SDK Platform 31
+2024-07-16 09:31:40 WARNING   To build this project, accept the SDK license agreements and install the missing components using the Android Studio SDK Manager.
+2024-07-16 09:31:40 WARNING   Alternatively, to transfer the license agreements from one workstation to another, see http://d.android.com/r/studio-ui/export-licenses.html
+2024-07-16 09:31:40 WARNING   
+2024-07-16 09:31:40 WARNING   Using Android SDK: /opt/android/sdk
+2024-07-16 09:31:40 WARNING 
+2024-07-16 09:31:40 WARNING * Try:
+2024-07-16 09:31:40 WARNING Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
+2024-07-16 09:31:40 WARNING 
+2024-07-16 09:31:40 WARNING * Get more help at https://help.gradle.org
+2024-07-16 09:31:40 WARNING 
+2024-07-16 09:31:40 WARNING BUILD FAILED in 1m 50s
+
+ +

為了方便我們先將PATH export,使其能在任何目錄下呼叫

+ +
export PATH=$PATH:/opt/android/sdk/tools/bin
+
+ +

同意所有 licenses

+ +
sdkmanager --licenses
+
+ +

全部輸入 y or yes

+ +

檢查環境變數

+ +

用 docker 使用 CHIP 提供的 image 環境好處之一就是我們不用再去下載指定的 sdk 及 ndk 版本,也不需要重新指向環境變數

+ +
root@f39e36c15675:/connectedhomeip# echo $ANDROID_HOME
+> /opt/android/sdk
+
+root@f39e36c15675:/connectedhomeip# echo $ANDROID_NDK_HOME
+> /opt/android/android-ndk-r23c
+
+ +

Preparing for build

+ +
    +
  1. 切換到 Matter repositoy
  2. +
+ +
cd /connectedhomeip
+
+ +
    +
  1. Run bootstrap (only required first time) 此步驟也需要一段時間
  2. +
+ +
source scripts/bootstrap.sh
+
+ +

Android CHIPTool from scripts

+ +
    +
  1. 執行 script
  2. +
+ +
./scripts/build/build_examples.py --target android-arm64-chip-tool build
+
+ +

但這個步驟跑到最後會出現錯誤說找不到 build.ninja

+ +
ninja: error: loading 'build.ninja': No such file or directory
+
+ +

我猜這邊是 CHIP 官方文件沒有寫完整,會無法進行下去,這邊我的解決方式是到 /out/android-arm64-chip-tool 目錄下去建立 build.ninja ,很簡單如下

+ +
cd /connectedhomeip/out/android-arm64-chip-tool
+gn gen .
+
+ +

這樣就會產生 build.ninja 檔案了

+ +

接下來回到 /connectedhomeip 目錄下

+ +
cd ../..
+
+ +

再執行一次就大功告成了! (Build 的過程會花比較久時間)

+ +
./scripts/build/build_examples.py --target android-arm64-chip-tool build
+
+ +

Build 完後可以在下面路徑找到 apk

+ +

out/android-$TARGET_CPU-chip-tool/outputs/apk/debug/app-debug.apk

+ +

參考

+ + + + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/how-to-capture-network-packet-on-android-using-tcpdump/index.html b/tools/how-to-capture-network-packet-on-android-using-tcpdump/index.html new file mode 100644 index 0000000..0b32228 --- /dev/null +++ b/tools/how-to-capture-network-packet-on-android-using-tcpdump/index.html @@ -0,0 +1,875 @@ + + + + + + +如何抓取 Android 的網路封包 - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 如何抓取 Android 的網路封包 + + +

+ +

抓封包利器 - tcpdump +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

最近工作上遇到需要抓封包分析才能釐清的問題

+ +

以前在開發 iOS 非常簡單,可以用 rvictl -s [iOS UUID] 在 Mac 上創建一個虛擬網卡介面,就可以開 Wireshark 抓 iOS 上的封包了

+ +

最近開發 Android 也遇到需要抓封包的情況,上網學習了一下,在這邊紀錄,希望可以幫到其他人🙂

+ +

事前準備

+ +
    +
  1. 一台 root 過的 Android Device
  2. +
  3. tcpdump tool
  4. +
  5. Wireshark
  6. +
+ +
+

補充說明: 如果沒有 root 的 Android Device,可以嘗試用 tPacketCapture 來抓包,它的原理類似開一個類似 VPN 的 app,發送的封包此 app 會抓取,但我個人實測,有時候有些封包會好像沒發出去,影響實際情況,在此不推薦!

+
+ +

將 tcpdump 放入 Android 裝置

+ +
adb push tcpdump /data/local/tcpdump
+
+ +

如果出現 can't execute: Permission denied,請以 root 身份執行 push,結束後再退出 root 身份

+ +
adb root
+adb push tcpdump /data/local/tcpdump
+adb unroot
+
+ +

執行 tcpdump

+

先到 /data/local 目錄下

+
adb shell
+su
+cd /data/local
+
+

修改 tcpdump 權限為可執行

+
chmod a+x tcpdump
+
+

執行 tcpdump 開始抓封包

+
./tcpdump -i any -p -s 0 -w /sdcard/capture.pcap
+
+

抓完封包後 control + c 結束抓包程序

+ +

導出 Android 封包檔案到電腦上

+
adb pull /sdcard/capture.pcap
+
+

在用 Wireshark 開啟即可 +wireshark_test_1

+ +

總結

+

抓封包是很重要的 debug 工具,很多時候使用第三方的 frameworks/libraries 較難查出是 server or client 的問題.

+ +

也有可能是 framework/library 本身沒寫好,需要修改第三方的程式碼

+ +

之前在做 iOS FFMpeg 在串 RTSP 時原始碼沒送GET_PARAMETER 的 keepalive 封包,導致 1 分鐘後就斷線,之後也是透過 Wireshark 抓包,改 FFMpeg 原始碼才解決

+ +

透過觀察封包能夠快速釐清問題,不需要花大量時間去猜測問題,是網路開發中很重要的技巧之一!

+ +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/how-to-capture-network-packet-on-ios/index.html b/tools/how-to-capture-network-packet-on-ios/index.html new file mode 100644 index 0000000..acc258c --- /dev/null +++ b/tools/how-to-capture-network-packet-on-ios/index.html @@ -0,0 +1,878 @@ + + + + + + +如何抓取 iOS 的網路封包 - Nick’s Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + +
+ +
+

+ + 如何抓取 iOS 的網路封包 + + +

+ +

利用遠端虛擬介面工具(rvictl)抓包好輕鬆! +

+ + + +

+ + + + + + + + + + + + + + + + + + + less than 1 minute read + + + +

+ + + + +
+ + + Photo credit: Unsplash + + +
+ + + + + +
+ + + + + +
+ + + + + +
+ + +
+ + + +

前言

+ +

上一篇介紹了 如何抓取 Android 的網路封包,順便也將之前如何抓 iOS 上的網路封包紀錄一下,希望能幫助到其他人.

+ +

事前準備

+ +
    +
  1. iOS Device
  2. +
  3. rvictl (通常安裝 XCode 時,會順便安裝附加工具)
  4. +
  5. Wireshark
  6. +
+ +

查看 Mac 上的網路介面

+ +
ifconfig -l
+
+

lo0 gif0 stf0 anpi1 anpi0 anpi2 en4 en5 en6 en1 en2 en3 ap1 en0 awdl0 bridge0 utun0 utun1 utun2 en7

+ +

查詢 iOS 裝置網路介面

+
    +
  1. 將 iOS 裝置連接到 Mac
  2. +
  3. 打開 XCode -> Window -> Devices and Simulators 查看 iOS 裝置的 UUID,如下圖
  4. +
+ +

ios_uuid

+ +

創建虛擬網路介面

+
rvictl -s 00xxxxxx-xxxxxxxxxxxxxx1E
+
+ +

如果出現

+
bootstrap_look_up(): 1102
+Starting device 00xxxxxx-xxxxxxxxxxxxxx1E [FAILED]
+
+

應該是因為 com.apple.rpmuxd 沒有啟動,用下面指令查詢

+
sudo launchctl list com.apple.rpmuxd
+
+

如果找不到,可以用下面指令重新載入

+
sudo launchctl load -w /Library/Apple/System/Library/LaunchDaemons/com.apple.rpmuxd.plist
+
+ +

成功創建後,在查看一次網路介面,會多一個 rvi0

+
ifconfig -l
+
+

lo0 gif0 stf0 anpi2 anpi1 anpi0 en4 en5 en6 en1 en2 en3 ap1 en0 bridge0 awdl0 llw0 en7 utun0 utun1 utun2 en9 en10 rvi0

+ +

Wireshark 抓包

+

開啟 Wireshark 後,選擇 rvi0 介面就可以觀察 iOS 裝置上的封包了,如下圖

+ +

wireshark_rvi0_interface

+ +

wireshark_test_2

+ +

刪除虛擬網路介面

+
rvictl -x 00xxxxxx-xxxxxxxxxxxxxx1E
+
+ +

總結

+

整體要在 iOS 上觀察封包,非常簡單,只需創建一個虛擬介面即可,Android 在這方便就複雜很多,還需要 root 裝置,有興趣可以參考這篇文章 如何抓取 Android 的網路封包

+ +

觀察封包在物聯網 IoT 領域是非常重要的 Debug 技巧,很多時候都是靠著觀察封包來快速釐清是 App / Server / Firmware 哪一方的問題,畢竟包封是不會騙人的😂

+ +

Note: 如果有任何建議、問題或不同想法,歡迎留言或寄信給我,可以一起討論進步成長🙂

+ + +
+ +
+ + + + + + + +

+ Tags: + + + , + + , + + , + + , + + + + +

+ + + + + + + + + +

+ Categories: + + + + + +

+ + + + +

Updated:

+ + +
+ + + + + + + +
+ + +
+ + +

Leave a comment

+
+ +
+ + +
+ + + + + + +
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file