Skip to content

Latest commit

 

History

History
334 lines (302 loc) · 11.1 KB

File metadata and controls

334 lines (302 loc) · 11.1 KB

kit:: {{ii}}

  • /**
     * Get the icon associated with the linked reference following the macro.
     *
     * @file nextRefIcon.md
     *
     * @usage Use with a logseq macro. Include the macro before a linked reference
     *        to include an icon from that page.
     *
     *        Optional: Specify the number of spaces to prefix the icon with as
     *        an argument.
     * 1. Create a trigger macro in `config.edn`
     *  :macros {
     *    :ii "[:div.kit.inline { :data-kit nextRefIcon :data-arguments \"$1\"} ]"
     *  }
     * 2. Use the `{{ii}}` macro before any linked reference.
     *  e.g. `{{ii}} funny [[mushrooms]] are here.
     * 3. In the target linked reference, include a tabler icon glyph in the -icon
     * page property.
     *  Example: in page `mushrooms`, the first block contains:
     *    `-icon:: f3f3`
     */
    
    /**
     * These functions are used in the main
     *
     * @namespace HelperFunctions
     */
    /**
     * Recursively creates a DOM element or text node from a format object.
     *
     * @function outputFormatter
     * @namespace HelperFunctions.Transformation
     *
     * @param formatObject {string|object} - The format object to process
     */
    const outputFormatter = (formatObject) => {
      // Case 1: String input
      if (typeof formatObject === "string") {
        return document.createTextNode(formatObject);
      }
    
      // Case 2: Object input
      const element = document.createElement(formatObject.element);
      if (formatObject.classNames) {
        element.classList.add(...formatObject.classNames.split(" "));
      }
      if (!formatObject.content) {
        return element;
      }
    
      // Case 3: String content
      if (typeof formatObject.content === "string") {
        element.innerHTML = formatObject.content;
        return element;
      }
    
      // Case 4: Array content
      if (Array.isArray(formatObject.content)) {
        formatObject.content.forEach((childContent) => {
          const childElement = outputFormatter(childContent);
          element.appendChild(childElement);
        });
        return element;
      }
    
      // Case 5: Object content
      const childElement = outputFormatter(formatObject.content);
      element.appendChild(childElement);
      return element;
    };
    
    /**
     * Generate whitespace by processing a logseq macro argument value.
     *
     * @function generateWhitespace
     * @namespace HelperFunctions.Generation
     *
     * @description Logseq macros passed without arguments result in
     * strings such as $1. Reject those.
     *
     * @param {string} whitespace - The number of spaces to generate
     * @returns {string} - A string of zero or more spaces
     */
    function generateWhitespace(whitespace) {
      // Input validation
      if (typeof whitespace === "string" && whitespace.startsWith("$")) {
        return ""; //
      }
    
      const number = Number(whitespace);
      return Number.isInteger(number) && number > 0 ? " ".repeat(number) : "";
    }
    
    /**
     * Get an HTML attribute value from a sibling later in the DOM
     *
     * @function getAttributeValueFromSiblingOfElement
     *
     * @param {HTMLElement} startingElement - The DOM element from which the search begins, ensuring a valid starting point for sibling traversal.
     * @param {string} siblingQuerySelectorPattern - A CSS selector string used to identify the target sibling or one of its descendant elements.
     * @param {string} attributeName - The name of the HTML attribute whose value is to be retrieved from the matching element.
     * @returns {string|null} - The lowercase value of the specified attribute if found; otherwise, null.
     */
    function getAttributeValueFromSiblingOfElement(
      startingElement,
      siblingQuerySelectorPattern,
      attributeName
    ) {
      let currentSiblingElement = startingElement.nextElementSibling;
      let targetAttributeValue = null;
    
      console.group("getAttributeValueFromSiblingOfElement");
      console.log("Starting search from:", startingElement);
      console.log("Looking for:", siblingQuerySelectorPattern);
      console.log("Attribute to extract:", attributeName);
    
      while (currentSiblingElement) {
        console.log("Checking sibling:", currentSiblingElement);
    
        // Case 1: Check if the sibling itself matches
        if (currentSiblingElement.matches(siblingQuerySelectorPattern)) {
          console.log("Direct match found on sibling");
          targetAttributeValue = currentSiblingElement
            .getAttribute(attributeName)
            ?.toLowerCase();
          if (targetAttributeValue) {
            console.log("Found value:", targetAttributeValue);
            break;
          }
        }
    
        // Case 2: Check if any children match
        const matchingElement = currentSiblingElement.querySelector(
          siblingQuerySelectorPattern
        );
        if (matchingElement) {
          console.log("Match found in child:", matchingElement);
          targetAttributeValue = matchingElement
            .getAttribute(attributeName)
            ?.toLowerCase();
          if (targetAttributeValue) {
            console.log("Found value:", targetAttributeValue);
            break;
          }
        }
    
        console.log("No match found, moving to next sibling");
        currentSiblingElement = currentSiblingElement.nextElementSibling;
      }
    
      console.log("Final result:", targetAttributeValue);
      console.groupEnd();
    
      return targetAttributeValue || null;
    }
    
    /**
     * Show a tabler icon for the linked reference in a block following the
     * triggering macro.
     *
     * @function nextRefIcon
     *
     * @async
     * @param {HTMLElement} div - The div element that triggered the macro
     *
     */
    logseq.kits.setStatic(async function nextRefIcon(div) {
      // Inputs
      const leftWhitespace = div.dataset.leftWhitespace;
      const rightWhitespace = div.dataset.rightWhitespace;
      const explicitRef = div.dataset.explicitRef; // new ▸ data-explicit-ref="$3"
    
      // Target element logic ----------------------------------------------
      const startingElement = div.closest("[data-macro-name]");
      const querySelectorPattern = ".page-reference";
      const attributeName = "data-ref";
    
      /**
       * Helper: a macro argument is “real” when it is neither undefined nor the
       * placeholder string produced by Logseq for missing parameters.
       */
      const isSupplied = (arg) => typeof arg === "string" && !arg.startsWith("$");
    
      /* === revised assignment === */
      const nextRefValue = isSupplied(explicitRef)
        ? explicitRef.trim().toLowerCase()
        : getAttributeValueFromSiblingOfElement(
            startingElement,
            querySelectorPattern,
            attributeName
          );
      console.log("nextRef", nextRefValue);
    
      // Data fetching logic
      /**
       * Processes an advanced query using the Logseq datascript query API.
       *
       * @async
       * @function processAdvancedQuery
       *
       * @param {string} queryString - The datascript query string to execute via the Logseq API.
       * @returns {Promise<Array>} - A promise that resolves to an array containing the flattened query results.
       */
      const processAdvancedQuery = async (queryString) => {
        const resultArray = await (async () => {
          const queryResults = await logseq.api
            .datascript_query(queryString)
            ?.flat();
          return queryResults;
        })();
        return await resultArray;
      };
    
      // Function inputs
      const queryStringLibrary = {
        fetchPageIcon: `
          [:find ?icon
          :keys icon
          :where
          [?r :block/name "${nextRefValue}"]
        
          [(identity "0000") ?default-icon]
          (or-join [?r ?default-icon ?icon]
                    (and
                    [?r :block/properties ?props]
                    [(get ?props :-icon) ?icon]
                    [(some? ?icon)])
                    (and
                    ;; :block/properties exists, but :-icon is nil.
                    [?r :block/properties ?props]
                    [(get ?props :-icon :not-found) ?icon-or-not-found]
                    [(= ?icon-or-not-found :not-found)]
                    [(identity ?default-icon) ?icon])
                    (and
                    [(missing? $ ?r :block/properties)]
                    [(identity ?default-icon) ?icon]))
          ]`,
      };
    
      const queryResult = await processAdvancedQuery(
        queryStringLibrary.fetchPageIcon
      );
      console.log("[nextRefIcon] icon:", queryResult[0].icon);
    
      // Output logic
      /**
       * @deprecated 2025-05-19
       * Legacy helper retained solely for reference.
       * Prefer literal objects unless multiple call-sites require the abstraction.
       *
       * Factory function to create a standardized output object for use
       * with outputFormatter.
       *
       * @function legacy.OUTPUT_STRINGS_TEMPLATE
       *
       * @param {string} noResultsString - Content for the "no result" case.
       * @param {string} resultString - Content for the "result" case.
       * @returns {Object} - Object containing the standardized inputs for outputFormatter.
       */
      const legacy = {};
      legacy.OUTPUT_STRINGS_TEMPLATE = (noResultsString, resultString) => ({
        noResults: noResultsString,
        resultString: resultString,
      });
      // eslint-disable-next-line no-unused-vars
      // Intentionally unused until the abstraction is required again.
    
      // Define the actual string values to be used in the output.
      const DEFAULT_NO_RESULTS = "&#x0000;";
      const outputStringValues = {
        noResults: DEFAULT_NO_RESULTS,
        resultString: `${generateWhitespace(leftWhitespace)}&#x${
          queryResult[0].icon
        };${generateWhitespace(rightWhitespace)}`,
      };
    
      /**
       * Format the output text content into an object compatible with outputFormatter.
       *
       * @function formatOutputContent
       *
       * @param {string} textContent - The text content to format.
       * @returns {Object} - The formatted object for output.
       */
      const DEFAULT_ELEMENT = "span";
      const formatOutputContent = (textContent) => ({
        element: DEFAULT_ELEMENT,
        classNames: "bti inline",
        content: textContent,
      });
    
      /**
       * Generate the final output by selecting and formatting content.
       *
       * @function generateOutput
       *
       * @param {Array} queryResult - The result of the query.
       * @param {Object} outputStringsObject - The standardized output strings object.
       * @param {Function} contentFormatterFunction - Function to format the content (default: formatOutputContent).
       * @param {Function} outputFormatFunction - Function to format the final output (default: outputFormatter).
       * @returns {HTMLElement} - The formatted output element.
       */
      const generateOutput = (
        queryResult,
        outputStringsObject,
        contentFormatterFunction = formatOutputContent,
        outputFormatFunction = outputFormatter
      ) => {
        const content =
          queryResult.length === 0
            ? outputStringsObject.noResults
            : outputStringsObject.resultString;
    
        const formattedObject = contentFormatterFunction(content);
        return outputFormatFunction(formattedObject);
      };
    
      const output = generateOutput(
        queryResult,
        outputStringValues,
        formatOutputContent,
        outputFormatter
      );
    
      div.appendChild(output);
    });
    • {{evalparent}}
  • This is