Chuyển đến nội dung chính

Phương pháp hàm sinh trong toán rời rạc

```
DISCRETE MATHEMATICS / GENERATING FUNCTIONS

Phương pháp hàm sinh trong toán rời rạc và các dãy số

a0, a1, a2, ...  →  A(x) = a0 + a1x + a2x2 + ···

Phương pháp hàm sinh (generating function) biến một dãy số thành một chuỗi lũy thừa để những quan hệ giữa các phần tử có thể được xử lý bằng đại số. Nếu dãy ban đầu là a0, a1, a2, ..., ta mã hóa nó thành một hàm A(x) sao cho hệ số của xn chính là an.

```
```
Tóm tắt nội dung

Ý tưởng cốt lõi của hàm sinh là chuyển bài toán về dãy số thành bài toán đại số. Các phép dịch chỉ số như an-1 hoặc an-2 có thể được biểu diễn thông qua xA(x) và x2A(x). Nhờ đó, một hệ vô hạn các quan hệ giữa các phần tử có thể được rút gọn thành một phương trình theo A(x).

```

1. Hàm sinh biểu diễn dãy số như thế nào?

Cho dãy a0, a1, a2, a3, ... . Hàm sinh thông thường của dãy được định nghĩa bởi biểu thức:

A(x) = a0 + a1x + a2x2 + a3x3 + ···

Ký hiệu [xn]A(x) thường được dùng để chỉ hệ số của xn trong A(x). Vì vậy:

[xn]A(x) = an

Chẳng hạn, dãy 1, 1, 1, 1, ... có hàm sinh:

A(x) = 1 + x + x2 + x3 + ··· = 1 1 − x

2. Dịch chỉ số và công thức truy hồi

Nếu A(x)=Σn≥0anxn, thì nhân với x tạo ra:

xA(x) = a0x + a1x2 + a2x3 + ···

Nghĩa là hệ số được dịch sang phải một vị trí. Tương tự, x2A(x) dịch chúng hai vị trí. Chính cơ chế này cho phép các thành phần như an-1 và an-2 trong công thức truy hồi được chuyển thành phép nhân đại số.

3. Ví dụ: tìm hàm sinh của dãy Fibonacci

Dãy Fibonacci được xác định bởi F0=0, F1=1 và:

Fn = Fn-1 + Fn-2,   n ≥ 2

Đặt F(x)=Σn≥0Fnxn . Sau khi nhân công thức truy hồi với xn, lấy tổng từ n=2 và xử lý các điều kiện đầu, ta thu được:

F(x) − x = xF(x) + x2F(x)

Gom các thành phần chứa F(x) về một phía:

F(x)(1 − x − x2) = x

Do đó hàm sinh của dãy Fibonacci là:

F(x) = x 1 − x − x2

4. Minh họa bằng Python

Trong thực tế, ta thường chỉ cần một số hữu hạn hệ số đầu tiên. Đoạn mã sau nhân hai đa thức hữu hạn để mô phỏng phép nhân hai hàm sinh.

