def char_ngrams(s, n=3, token_fn=tokens.on_whitespace):
    """
        Character-level n-grams from within the words in a string.

        By default, the word boundary is assumed to be whitespace.  n-grams are
        not taken across word boundaries, only within words.

        If a word's length is less than or equal to n, the n-grams are simply a
        list with the word itself.

        >>> ng.char_ngrams('This is not a test!')
        ['Thi', 'his', 'is', 'not', 'a', 'tes', 'est', 'st!']

        Therefore some n-grams may have a length less than n, like 'is' and 'a'
        in this example.

        Args:
            s: a string
            n: an int for the n in n-gram
            token_fn: a function that splits a string into a list of strings

        Returns:
            list: strings of char-level n-grams
    """
    tokens = token_fn(s)
    ngram_tuples = [__ngrams(t, n=min(len(t), n)) for t in tokens]
    def unpack(l):
        return sum(l, [])
    def untuple(l):
        return [''.join(t) for t in l]
    return untuple(unpack(ngram_tuples))
Character-level n-grams from within the words in a string.

        By default, the word boundary is assumed to be whitespace.  n-grams are
        not taken across word boundaries, only within words.

        If a word's length is less than or equal to n, the n-grams are simply a
        list with the word itself.

        >>> ng.char_ngrams('This is not a test!')
        ['Thi', 'his', 'is', 'not', 'a', 'tes', 'est', 'st!']

        Therefore some n-grams may have a length less than n, like 'is' and 'a'
        in this example.

        Args:
            s: a string
            n: an int for the n in n-gram
            token_fn: a function that splits a string into a list of strings

        Returns:
            list: strings of char-level n-grams