Showing posts with label Test. Show all posts
Showing posts with label Test. Show all posts

Wednesday, 3 September 2025

Alphabetical List Test Version


<!-- Blogger Alphabetical Post Index - Version 2.24
     Final version with adaptive Blogger feed recovery.
-->

<div id="alphabetical-post-index">
  <p id="api-status">Loading posts...</p>
  <div id="api-results"></div>
</div>

<style>
#alphabetical-post-index { width: 100%; }
#api-status { margin: 0 0 1em 0; }
.post-row {
  margin: 0 0 0.45em 0;
  line-height: 1.4;
}
.post-date {
  color: #777;
  margin-right: 0.9em;
}
.post-link {
  text-decoration: none;
}
.post-link:hover {
  text-decoration: underline;
}
</style>

<script>
(function () {
  "use strict";

  const BLOG_ROOT = window.location.origin + "/";
  const INITIAL_BATCH_SIZE = 100;
  const RECOVERY_SIZES = [50, 25, 10, 5, 1];
  const PARALLEL_REQUESTS = 5;
  const RETRIES = 2;

  const status = document.getElementById("api-status");
  const results = document.getElementById("api-results");

  let callbackCounter = 0;
  let posts = [];

  function jsonp(url) {
    return new Promise(function (resolve, reject) {
      callbackCounter++;

      const callbackName =
        "__bloggerIndexCallback" + callbackCounter;

      const script = document.createElement("script");
      let finished = false;

      const timer = setTimeout(function () {
        if (finished) return;
        finished = true;
        cleanup();
        reject(new Error("Timed out"));
      }, 15000);

      function cleanup() {
        clearTimeout(timer);

        try {
          delete window[callbackName];
        } catch (error) {
          window[callbackName] = undefined;
        }

        if (script.parentNode) {
          script.parentNode.removeChild(script);
        }
      }

      window[callbackName] = function (data) {
        if (finished) return;

        finished = true;
        cleanup();
        resolve(data);
      };

      script.onerror = function () {
        if (finished) return;

        finished = true;
        cleanup();
        reject(new Error("Request failed"));
      };

      script.src =
        url +
        "&callback=" +
        encodeURIComponent(callbackName);

      script.async = true;
      document.body.appendChild(script);
    });
  }

  async function request(start, count) {
    let lastError = "";

    for (let attempt = 0; attempt <= RETRIES; attempt++) {
      try {
        const data =
          await jsonp(
            BLOG_ROOT +
            "feeds/posts/summary?alt=json" +
            "&start-index=" + start +
            "&max-results=" + count
          );

        const entries =
          data &&
          data.feed &&
          data.feed.entry
            ? data.feed.entry
            : [];

        let total = null;

        if (
          data &&
          data.feed &&
          data.feed.openSearch$totalResults &&
          data.feed.openSearch$totalResults.$t !== undefined
        ) {
          total = Number(
            data.feed.openSearch$totalResults.$t
          );
        }

        return {
          start: start,
          requested: count,
          returned: entries.length,
          entries: entries,
          total: total
        };

      } catch (error) {
        lastError = error.message;

        if (attempt < RETRIES) {
          await new Promise(function (resolve) {
            setTimeout(
              resolve,
              300 * (attempt + 1)
            );
          });
        }
      }
    }

    return {
      start: start,
      requested: count,
      returned: -1,
      entries: [],
      total: null,
      error: lastError
    };
  }

  function validEntry(entry) {
    if (!entry) return false;

    const title =
      entry.title &&
      entry.title.$t
        ? entry.title.$t.trim()
        : "";

    let url = "";

    if (entry.link) {
      for (let i = 0; i < entry.link.length; i++) {
        if (
          entry.link[i].rel === "alternate" &&
          entry.link[i].href
        ) {
          url = entry.link[i].href;
          break;
        }
      }
    }

    return !!title && !!url;
  }

  function makePost(entry) {
    let title = entry.title.$t.trim();
    let url = "";

    for (let i = 0; i < entry.link.length; i++) {
      if (
        entry.link[i].rel === "alternate" &&
        entry.link[i].href
      ) {
        url = entry.link[i].href;
        break;
      }
    }

    let date = "";

    if (
      entry.published &&
      entry.published.$t
    ) {
      const d = new Date(entry.published.$t);

      if (!isNaN(d.getTime())) {
        date =
          String(d.getDate()).padStart(2, "0") +
          "/" +
          String(d.getMonth() + 1).padStart(2, "0") +
          "/" +
          d.getFullYear();
      }
    }

    return {
      title: title,
      url: url,
      date: date,
      published: entry.published &&
        entry.published.$t
        ? entry.published.$t
        : ""
    };
  }

  function addEntries(entries) {
    for (const entry of entries) {
      if (!validEntry(entry)) continue;

      const post = makePost(entry);

      if (
        !posts.some(function (existing) {
          return existing.url === post.url;
        })
      ) {
        posts.push(post);
      }
    }

    updateStatus();
  }

  function updateStatus() {
    status.textContent =
      "Loading posts... " +
      posts.length +
      " found";
  }

  function sortPosts() {
    posts.sort(function (a, b) {
      const titleCompare =
        a.title.localeCompare(
          b.title,
          "en",
          {
            sensitivity: "base",
            numeric: true
          }
        );

      if (titleCompare !== 0) {
        return titleCompare;
      }

      return a.published.localeCompare(
        b.published
      );
    });
  }

  function displayPosts() {
    results.innerHTML = "";

    const fragment =
      document.createDocumentFragment();

    for (const post of posts) {
      const row =
        document.createElement("div");

      row.className = "post-row";

      const date =
        document.createElement("span");

      date.className = "post-date";
      date.textContent =
        post.date;

      const link =
        document.createElement("a");

      link.className = "post-link";
      link.href = post.url;
      link.textContent = post.title;
      link.target = "_blank";
      link.rel = "noopener";

      row.appendChild(date);
      row.appendChild(link);

      fragment.appendChild(row);
    }

    results.appendChild(fragment);
  }

  async function recoverRange(
    start,
    end,
    failedResult
  ) {
    const expected =
      end - start + 1;

    for (const size of RECOVERY_SIZES) {
      let position = start;
      let recoveredEntries = [];
      let complete = true;

      while (position <= end) {
        const count =
          Math.min(
            size,
            end - position + 1
          );

        const result =
          await request(
            position,
            count
          );

        if (
          result.returned < 0 ||
          result.returned !== count
        ) {
          complete = false;
          break;
        }

        recoveredEntries =
          recoveredEntries.concat(
            result.entries
          );

        position += count;
      }

      if (complete) {
        addEntries(recoveredEntries);
        return true;
      }
    }

    // Last resort: try every position individually.
    // This should only be reached if all larger recovery
    // sizes fail.
    let position = start;
    let recoveredEntries = [];

    while (position <= end) {
      const result =
        await request(position, 1);

      if (
        result.returned === 1
      ) {
        recoveredEntries.push(
          result.entries[0]
        );
      }

      position++;
    }

    addEntries(recoveredEntries);

    return recoveredEntries.length === expected;
  }

  async function processRange(
    start,
    end
  ) {
    const expected =
      end - start + 1;

    const initial =
      await request(
        start,
        expected
      );

    if (
      initial.returned === expected
    ) {
      addEntries(initial.entries);
      return;
    }

    await recoverRange(
      start,
      end,
      initial
    );
  }

  async function start() {
    try {
      // First request establishes Blogger's reported
      // total number of published posts.
      const first =
        await request(
          1,
          INITIAL_BATCH_SIZE
        );

      if (first.total === null) {
        throw new Error(
          "Blogger did not report a total post count."
        );
      }

      const total =
        first.total;

      if (first.returned === 0) {
        status.textContent =
          "No posts found.";
        return;
      }

      // Process the first range using the response
      // already obtained, avoiding a duplicate request.
      if (
        first.returned ===
        Math.min(
          INITIAL_BATCH_SIZE,
          total
        )
      ) {
        addEntries(first.entries);
      } else {
        await recoverRange(
          1,
          Math.min(
            INITIAL_BATCH_SIZE,
            total
          ),
          first
        );
      }

      // Build all remaining fixed positional ranges.
      const ranges = [];

      for (
        let start = INITIAL_BATCH_SIZE + 1;
        start <= total;
        start += INITIAL_BATCH_SIZE
      ) {
        ranges.push({
          start: start,
          end: Math.min(
            start +
            INITIAL_BATCH_SIZE -
            1,
            total
          )
        });
      }

      // Process ranges in small parallel groups.
      // Each range remains positional, so a short Blogger
      // response can never shift the following range.
      for (
        let i = 0;
        i < ranges.length;
        i += PARALLEL_REQUESTS
      ) {
        const group =
          ranges.slice(
            i,
            i + PARALLEL_REQUESTS
          );

        await Promise.all(
          group.map(function (range) {
            return processRange(
              range.start,
              range.end
            );
          })
        );
      }

      sortPosts();
      displayPosts();

      status.textContent =
        posts.length +
        " posts found";

    } catch (error) {
      status.textContent =
        "Unable to load posts.";

      results.textContent =
        error.message;
    }
  }

  start();

})();
</script>