```
Python
def nhan_da_thuc(a, b, bac_toi_da):
ket_qua = [0] * (bac_toi_da + ';
      link.after(button);
      link.parentElement.classList.add('has-topic-menu');
      var entry = {button:button, panel:panel};
      triggers.push(entry);
      button.addEventListener('click', function(event){
        if(active === entry){ close(false); }else{ open(entry, event.detail === 0); }
      });
      button.addEventListener('keydown', function(event){
        if(event.key === 'ArrowDown'){ event.preventDefault(); open(entry, true); }
      });
      var current = false;
      panel.querySelectorAll('a[href]').forEach(function(item){
        if(selected && labelOf(item.href) === selected){ item.setAttribute('aria-current', 'page'); current = true; }
      });
      if(current){ link.parentElement.classList.add('has-current-topic'); }
      panel.querySelector('.topic-close').addEventListener('click', function(){ close(true); });
    });
    document.addEventListener('click', function(event){
      if(active && !directory.contains(event.target) && !triggers.some(function(item){ return item.button.contains(event.target); })){ close(false); }
    });
    document.addEventListener('focusin', function(event){
      if(active && !directory.contains(event.target) && !triggers.some(function(item){ return item.button.contains(event.target); })){ close(false); }
    });
    document.addEventListener('keydown', function(event){
      if(active && event.key === 'Escape'){ event.preventDefault(); close(true); }
    });
    directory.addEventListener('click', function(event){ if(event.target.closest('a[href]')){ close(false); } });
    window.addEventListener('resize', schedulePlace, {passive:true});
    window.addEventListener('scroll', schedulePlace, {passive:true});
    if(window.visualViewport){ window.visualViewport.addEventListener('resize', schedulePlace, {passive:true}); }
    window.addEventListener('pagehide', function(){ close(false); });
  });

  // Meta descriptions are server-rendered by Blogger/Search Description for deterministic crawling.

  onReady(function initContentProtection(){
    var article = document.querySelector('body.item-view .Blog .post-body[id^="post-body-"]');
    if(!article){ return; }

    var notice = document.createElement('div');
    notice.className = 'content-copy-notice';
    notice.setAttribute('role', 'status');
    notice.setAttribute('aria-live', 'polite');
    notice.setAttribute('aria-atomic', 'true');
    document.body.appendChild(notice);
    var noticeTimer;
    var editableSelector = 'input,textarea,select,[contenteditable]:not([contenteditable="false"])';
    var reusableSelector = 'pre,code,kbd,samp,math,.katex,.MathJax,mjx-container';

    function asElement(node){
      return node && (node.nodeType === 1 ? node : node.parentElement);
    }

    function isEditable(node){
      var element = asElement(node);
      return !!(element && (element.isContentEditable || element.closest(editableSelector)));
    }

    function isReusableRange(range){
      var element = asElement(range.startContainer);
      // A whole code block can start at the PRE element or inside nested spans.
      while(element && article.contains(element)){
        if(element.matches(reusableSelector) && element.contains(range.endContainer)){
          return true;
        }
        element = element.parentElement;
      }
      // selectNode(code) sets the range endpoints on its parent.
      if(range.startContainer === range.endContainer &&
          range.endOffset === range.startOffset + 1){
        var child = range.startContainer.childNodes[range.startOffset];
        return !!(child && child.nodeType === 1 && child.matches(reusableSelector) && article.contains(child));
      }
      return false;
    }

    function hasProtectedText(event){
      if(event.defaultPrevented || isEditable(event.target) || isEditable(document.activeElement)){
        return false;
      }
      var selection = window.getSelection();
      if(!selection || selection.isCollapsed || !selection.toString().trim()){ return false; }
      for(var i = 0; i < selection.rangeCount; i++){
        var range = selection.getRangeAt(i);
        if(range.intersectsNode(article) && !isReusableRange(range)){
          return true;
        }
      }
      return false;
    }

    function restrictTextCopy(event){
      if(!hasProtectedText(event)){ return; }
      event.preventDefault();
      clearTimeout(noticeTimer);
      notice.textContent = 'Nội dung bài viết được bảo lưu quyền. Bạn vẫn có thể sao chép code hoặc chia sẻ liên kết bài viết.';
      noticeTimer = setTimeout(function(){ notice.textContent = ''; }, 4500);
    }

    // Do not block selection, keyboard shortcuts, context menus, links or comments.
    document.addEventListener('copy', restrictTextCopy);
    document.addEventListener('cut', restrictTextCopy);
    article.addEventListener('dragstart', function(event){
      var element = asElement(event.target);
      if(element && element.closest('a,img')){ return; }
      restrictTextCopy(event);
    });
  });

  var postThumbnails = (function(){
    var cache = new Map();
    var resolved = new Map();
    var storageKey = 'cs-note-thumbnails-v32';
    var persistTimer;
    var queued = new WeakSet();
    var queue = [];
    var active = 0;
    var observer;
    var svgNS = 'http://www.w3.org/2000/svg';
    var svgTags = new Set('svg g defs rect path text tspan circle ellipse line polyline polygon linearGradient radialGradient stop clipPath mask marker title desc use'.split(' '));
    var svgAttributes = new Set('id viewBox x y x1 y1 x2 y2 cx cy r rx ry width height d points dx dy transform fill fill-opacity fill-rule stroke stroke-width stroke-opacity stroke-linecap stroke-linejoin stroke-dasharray opacity font-family font-size font-weight font-style text-anchor dominant-baseline alignment-baseline letter-spacing offset stop-color stop-opacity gradientUnits gradientTransform spreadMethod markerWidth markerHeight refX refY orient markerUnits marker-start marker-mid marker-end clip-path mask preserveAspectRatio'.split(' '));
    var shapeTags = new Set('rect path circle ellipse line polyline polygon use'.split(' '));

    // Persist only small derived previews, never full article HTML.
    try{
      var stored = localStorage.getItem(storageKey);
      if(stored && stored.length <= 800000){
        JSON.parse(stored).forEach(function(item){
          if(/^\d+$/.test(item.id) && item.expires > Date.now() &&
             (item.result === null || (item.result && typeof item.result.url === 'string' &&
             /^(https?:\/\/|data:image\/(svg\+xml;charset=utf-8,|(?:png|jpeg|webp|gif);base64,))/i.test(item.result.url)))){
            resolved.set(item.id, item);
          }
        });
      }
    }catch(error){ /* Storage is optional, including in private browsing. */ }
    function remember(id, result, updated){
      resolved.delete(id);
      resolved.set(id, {id:id, result:result, updated:updated || '', expires:Date.now() + (result ? 86400000 : 3600000)});
      while(resolved.size > 60){ resolved.delete(resolved.keys().next().value); }
      if(persistTimer){ return; }
      persistTimer = setTimeout(function(){
        persistTimer = null;
        var items = Array.from(resolved.values()).filter(function(item){ return !item.result || item.result.url.length < 64000; });
        var json = JSON.stringify(items);
        while(json.length > 750000 && items.length){ items.shift(); json = JSON.stringify(items); }
        try{ localStorage.setItem(storageKey, json); }catch(error){ /* Keep the in-memory cache if storage is full. */ }
      }, 250);
    }
    function cached(id, updated){
      var item = resolved.get(id);
      if(item && item.expires > Date.now() && (!updated || item.updated === updated)){ return item; }
      return null;
    }
    function preview(entry, useContent){
      var id = (entry.id && entry.id.$t || '').split('.post-').pop();
      var updated = entry.updated && entry.updated.$t || '';
      var existing = cached(id, updated);
      if(existing && !useContent){ return existing.result; }
      var alternate = (entry.link || []).find(function(link){ return link.rel === 'alternate' && link.type === 'text/html'; });
      var base = alternate ? alternate.href : window.location.href;
      var thumbnail = !useContent && entry.media$thumbnail && imageUrl(entry.media$thumbnail.url, base);
      var result = thumbnail ? {url:thumbnail, diagram:false} : extract(entry.content && entry.content.$t, base, entry.title && entry.title.$t);
      remember(id, result, updated);
      return result;
    }
    function attach(target, result, priority, fallback){
      if(!result){ return; }
      var img = document.createElement('img');
      img.className = 'cs-derived-thumbnail cs-preview-image';
      img.alt = '';
      img.width = 640;
      img.height = 360;
      img.decoding = 'async';
      img.loading = priority === 'high' ? 'eager' : 'lazy';
      img.fetchPriority = priority === 'high' ? 'high' : 'auto';
      if(result.diagram){ target.classList.add('cs-diagram-cover'); }
      img.onload = function(){
        target.classList.remove('thumbnail-empty');
        // Wide diagrams / tall images are shown whole instead of being cropped and enlarged.
        var ratio = img.naturalWidth / (img.naturalHeight || 1);
        if(!result.diagram && (ratio < 1.45 || ratio > 2.1)){ target.classList.add('cs-diagram-cover'); }
        img.classList.add('is-loaded');
        img.onload = null;
        img.onerror = null;
      };
      img.onerror = function(){
        img.remove();
        target.classList.add('thumbnail-empty');
        target.classList.remove('cs-diagram-cover');
        resolved.delete(target.getAttribute('data-post-id'));
        if(fallback){
          var alternative = fallback();
          if(alternative && alternative.url !== result.url){ attach(target, alternative, priority); }
        }
      };
      var src = result.url;
      if(/^https?:/.test(src)){
        var url = new URL(src);
        if(/(^|\.)(blogspot\.com|googleusercontent\.com|ggpht\.com)$/.test(url.hostname)){
          // Request the whole image at high resolution (no server-side crop); CSS decides how to fit it.
          var resized = url.pathname.replace(/\/(?:s|w|h)\d+(?:-[a-z0-9]+)*\//i, '/s1280/');
          if(resized === url.pathname){ resized = url.pathname.replace(/=(?:s|w|h)\d+(?:-[a-z0-9]+)*$/i, '=s1280'); }
          url.pathname = resized;
        }
        src = url.href;
      }
      img.src = src;
      target.appendChild(img);
    }

    function imageUrl(value, base){
      if(!value){ return ''; }
      if(/^data:image\/(png|jpeg|webp|gif);base64,/i.test(value)){ return value; }
      try{
        var url = new URL(value, base);
        return /^https?:$/.test(url.protocol) ? url.href : '';
      }catch(error){ return ''; }
    }
    function safeSvg(source){
      var viewBox = (source.getAttribute('viewBox') || source.getAttribute('viewbox') || '').trim();
      var box = viewBox ? viewBox.split(/[\s,]+/).map(Number) : [];
      if(box.length !== 4 || !box.every(Number.isFinite) || box[2] <= 0 || box[3] <= 0){
        var width = parseFloat(source.getAttribute('width')) || 0;
        var height = parseFloat(source.getAttribute('height')) || 0;
        if(width < 80 || height < 60){ return ''; }
        box = [0, 0, width, height];
      }
      if(box[2] < 80 || box[3] < 60 || box[2] > 5000 || box[3] > 5000){ return ''; }
      if(source.querySelectorAll('*').length > 2500){ return ''; }
      var root = document.createElementNS(svgNS, 'svg');
      root.setAttribute('viewBox', box.join(' '));
      root.setAttribute('width', String(box[2]));
      root.setAttribute('height', String(box[3]));
      root.setAttribute('fill', '#111827');
      root.setAttribute('font-family', 'Arial, sans-serif');
      function copyAttributes(from, to){
        Array.prototype.forEach.call(from.attributes, function(attr){
          if(svgAttributes.has(attr.name) && (!/url\s*\(/i.test(attr.value) || /^url\(\s*['"]?#[\w:.-]+['"]?\s*\)$/.test(attr.value))){
            to.setAttribute(attr.name, attr.value);
          }
          if((attr.name === 'href' || attr.name === 'xlink:href') && /^#[\w:.-]+$/.test(attr.value)){
            to.setAttribute('href', attr.value);
          }
        });
        // Retain presentation styles, never scripts, external resources or page-wide CSS.
        Array.prototype.forEach.call(from.style || [], function(name){
          var value = from.style.getPropertyValue(name);
          if(svgAttributes.has(name) && !/url\s*\(|var\s*\(/i.test(value)){ to.setAttribute(name, value); }
        });
      }
      function copyChildren(from, to){
        Array.prototype.forEach.call(from.childNodes, function(child){
          if(child.nodeType === 3){ to.appendChild(document.createTextNode(child.nodeValue)); return; }
          if(child.nodeType !== 1 || !svgTags.has(child.localName)){ return; }
          var node = document.createElementNS(svgNS, child.localName);
          copyAttributes(child, node);
          to.appendChild(node);
          // Blogger's editor can nest text under a shape when self-closing tags are lost.
          copyChildren(child, shapeTags.has(child.localName) ? to : node);
        });
      }
      copyAttributes(source, root);
      root.setAttribute('viewBox', box.join(' '));
      root.setAttribute('width', String(box[2]));
      root.setAttribute('height', String(box[3]));
      copyChildren(source, root);
      return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(new XMLSerializer().serializeToString(root));
    }
    function firstFromSrcset(value){
      if(!value){ return ''; }
      return (value.split(',')[0] || '').trim().split(/\s+/)[0] || '';
    }
    function backgroundImage(node, base){
      var style = node.getAttribute('style') || '';
      // Prefer inline CSS images; external stylesheet-generated images cannot be read from a feed.
      var match = style.match(/background(?:-image)?\s*:[^;]*?url\(\s*(['"]?)(.*?)\1\s*\)/i);
      return match ? imageUrl(match[2].trim(), base) : '';
    }
    function diagramFallback(title){
      // A safe labeled preview is better than a blank card when the source is a CSS/Canvas chart.
      // It is NOT a screenshot/reproduction of the original HTML diagram.
      var svg = document.createElementNS(svgNS, 'svg');
      svg.setAttribute('xmlns', svgNS);
      svg.setAttribute('viewBox', '0 0 640 360');
      var rect = document.createElementNS(svgNS, 'rect');
      rect.setAttribute('width', '640'); rect.setAttribute('height', '360'); rect.setAttribute('fill', '#e9f1ff'); svg.appendChild(rect);
      var line = document.createElementNS(svgNS, 'rect');
      line.setAttribute('x', '42'); line.setAttribute('y', '48'); line.setAttribute('width', '82');
      line.setAttribute('height', '5'); line.setAttribute('rx', '2'); line.setAttribute('fill', '#2563eb'); svg.appendChild(line);
      var label = document.createElementNS(svgNS, 'text');
      label.setAttribute('x', '42'); label.setAttribute('y', '91'); label.setAttribute('fill', '#475569');
      label.setAttribute('font-family', 'Arial, sans-serif'); label.setAttribute('font-size', '17');
      label.textContent = 'NỘI DUNG MINH HỌA HTML'; svg.appendChild(label);
      var words = String(title || 'Ghi chú khoa học máy tính').replace(/\s+/g, ' ').trim().split(' ');
      var rows = [], row = '';
      words.forEach(function(word){
        if((row + ' ' + word).trim().length > 29 && row){ rows.push(row); row = word; }
        else{ row = (row + ' ' + word).trim(); }
      });
      if(row){ rows.push(row); }
      rows.slice(0, 3).forEach(function(value, index){
        var text = document.createElementNS(svgNS, 'text');
        text.setAttribute('x', '42'); text.setAttribute('y', String(151 + 55 * index));
        text.setAttribute('fill', '#0f172a'); text.setAttribute('font-family', 'Arial, sans-serif');
        text.setAttribute('font-size', '30'); text.setAttribute('font-weight', '700');
        text.textContent = value; svg.appendChild(text);
      });
      return {url:'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(new XMLSerializer().serializeToString(svg)), diagram:true, generated:true};
    }
    function extract(html, base, title){
      var template = document.createElement('template');
      template.innerHTML = html || '';
      var visuals = template.content.querySelectorAll('img, picture source, svg, [style*="background"]');
      var hasUnrenderedIllustration = !!template.content.querySelector('canvas, [class*="diagram"], [class*="chart"], [class*="flowchart"], [class*="infographic"], [class*="visual"], [class*="illustration"], [style*="linear-gradient"], [style*="grid-template"]');
      for(var i = 0; i < visuals.length; i++){
        var visual = visuals[i];
        if(visual.closest('svg') && visual.localName !== 'svg'){ continue; }
        if(visual.localName === 'svg'){
          var svg = safeSvg(visual);
          if(svg){ return {url:svg, diagram:true}; }
          hasUnrenderedIllustration = true;
        }else{
          if(visual.localName === 'source' && !visual.closest('picture')){ continue; }
          var width = parseFloat(visual.getAttribute('width'));
          var height = parseFloat(visual.getAttribute('height'));
          if((width > 0 && width < 80) || (height > 0 && height < 60)){ continue; }
          var possibilities = [
            visual.getAttribute('data-src'), visual.getAttribute('data-original'),
            visual.getAttribute('data-lazy-src'), firstFromSrcset(visual.getAttribute('data-srcset')),
            firstFromSrcset(visual.getAttribute('srcset')), visual.getAttribute('src'),
            backgroundImage(visual, base)
          ];
          for(var j = 0; j < possibilities.length; j++){
            var src = imageUrl(possibilities[j], base);
            if(src && !/^data:image\/(?:gif|png);base64,R0lGOD|^data:image\/svg\+xml,?$/i.test(src)){
              return {url:src, diagram:false};
            }
          }
        }
      }
      return hasUnrenderedIllustration ? diagramFallback(title) : null;
    }
    async function getThumbnail(id, base){
      var existing = cached(id);
      if(existing){ return existing.result; }
      if(cache.has(id)){ return cache.get(id); }
      var controller = new AbortController();
      var timeout = setTimeout(function(){ controller.abort(); }, 10000);
      var pending = fetch('/feeds/posts/default/' + encodeURIComponent(id) + '?alt=json', {
        credentials:'same-origin', signal:controller.signal
      }).then(function(response){
        if(!response.ok){ throw new Error('Thumbnail feed unavailable'); }
        return response.json();
      }).then(function(data){
        var entry = data.entry || (data.feed && data.feed.entry && data.feed.entry[0]);
        var result = extract(entry && entry.content && entry.content.$t, base, entry && entry.title && entry.title.$t);
        remember(id, result, entry && entry.updated && entry.updated.$t);
        return result;
      }).catch(function(){ return null; }).finally(function(){ clearTimeout(timeout); cache.delete(id); });
      cache.set(id, pending);
      if(cache.size > 60){ cache.delete(cache.keys().next().value); }
      return pending;
    }
    function drain(){
      while(active < 3 && queue.length){
        var cover = queue.shift();
        if(!cover.isConnected){ continue; }
        active++;
        (function(target){
          getThumbnail(target.getAttribute('data-post-id'), target.href).then(function(result){
            if(!result || !target.isConnected){ return; }
            attach(target, result, 'visible');
          }).finally(function(){ active--; drain(); });
        })(cover);
      }
    }
    function enqueue(cover){ queue.push(cover); drain(); }
    if('IntersectionObserver' in window){
      observer = new IntersectionObserver(function(items){
        items.forEach(function(item){
          if(item.isIntersecting){ observer.unobserve(item.target); enqueue(item.target); }
        });
      }, {rootMargin:'600px'});
    }
    return {preview:preview, attach:attach, release:function(root){
      if(observer){
        root.querySelectorAll('a[data-post-id]').forEach(function(cover){ observer.unobserve(cover); });
      }
    }, observe:function(root){
      if(!window.fetch || !window.AbortController){ return; }
      var selector = 'a.thumbnail-empty[data-post-id]';
      var covers = Array.prototype.slice.call(root.querySelectorAll(selector));
      if(root.matches && root.matches(selector)){ covers.unshift(root); }
      covers.forEach(function(cover){
        if(queued.has(cover) || !/^\d+$/.test(cover.getAttribute('data-post-id'))){ return; }
        queued.add(cover);
        var existing = cached(cover.getAttribute('data-post-id'));
        if(existing){ attach(cover, existing.result, 'visible'); return; }
        if(observer){ observer.observe(cover); }else{ enqueue(cover); }
      });
    }};
  })();
  onReady(function initNativePostThumbnails(){
    // The native first-page HTML is kept for SEO and immediately usable links.
    // Missing covers in that HTML must be observed on home and label pages too.
    postThumbnails.observe(document);
  });

  // Monetag vignette: one request after roughly 2 engaged article reads, not on each page.
  // The Monetag platform retains final control over impression delivery and its own caps.
  onReady(function initReadingBasedMonetag(){
    var marker = document.getElementById('cs-ad-end');
    var article = document.querySelector('body.item-view .post-body.entry-content');
    if(!marker || !article || !marker.getAttribute('data-src')){ return; }
    var MIN_ENGAGED_MS = 8000;
    var MIN_ARTICLE_PROGRESS = 0.52;
    var READS_PER_REQUEST = 2;
    var MIN_REQUEST_GAP_MS = 150000;
    var READ_DEDUP_MS = 45 * 60000;
    var STORAGE_KEY = 'cs_monetag_reading_v3';
    var id = window.location.pathname.replace(/\/$/, '') || '/';
    var current = Date.now();
    var state = {readIds:[], recent:[], lastRequest:0};
    var requested = false, counted = false, pending = false;
    var visibleSince = document.visibilityState !== 'hidden' ? current : 0;
    var activeTimeMs = 0, lastCheck = 0, timer = null;
    try{
      var stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
      if(stored && Array.isArray(stored.readIds) && Array.isArray(stored.recent)){
        state = {readIds:stored.readIds.filter(function(v){ return typeof v === 'string'; }).slice(-6),
          recent:stored.recent.filter(function(v){ return v && typeof v.id === 'string' && Number.isFinite(v.at) && current - v.at >= 0 && current - v.at < READ_DEDUP_MS; }).slice(-20),
          lastRequest:Number(stored.lastRequest) || 0};
      }
    }catch(e){ /* Storage is optional; a private browser may use per-page behavior. */ }
    function save(){ try{ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }catch(e){} }
    function readingProgress(){
      var rect = article.getBoundingClientRect();
      var height = Math.max(article.offsetHeight, rect.height, 1);
      return Math.max(0, Math.min(1, (window.innerHeight - rect.top) / height));
    }
    function engagedTime(){ return activeTimeMs + (visibleSince ? Date.now() - visibleSince : 0); }
    function requestAd(now){
      if(requested){ return; }
      requested = true;
      state.readIds = [];
      state.lastRequest = now;
      save();
      cleanup();
      var tag = document.createElement('script');
      tag.setAttribute('data-zone', marker.getAttribute('data-zone'));
      tag.setAttribute('data-cfasync', 'false');
      tag.async = true;
      tag.src = marker.getAttribute('data-src');
      (document.body || document.documentElement).appendChild(tag);
    }
    function check(){
      if(requested || document.visibilityState === 'hidden'){ return; }
      var now = Date.now();
      if(engagedTime() < MIN_ENGAGED_MS || readingProgress() < MIN_ARTICLE_PROGRESS){ return; }
      if(!counted){
        counted = true;
        if(!state.recent.some(function(v){ return v.id === id; })){
          state.readIds.push(id);
          state.readIds = state.readIds.slice(-6);
          state.recent.push({id:id, at:now});
          state.recent = state.recent.slice(-20);
          save();
        }
      }
      if(state.readIds.length >= READS_PER_REQUEST){
        if(now - state.lastRequest >= MIN_REQUEST_GAP_MS){ requestAd(now); }
        else if(!pending){
          pending = true;
          timer = setTimeout(function(){ pending = false; check(); }, Math.min(60000, Math.max(1000, MIN_REQUEST_GAP_MS - (now - state.lastRequest))));
        }
      }
    }
    function onScroll(){ var now = Date.now(); if(now - lastCheck >= 200){ lastCheck = now; check(); } }
    function onVisibility(){
      if(document.visibilityState === 'hidden'){
        if(visibleSince){ activeTimeMs += Date.now() - visibleSince; visibleSince = 0; }
      }else{ visibleSince = Date.now(); check(); }
    }
    function cleanup(){
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', check);
      document.removeEventListener('visibilitychange', onVisibility);
      window.removeEventListener('pageshow', check);
      if(timer){ clearTimeout(timer); timer = null; }
    }
    window.addEventListener('scroll', onScroll, {passive:true});
    window.addEventListener('resize', check, {passive:true});
    document.addEventListener('visibilitychange', onVisibility);
    window.addEventListener('pageshow', check);
    timer = setTimeout(function(){ timer = null; check(); }, MIN_ENGAGED_MS + 100);
    check();
  });

  // Show dates on the Most Viewed cards as dd/mm/yyyy, matching the catalog cards.
  onReady(function formatPopularDates(){
    document.querySelectorAll('.homepage-popular time, .homepage-popular abbr.published').forEach(function(node){
      var date = new Date(node.getAttribute('datetime') || node.getAttribute('title'));
      if(Number.isFinite(date.getTime())){
        node.textContent = date.toLocaleDateString('vi-VN', {day:'2-digit', month:'2-digit', year:'numeric'});
      }
    });
  });

  onReady(function initHeaderSearch(){
    var header = document.querySelector('.centered-top-container .centered-top');
    var search = header && header.querySelector('.search');
    var toggle = search && search.querySelector('.search-expand');
    var input = search && search.querySelector('input[name="q"]');
    if(!toggle || !input){ return; }

    function setOpen(open, restoreFocus){
      search.classList.toggle('focused', open);
      header.classList.toggle('search-focused', open);
      toggle.setAttribute('aria-expanded', String(open));
      if(open){
        document.body.classList.remove('header-hidden');
        input.focus({preventScroll:true});
      }else if(restoreFocus){
        toggle.focus({preventScroll:true});
      }
    }
    // One toggle handler prevents Rockpool and the custom header from toggling twice.
    document.addEventListener('click', function(event){
      if(toggle.contains(event.target)){
        event.preventDefault();
        event.stopImmediatePropagation();
        setOpen(!search.classList.contains('focused'), false);
      }else if(!search.contains(event.target)){
        setOpen(false, false);
      }
    }, true);
    search.addEventListener('keydown', function(event){
      if(event.key === 'Escape'){
        event.preventDefault();
        event.stopPropagation();
        setOpen(false, true);
      }
    });
    new MutationObserver(function(){
      toggle.setAttribute('aria-expanded', String(search.classList.contains('focused')));
    }).observe(search, {attributes:true, attributeFilter:['class']});
  });

  // Progressive catalog enhancement: native HTML remains visible/indexable while
  // missing cards are filled from the feed. Blogger sometimes server-renders only
  // three posts because of its auto-pagination or large HTML-heavy articles.
  onReady(function fillShortBloggerCatalog(){
    var url = new URL(window.location.href);
    if(url.searchParams.has('cs-page') || url.searchParams.has('updated-max') ||
       url.searchParams.has('updated-min') || url.searchParams.has('start-index') ||
       url.searchParams.has('q') || !window.fetch || !window.AbortController ||
       !document.body.classList.contains('feed-view')){ return; }
    var labelMatch = url.pathname.match(/^\/search\/label\/([^/]+)\/?$/);
    if(!labelMatch && url.pathname !== '/' && url.pathname !== '/search'){ return; }
    var widget = document.querySelector('.widget.Blog');
    var grid = widget && widget.querySelector('.blog-posts');
    var pager = widget && widget.querySelector('#blog-pager');
    if(!grid || !pager){ return; }
    var label = '';
    try{ label = labelMatch ? decodeURIComponent(labelMatch[1]) : ''; }catch(e){ return; }
    var feedPath = '/feeds/posts/default' + (label ? '/-/' + encodeURIComponent(label) : '');
    var PAGE_SIZE = 12;
    var seen = new Set();
    function normalize(href){
      try{
        var a = new URL(href, window.location.href);
        return a.origin === url.origin ? a.pathname.replace(/\/$/, '') : '';
      }catch(e){ return ''; }
    }
    grid.querySelectorAll('.post-outer-container .post-title a[href]').forEach(function(link){
      var key = normalize(link.href); if(key){ seen.add(key); }
    });
    var nativeCount = seen.size;
    // Do not load a duplicate feed when Blogger has already rendered a full page.
    if(nativeCount >= PAGE_SIZE){ return; }
    var nextIndex = PAGE_SIZE + 1, total = 0, running = false;
    var control = document.createElement('button');
    control.type = 'button'; control.className = 'cs-load-more';
    control.textContent = 'Xem thêm bài viết';
    control.setAttribute('aria-label', 'Tải thêm 12 bài viết');
    var status = document.createElement('span');
    status.className = 'cs-feed-status'; status.setAttribute('role', 'status');
    pager.prepend(status); pager.prepend(control);
    control.hidden = true;
    var nativeMore = pager.querySelector('a.blog-pager-older-link');
    if(nativeMore){ nativeMore.setAttribute('aria-label', 'Xem trang bài đăng cũ trên Blogger'); }
    function plainText(html){
      var temp = document.createElement('template'); temp.innerHTML = html || '';
      temp.content.querySelectorAll('script,style,noscript,iframe,svg,object').forEach(function(e){ e.remove(); });
      return (temp.content.textContent || '').replace(/\s+/g, ' ').trim();
    }
    function createCard(entry, index){
      var link = (entry.link || []).find(function(a){ return a.rel === 'alternate' && a.type === 'text/html'; });
      var path = link && normalize(link.href);
      if(!path || seen.has(path)){ return null; }
      var title = plainText(entry.title && entry.title.$t) || 'Bài viết';
      var article = document.createElement('article'); article.className = 'post-outer-container cs-catalog-card';
      var outer = document.createElement('div'); outer.className = 'post-outer';
      var cover = document.createElement('a'); cover.className = 'snippet-thumbnail thumbnail-empty';
      cover.href = link.href; cover.setAttribute('aria-label', title);
      cover.setAttribute('data-post-id', (entry.id && entry.id.$t || '').split('.post-').pop());
      var thumb = postThumbnails.preview(entry);
      if(thumb){ postThumbnails.attach(cover, thumb, index < 3 ? 'visible' : false, function(){ return postThumbnails.preview(entry, true); }); }
      var content = document.createElement('div'); content.className = 'post-content';
      var heading = document.createElement('h3'); heading.className = 'post-title entry-title';
      var titleLink = document.createElement('a'); titleLink.href = link.href; titleLink.textContent = title;
      heading.appendChild(titleLink); content.appendChild(heading);
      var published = entry.published && new Date(entry.published.$t);
      if(published && Number.isFinite(published.getTime())){
        var meta = document.createElement('div'); meta.className = 'post-header-container';
        var time = document.createElement('time'); time.dateTime = published.toISOString();
        time.textContent = published.toLocaleDateString('vi-VN', {day:'2-digit',month:'2-digit',year:'numeric'});
        meta.appendChild(time); content.appendChild(meta);
      }
      var excerpt = plainText((entry.summary || entry.content || {}).$t);
      if(excerpt){
        var p = document.createElement('p'); p.className = 'post-body';
        p.textContent = excerpt.length > 210 ? excerpt.slice(0, 207) + '…' : excerpt;
        content.appendChild(p);
      }
      outer.append(cover, content); article.appendChild(outer);
      seen.add(path);
      return article;
    }
    var controller = new AbortController();
    async function fetchChunk(start, count){
      var feed = new URL(feedPath, url.origin);
      feed.searchParams.set('alt', 'json');
      feed.searchParams.set('orderby', 'published');
      feed.searchParams.set('start-index', String(start));
      feed.searchParams.set('max-results', String(count));
      var resp = await fetch(feed.href, {credentials:'same-origin',signal:controller.signal});
      if(!resp.ok){ throw new Error('Blogger feed unavailable'); }
      var json = await resp.json(), data = json.feed;
      if(!data){ throw new Error('Invalid Blogger feed'); }
      var countTotal = Number(data.openSearch$totalResults && data.openSearch$totalResults.$t);
      if(!Number.isSafeInteger(countTotal) || countTotal < 0){ throw new Error('Invalid feed total'); }
      return {total:countTotal, entries:Array.isArray(data.entry) ? data.entry : []};
    }
    async function loadBatch(start, length){
      var items = [], totalCount = null;
      // Some Blogger feeds return fewer items than requested; retry with offsets.
      for(var attempt = 0; attempt < length && items.length < length; attempt++){
        var batch = await fetchChunk(start + items.length, length - items.length);
        if(totalCount === null){ totalCount = batch.total; }
        if(!batch.entries.length || start + items.length > totalCount){ break; }
        var previous = items.length;
        items.push.apply(items, batch.entries);
        if(items.length === previous){ break; }
      }
      return {total:totalCount || 0, entries:items.slice(0,length)};
    }
    async function appendBatch(start, count){
      if(running){ return; } running = true;
      control.disabled = true; status.textContent = 'Đang tải bài viết…';
      try{
        var data = await loadBatch(start, count);
        total = data.total;
        var fragment = document.createDocumentFragment(), added = 0;
        data.entries.forEach(function(entry, index){
          var card = createCard(entry, index);
          if(card){ fragment.appendChild(card); added++; }
        });
        if(added){
          grid.appendChild(fragment); postThumbnails.observe(grid);
          grid.setAttribute('data-page-size', String(PAGE_SIZE));
        }
        nextIndex = start + data.entries.length;
        control.hidden = nextIndex > total || !data.entries.length;
        status.textContent = '';
        // Retain the native Older Posts link: it provides a server-rendered crawl path.
      }catch(e){
        status.textContent = 'Chưa thể tải thêm bài; có thể dùng liên kết trang cũ bên dưới.';
        control.hidden = false;
      }finally{ running = false; control.disabled = false; }
    }
    control.addEventListener('click', function(){ appendBatch(nextIndex, PAGE_SIZE); });
    appendBatch(1, PAGE_SIZE);
    window.addEventListener('pagehide', function(){ controller.abort(); }, {once:true});
  });

  // Catalog pagination uses feed offsets; native Blogger pages can be shortened automatically.
  onReady(function initCatalogPagination(){
    var locationUrl = new URL(window.location.href);
    // Keep Blogger's server-rendered first page intact. Replacing the grid while a reader
    // is clicking can cancel the first click and make article titles feel frozen.
    // Client-side catalog loading is retained only for explicit legacy cs-page URLs.
    if(!locationUrl.searchParams.has('cs-page')){ return; }
    var labelMatch = locationUrl.pathname.match(/^\/search\/label\/([^/]+)\/?$/);
    var homePath = locationUrl.pathname === '/' || locationUrl.pathname === '/search';
    if(!document.body.classList.contains('feed-view') || (!labelMatch && !homePath) ||
       locationUrl.searchParams.has('q') || !window.fetch || !window.AbortController){ return; }

    var widget = document.querySelector('.widget.Blog');
    if(!widget){ return; }
    var grid = widget.querySelector('.blog-posts');
    var pager = widget.querySelector('#blog-pager');
    if(!grid || !pager){ return; }
    var label;
    try{ label = labelMatch ? decodeURIComponent(labelMatch[1]) : ''; }catch(error){ return; }
    // Keep a page's offsets stable on resize; CSS reflows its existing cards.
    var PAGE_SIZE = 12;
    grid.setAttribute('data-page-size', String(PAGE_SIZE));
    var basePath = labelMatch ? '/search/label/' + encodeURIComponent(label) : '/';
    var feedPath = '/feeds/posts/default' + (label ? '/-/' + encodeURIComponent(label) : '');
    var notice = document.createElement('div');
    notice.className = 'catalog-notice';
    notice.setAttribute('role', 'status');
    grid.before(notice);
    grid.id = 'cs-post-catalog';
    grid.setAttribute('tabindex', '-1');
    var requestId = 0;
    var activeController;
    var displayedPage = null;
    var displayedSnapshot = null;
    var snapshotKey = 'cs-note-catalog-snapshot:' + basePath;

    function element(tag, className, value){
      var node = document.createElement(tag);
      if(className){ node.className = className; }
      if(value !== undefined){ node.textContent = value; }
      return node;
    }
    function readPage(url){
      var raw = url.searchParams.get('cs-page') || '1';
      var page = Number(raw);
      if(!/^\d+$/.test(raw) || !Number.isSafeInteger(page) || page < 1){ return 1; }
      var previousSize = Number(url.searchParams.get('cs-size')) === 9 ? 9 : 12;
      return Math.floor(((page - 1) * previousSize) / PAGE_SIZE) + 1;
    }
    function readSnapshot(url, navigation){
      var value = url.searchParams.get('cs-before');
      if(!value && navigation === 'push'){ value = displayedSnapshot; }
      if(!value && (navigation === 'pop' || url.searchParams.has('cs-page'))){
        var state = history.state;
        if(state && state.csBasePath === basePath){ value = state.csSnapshot; }
        if(!value){
          try{
            var saved = JSON.parse(sessionStorage.getItem(snapshotKey) || 'null');
            if(saved && Date.now() - saved.savedAt >= 0 && Date.now() - saved.savedAt < 3600000){ value = saved.snapshot; }
          }catch(error){ /* A fresh snapshot is safe when session storage is unavailable. */ }
        }
      }
      var date = value && new Date(value);
      return date && Number.isFinite(date.getTime()) && date.getTime() <= Date.now()
        ? date.toISOString() : new Date().toISOString();
    }
    function pageUrl(page){
      var url = new URL(basePath, locationUrl.origin);
      if(locationUrl.searchParams.has('m')){ url.searchParams.set('m', locationUrl.searchParams.get('m')); }
      if(page > 1){
        url.searchParams.set('max-results', String(PAGE_SIZE));
        url.searchParams.set('cs-page', String(page));
        url.searchParams.set('cs-size', String(PAGE_SIZE));
      }
      url.hash = 'cs-post-catalog';
      return url;
    }
    function safeUrl(value){
      try{
        var url = new URL(value, locationUrl.origin);
        return /^https?:$/.test(url.protocol) ? url.href : '';
      }catch(error){ return ''; }
    }
    function plainText(value){
      var template = document.createElement('template');
      template.innerHTML = value || '';
      template.content.querySelectorAll('script, style, iframe, object, svg, noscript').forEach(function(node){ node.remove(); });
      return template.content.textContent.replace(/\s+/g, ' ').trim();
    }
    function card(entry, index){
      var alternate = (entry.link || []).find(function(link){ return link.rel === 'alternate' && link.type === 'text/html'; });
      var href = alternate && safeUrl(alternate.href);
      if(!href){ throw new Error('Missing article URL'); }
      var title = plainText(entry.title && entry.title.$t) || 'Bài viết';
      var article = element('article', 'post-outer-container cs-catalog-card');
      var outer = element('div', 'post-outer');
      var thumbnail = postThumbnails.preview(entry);
      var cover = element('a', 'snippet-thumbnail' + (thumbnail ? '' : ' thumbnail-empty'));
      cover.href = href;
      cover.setAttribute('aria-label', title);
      cover.setAttribute('data-post-id', (entry.id && entry.id.$t || '').split('.post-').pop());
      if(thumbnail){
        postThumbnails.attach(cover, thumbnail, index === 0 ? 'high' : index < 4 ? 'visible' : false, function(){
          return postThumbnails.preview(entry, true);
        });
      }
      var content = element('div', 'post-content');
      var titleContainer = element('div', 'post-title-container');
      var heading = element('h3', 'post-title entry-title');
      var link = element('a', '', title);
      link.href = href;
      heading.appendChild(link);
      titleContainer.appendChild(heading);
      content.appendChild(titleContainer);
      var published = entry.published && new Date(entry.published.$t);
      if(published && Number.isFinite(published.getTime())){
        var metadata = element('div', 'post-header-container');
        var time = element('time', '', published.toLocaleDateString('vi-VN', {day:'2-digit', month:'2-digit', year:'numeric'}));
        time.dateTime = published.toISOString();
        metadata.appendChild(time);
        content.appendChild(metadata);
      }
      var excerpt = plainText((entry.summary || entry.content || {}).$t);
      if(excerpt){ content.appendChild(element('p', 'post-body', excerpt.length > 240 ? excerpt.slice(0, 237) + '...' : excerpt)); }
      outer.append(cover, content);
      article.appendChild(outer);
      return article;
    }
    async function readFeed(start, count, snapshot, signal){
      var url = new URL(feedPath, locationUrl.origin);
      url.searchParams.set('alt', 'json');
      url.searchParams.set('orderby', 'published');
      url.searchParams.set('start-index', String(start));
      url.searchParams.set('max-results', String(count));
      url.searchParams.set('published-max', snapshot);
      var response = await fetch(url.href, {credentials:'same-origin', signal:signal});
      if(!response.ok){ throw new Error('Feed unavailable'); }
      var data = await response.json();
      var feed = data.feed;
      var rawTotal = feed && feed.openSearch$totalResults && feed.openSearch$totalResults.$t;
      var total = Number(rawTotal);
      if(rawTotal === undefined || !Number.isSafeInteger(total) || total < 0 ||
         (feed.entry !== undefined && !Array.isArray(feed.entry))){ throw new Error('Invalid feed'); }
      var rawStart = feed.openSearch$startIndex && Number(feed.openSearch$startIndex.$t);
      if(rawStart && rawStart !== start && (feed.entry || []).length){ throw new Error('Feed offset ignored'); }
      return {total:total, entries:feed.entry || []};
    }
    async function readPageData(page, snapshot, signal){
      var start = (page - 1) * PAGE_SIZE + 1;
      if(!Number.isSafeInteger(start)){ page = 1; start = 1; }
      var batch = await readFeed(start, PAGE_SIZE, snapshot, signal);
      var total = batch.total;
      var pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
      if(page > pages){ return readPageData(pages, snapshot, signal); }
      var entries = batch.entries.slice(0, PAGE_SIZE);
      var needed = Math.min(PAGE_SIZE, total - start + 1);
      // Fill partial feed responses without confusing a short response with the last page.
      while(entries.length < needed){
        batch = await readFeed(start + entries.length, needed - entries.length, snapshot, signal);
        if(!batch.entries.length || batch.total !== total){ throw new Error('Incomplete feed'); }
        entries = entries.concat(batch.entries).slice(0, needed);
      }
      var ids = new Set();
      entries.forEach(function(entry){
        var id = entry.id && entry.id.$t;
        if(!id || ids.has(id)){ throw new Error('Duplicate feed entry'); }
        ids.add(id);
      });
      return {page:page, pages:pages, total:total, entries:entries};
    }
    function pagination(data){
      var fragment = document.createDocumentFragment();
      var first = data.total ? (data.page - 1) * PAGE_SIZE + 1 : 0;
      var last = first ? first + data.entries.length - 1 : 0;
      fragment.appendChild(element('p', 'cs-page-summary', 'Trang ' + data.page + ' / ' + data.pages + ' · ' + first + '–' + last + ' / ' + data.total + ' bài viết'));
      function control(text, page, className, disabled, relation){
        var node = element(disabled ? 'button' : 'a', className, text);
        node.setAttribute('aria-label', className === 'cs-page-number' ? 'Trang ' + page : text);
        if(disabled){ node.type = 'button'; node.disabled = true; }
        else{
          node.href = pageUrl(page).href;
          node.dataset.csPage = String(page);
          node.setAttribute('aria-controls', grid.id);
          if(relation){ node.rel = relation; }
          if(page === data.page){ node.setAttribute('aria-current', 'page'); }
        }
        fragment.appendChild(node);
      }
      control('Trang trước', data.page - 1, 'blog-pager-newer-link', data.page === 1, 'prev');
      var previous = 0;
      var numbers = Array.from(new Set([1, data.page - 1, data.page, data.page + 1, data.pages]))
        .filter(function(number){ return number >= 1 && number <= data.pages; })
        .sort(function(a, b){ return a - b; });
      numbers.forEach(function(number){
        if(previous && number - previous > 1){ fragment.appendChild(element('span', 'cs-page-gap', '…')); }
        control(String(number), number, 'cs-page-number', false);
        previous = number;
      });
      control('Trang sau', data.page + 1, 'blog-pager-older-link', data.page === data.pages, 'next');
      return fragment;
    }
    async function loadPage(url, navigation){
      var token = ++requestId;
      if(activeController){ activeController.abort(); }
      var controller = new AbortController();
      activeController = controller;
      var timeout = setTimeout(function(){ controller.abort(); }, 15000);
      var page = readPage(url);
      var snapshot = readSnapshot(url, navigation);
      grid.setAttribute('aria-busy', 'true');
      notice.textContent = 'Đang tải bài viết…';
      try{
        var data = await readPageData(page, snapshot, controller.signal);
        if(token !== requestId){ return; }
        var cards = document.createDocumentFragment();
        for(var index = 0; index < data.entries.length; index++){
          cards.appendChild(card(data.entries[index], index));
          // Yield between rows so parsing technical articles does not block scrolling/input.
          if(index % 4 === 3){ await new Promise(function(resolve){ setTimeout(resolve, 0); }); }
          if(token !== requestId){ return; }
        }
        var controls = pagination(data);
        postThumbnails.release(grid);
        grid.replaceChildren(cards);
        grid.classList.remove('cs-catalog-hidden');
        widget.querySelectorAll('.no-posts, .no-posts-message, .status-msg-wrap, .post-filter-message').forEach(function(node){ node.classList.add('cs-native-empty-hidden'); });
        pager.replaceChildren(controls);
        pager.classList.add('cs-pagination');
        pager.setAttribute('aria-label', 'Phân trang bài viết');
        var nextUrl = pageUrl(data.page);
        if(navigation === 'initial'){
          nextUrl.hash = url.hash;
          if(!url.searchParams.has('cs-page')){ nextUrl = new URL(url.href); }
        }
        var state = Object.assign({}, history.state || {}, {csCatalog:true, csBasePath:basePath, csSnapshot:snapshot});
        history[navigation === 'push' ? 'pushState' : 'replaceState'](state, '', nextUrl.href);
        displayedPage = data.page;
        displayedSnapshot = snapshot;
        try{ sessionStorage.setItem(snapshotKey, JSON.stringify({snapshot:snapshot, savedAt:Date.now()})); }catch(error){}
        notice.textContent = data.total ? '' : 'Chưa có bài viết trong chuyên mục này.';
        if(navigation !== 'initial'){
          grid.scrollIntoView({block:'start', behavior:'instant'});
          grid.focus({preventScroll:true});
        }
      }catch(error){
        if(token !== requestId){ return; }
        // A failed request must never display page-one posts under a different page number.
        if(navigation === 'pop' || (displayedPage === null && page > 1)){
          grid.classList.add('cs-catalog-hidden');
          pager.replaceChildren();
        }
        notice.textContent = 'Không tải được danh sách bài viết. ';
        var retry = element('button', '', 'Thử lại');
        retry.type = 'button';
        retry.addEventListener('click', function(){ loadPage(url, navigation); });
        var firstLink = element('a', '', 'Về trang đầu');
        firstLink.href = basePath + '?max-results=' + PAGE_SIZE;
        notice.append(firstLink, retry);
      }finally{
        clearTimeout(timeout);
        if(token === requestId){ grid.removeAttribute('aria-busy'); }
      }
    }
    pager.addEventListener('click', function(event){
      var link = event.target.closest('a[data-cs-page]');
      if(!link || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey){ return; }
      event.preventDefault();
      if(Number(link.dataset.csPage) !== displayedPage){ loadPage(new URL(link.href), 'push'); }
    });
    window.addEventListener('popstate', function(){ loadPage(new URL(window.location.href), 'pop'); });
    loadPage(locationUrl, 'initial');
  });

  onReady(function(){
    var article = document.querySelector('body.item-view .post-body.entry-content');
    var toc = document.querySelector('body.item-view .post-toc');
    if(!article || !toc){
      return;
    }

    var headings = Array.prototype.slice.call(article.querySelectorAll('h2, h3')).filter(function(heading){
      return heading.textContent.trim().length > 0;
    });

    if(headings.length < 3){
      return;
    }

    var list = toc.querySelector('ol');
    if(!list){
      return;
    }

	    var tocLinks = [];
	    headings.forEach(function(heading, index){
	      if(!heading.id){
	        var slug = heading.textContent.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
	        heading.id = slug || ('section-' + (index + 1));
      }

      var item = document.createElement('li');
      if(heading.tagName.toLowerCase() === 'h3'){
        item.className = 'toc-subitem';
      }

      var link = document.createElement('a');
	      link.href = '#' + heading.id;
	      link.textContent = heading.textContent;
	      link.setAttribute('data-toc-target', heading.id);
	      item.appendChild(link);
	      list.appendChild(item);
	      tocLinks.push({
	        heading: heading,
	        link: link
	      });
	    });

	    function setActiveToc(activeHeading){
	      tocLinks.forEach(function(item){
	        if(item.heading === activeHeading){
	          item.link.classList.add('toc-active');
	          item.link.setAttribute('aria-current', 'true');
	        }else{
	          item.link.classList.remove('toc-active');
	          item.link.removeAttribute('aria-current');
	        }
	      });
	    }

	    function updateTocActive(){
	      var activeHeading = headings[0];
	      var offset = 140;
	      headings.forEach(function(heading){
	        if(heading.getBoundingClientRect().top <= offset){
	          activeHeading = heading;
	        }
	      });
	      setActiveToc(activeHeading);
	    }

	    var tocTicking = false;
	    window.addEventListener('scroll', function(){
	      if(!tocTicking){
	        window.requestAnimationFrame(function(){
	          updateTocActive();
	          tocTicking = false;
	        });
	        tocTicking = true;
	      }
	    }, { passive: true });

	    toc.className += ' is-visible';
	    if(toc.parentNode){
	      toc.parentNode.className += ' has-toc';
	    }
	    updateTocActive();
	  });

	  onReady(function(){
	    var rail = document.querySelector('body.item-view .post-related-rail');
	    if(!rail){
	      return;
	    }

	    var list = rail.querySelector('.related-posts-list');
	    if(!list){
	      return;
	    }
	    if(!window.fetch){
	      list.innerHTML = '';
	      return;
	    }

	    var labelNodes = Array.prototype.slice.call(rail.querySelectorAll('[data-related-label]'));
	    var labels = labelNodes.map(function(node){
	      return (node.getAttribute('data-related-label') || '').trim();
	    }).filter(function(label, index, all){
	      return label && all.indexOf(label) === index;
		    }).slice(0, 4);

	    if(!labels.length){
	      list.innerHTML = '';
	      return;
	    }

	    function cleanUrl(url){
	      try{
	        return new URL(url, window.location.href).href.replace(/[?#].*$/, '').replace(/\/$/, '');
	      }catch(error){
	        return String(url || '').replace(/[?#].*$/, '').replace(/\/$/, '');
	      }
	    }

	    function getEntryUrl(entry){
	      var links = entry.link || [];
	      for(var i = 0; i < links.length; i += 1){
	        if(links[i].rel === 'alternate'){
	          return links[i].href;
	        }
	      }
	      return '';
	    }

	    function getEntryTitle(entry){
	      return entry.title && entry.title.$t ? entry.title.$t : 'Untitled note';
	    }

	    function getEntryDate(entry){
	      if(!entry.published || !entry.published.$t){
	        return '';
	      }
	      try{
	        return new Date(entry.published.$t).toLocaleDateString(document.documentElement.lang || undefined, {
	          day: 'numeric',
	          month: 'short',
	          year: 'numeric'
	        });
	      }catch(error){
	        return '';
	      }
	    }

	    var currentUrl = cleanUrl(rail.getAttribute('data-current-url') || window.location.href);
	    var blogUrl = (rail.getAttribute('data-blog-url') || window.location.origin + '/').replace(/\/$/, '');
	    var requests = labels.map(function(label){
		      var url = blogUrl + '/feeds/posts/default/-/' + encodeURIComponent(label) + '?alt=json&max-results=15';
	      return fetch(url, { credentials: 'same-origin' }).then(function(response){
	        return response.ok ? response.json() : null;
	      }).catch(function(){
	        return null;
	      });
	    });

	    Promise.all(requests).then(function(results){
	      var seen = {};
	      var posts = [];
	      results.forEach(function(result){
	        var entries = result && result.feed && result.feed.entry ? result.feed.entry : [];
	        entries.forEach(function(entry){
	          var url = getEntryUrl(entry);
	          var cleaned = cleanUrl(url);
	          if(!url || cleaned === currentUrl || seen[cleaned]){
	            return;
	          }
	          seen[cleaned] = true;
	          posts.push({
	            title: getEntryTitle(entry),
	            url: url,
	            date: getEntryDate(entry)
	          });
	        });
	      });

	      list.innerHTML = '';
	      if(!posts.length){
	        list.innerHTML = '';
	        return;
	      }

		      posts.slice(0, 10).forEach(function(post){
	        var item = document.createElement('div');
	        item.className = 'related-post-item';

	        var link = document.createElement('a');
	        link.href = post.url;
	        link.textContent = post.title;
	        item.appendChild(link);

	        if(post.date){
	          var date = document.createElement('div');
	          date.className = 'related-post-date';
	          date.textContent = post.date;
	          item.appendChild(date);
	        }

	        list.appendChild(item);
	      });
		    });
		  });

  onReady(function initCommentAvatars(){
    var comments = document.getElementById('comments');
    if(!comments){return;}

    function isBloggerDefaultAvatar(src){
      if(!src){return false;}
      try{
        var url = new URL(src, document.baseURI);
        return /(^|\.)(blogblog\.com|blogger\.com)$/i.test(url.hostname) &&
          /^\/img\/(?:b(?:16|35|36)-rounded\.(?:png|gif)|blank\.gif|anon(?:16|36)?\.png)$/i.test(url.pathname);
      }catch(error){return false;}
    }

    function refreshAvatars(){
      comments.querySelectorAll('.avatar-image-container').forEach(function(avatar){
        var img = avatar.querySelector('img');
        var isDefault = img ? isBloggerDefaultAvatar(img.getAttribute('src')) :
          !!avatar.querySelector('.blogger-logo');
        var initials = avatar.querySelector('.comment-initials');
        avatar.classList.toggle('cs-default-avatar', isDefault);
        if(!isDefault){
          if(initials){initials.remove();}
          return;
        }
        var comment = avatar.closest('.comment');
        var author = comment && comment.querySelector('.comment-header .user, .comment-author');
        var name = author ? author.textContent.trim() : '';
        var words = name.split(/\s+/).filter(Boolean);
        var label = words.length ? Array.from(words[0])[0] : '?';
        if(words.length > 1){label += Array.from(words[words.length - 1])[0];}
        label = label.toLocaleUpperCase();
        if(!initials){
          initials = document.createElement('span');
          initials.className = 'comment-initials';
          initials.setAttribute('aria-hidden', 'true');
          avatar.appendChild(initials);
        }
        if(initials.textContent !== label){initials.textContent = label;}
        var profile = avatar.querySelector('a');
        if(profile && !profile.getAttribute('aria-label')){
          profile.setAttribute('aria-label', name || 'Hồ sơ người nhận xét');
        }
      });
    }

    refreshAvatars();
    // Blogger can append threaded replies after the initial document has loaded.
    if(window.MutationObserver){
      var pending = false;
      new MutationObserver(function(){
        if(pending){return;}
        pending = true;
        window.requestAnimationFrame(function(){
          pending = false;
          refreshAvatars();
        });
      }).observe(comments, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
    }
  });

		  onReady(function(){
		    if(!document.body.classList.contains('item-view')){
      return;
    }

    var header = document.querySelector('.centered-top-container');
    var lastY = window.pageYOffset || document.documentElement.scrollTop || 0;
    var mobileHeaderQuery = window.matchMedia ? window.matchMedia('(max-width: 760px)') : null;
    var accumulatedUp = 0;
    var accumulatedDown = 0;
    var ticking = false;

    function isMobileHeaderMode(){
      return mobileHeaderQuery ? mobileHeaderQuery.matches : window.innerWidth <= 760;
    }

    function hasOpenHeaderControl(){
      return document.body.classList.contains('sidebar-visible') ||
        document.body.classList.contains('topic-menu-open') ||
        !!document.querySelector('.search.focused, .search input:focus');
    }

    function setHeaderHidden(hidden){
      if(hidden){
        document.body.classList.add('header-hidden');
      }else{
        document.body.classList.remove('header-hidden');
      }
    }

    function updateHeader(){
      var currentY = window.pageYOffset || document.documentElement.scrollTop || 0;
      var delta = currentY - lastY;
      var isMobile = isMobileHeaderMode();
      var scrollingDown = delta > 8;
      var scrollingUp = delta < -8;

      if(!header){
        ticking = false;
        return;
      }

      if(!isMobile){
        if(currentY < 180 || scrollingUp || hasOpenHeaderControl()){
          setHeaderHidden(false);
        }else if(scrollingDown){
          setHeaderHidden(true);
        }
        lastY = currentY;
        ticking = false;
        return;
      }

      if(currentY < 72 || hasOpenHeaderControl()){
        setHeaderHidden(false);
        accumulatedUp = 0;
        accumulatedDown = 0;
      }else if(delta > 4){
        accumulatedDown += delta;
        accumulatedUp = 0;
        if(currentY > 112 && accumulatedDown >= 24){
          setHeaderHidden(true);
          accumulatedDown = 0;
        }
      }else if(delta < -4){
        accumulatedUp += Math.abs(delta);
        accumulatedDown = 0;
        if(accumulatedUp >= 56 || delta < -18){
          setHeaderHidden(false);
          accumulatedUp = 0;
        }
      }

      lastY = currentY;
      ticking = false;
    }

    window.addEventListener('scroll', function(){
      if(!ticking){
        window.requestAnimationFrame(updateHeader);
        ticking = true;
      }
    }, { passive: true });
  });
})();

// Monetag vignette requests are counted across distinct, engaged article reads (default: two).
//]]>